Flashanzeige erweitert: Löschen und Springen abschalten
[kivitendo-erp.git] / SL / ClientJS.pm
1 package SL::ClientJS;
2
3 use strict;
4
5 use parent qw(Rose::Object);
6
7 use Carp;
8 use SL::JSON ();
9
10 use Rose::Object::MakeMethods::Generic
11 (
12   scalar                  => [ qw() ],
13   'scalar --get_set_init' => [ qw(controller _actions _flash _flash_detail _error) ],
14 );
15
16 my %supported_methods = (
17   # ## jQuery basics ##
18
19   # Basic effects
20   hide         => 1,
21   show         => 1,
22   toggle       => 1,
23
24   # DOM insertion, around
25   unwrap       => 1,
26   wrap         => 2,
27   wrapAll      => 2,
28   wrapInner    => 2,
29
30   # DOM insertion, inside
31   append       => 2,
32   appendTo     => 2,
33   html         => 2,
34   prepend      => 2,
35   prependTo    => 2,
36   text         => 2,
37
38   # DOM insertion, outside
39   after        => 2,
40   before       => 2,
41   insertAfter  => 2,
42   insertBefore => 2,
43
44   # DOM removal
45   empty        => 1,
46   remove       => 1,
47
48   # DOM replacement
49   replaceAll   => 2,
50   replaceWith  => 2,
51
52   # General attributes
53   attr         => 3,
54   prop         => 3,
55   removeAttr   => 2,
56   removeProp   => 2,
57   val          => 2,
58
59   # Class attribute
60   addClass     => 2,
61   removeClass  => 2,
62   toggleClass  => 2,
63
64   # Data storage
65   data         => 3,
66   removeData   => 2,
67
68   # Form Events
69   focus        => 1, # kivi.set_focus(<TARGET>)
70
71   # Generic Event Handling ## pattern: $(<TARGET>).<FUNCTION>(<ARG1>, kivi.get_function_by_name(<ARG2>))
72   on           => 3,
73   off          => 3,
74   one          => 3,
75
76   # ## jQuery UI dialog plugin ## pattern: $(<TARGET>).dialog('<FUNCTION>')
77
78   # Opening and closing and closing a popup
79   'dialog:open'          => 1, # kivi.popup_dialog(<TARGET>)
80   'dialog:close'         => 1,
81
82   # ## jQuery Form plugin ##
83   'ajaxForm'             => 1, # pattern: $(<TARGET>).ajaxForm({ success: eval_json_result })
84
85   # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
86
87   # Operations on the whole tree
88   'jstree:lock'          => 1,
89   'jstree:unlock'        => 1,
90
91   # Opening and closing nodes
92   'jstree:open_node'     => 2,
93   'jstree:open_all'      => 2,
94   'jstree:close_node'    => 2,
95   'jstree:close_all'     => 2,
96   'jstree:toggle_node'   => 2,
97   'jstree:save_opened'   => 1,
98   'jstree:reopen'        => 1,
99
100   # Modifying nodes
101   'jstree:create_node'   => 4,
102   'jstree:rename_node'   => 3,
103   'jstree:delete_node'   => 2,
104   'jstree:move_node'     => 5,
105
106   # Selecting nodes (from the 'ui' plugin to jstree)
107   'jstree:select_node'   => 2,  # $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>, true)
108   'jstree:deselect_node' => 2,
109   'jstree:deselect_all'  => 1,
110
111   # ## ckeditor stuff ##
112   'focus_ckeditor'       => 1,  # kivi.focus_ckeditor_when_ready(<TARGET>)
113
114   # ## other stuff ##
115   redirect_to            => 1,  # window.location.href = <TARGET>
116
117   flash                  => 2,  # kivi.display_flash(<TARGET>, <ARGS>)
118   flash_detail           => 2,  # kivi.display_flash_detail(<TARGET>, <ARGS>)
119   clear_flash            => 2,  # kivi.display_flash_detail(<TARGET>, <ARGS>)
120   reinit_widgets         => 0,  # kivi.reinit_widgets()
121   run                    => -1, # kivi.run(<TARGET>, <ARGS>)
122   run_once_for           => 3,  # kivi.run_once_for(<TARGET>, <ARGS>)
123
124   scroll_into_view       => 1,  # $(<TARGET>)[0].scrollIntoView()
125 );
126
127 my %trim_target_for = map { ($_ => 1) } qw(insertAfter insertBefore appendTo prependTo);
128
129 sub AUTOLOAD {
130   our $AUTOLOAD;
131
132   my ($self, @args) = @_;
133
134   my $method        =  $AUTOLOAD;
135   $method           =~ s/.*:://;
136   return if $method eq 'DESTROY';
137   return $self->action($method, @args);
138 }
139
140 sub action {
141   my ($self, $method, @args) = @_;
142
143   $method      =  (delete($self->{_prefix}) || '') . $method;
144   my $num_args =  $supported_methods{$method};
145
146   croak "Unsupported jQuery action: $method" unless defined $num_args;
147
148   if ($num_args > 0) {
149     croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)"          if scalar(@args) != $num_args;
150   } else {
151     $num_args *= -1;
152     croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted at least: $num_args)" if scalar(@args) < $num_args;
153     $num_args  = scalar @args;
154   }
155
156   foreach my $idx (0..$num_args - 1) {
157     # Force flattening from SL::Presenter::EscapedText.
158     $args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
159   }
160
161   # Trim leading whitespaces for certain jQuery functions that operate
162   # on HTML code: $("<p>test</p>").appendTo('#some-id'). jQuery croaks
163   # on leading whitespaces, e.g. on $(" <p>test</p>").
164   $args[0] =~ s{^\s+}{} if $trim_target_for{$method};
165
166   push @{ $self->_actions }, [ $method, @args ];
167
168   return $self;
169 }
170
171 sub action_if {
172   my ($self, $condition, @args) = @_;
173
174   return $condition ? $self->action(@args) : $self;
175 }
176
177 sub init__actions {
178   return [];
179 }
180
181 sub init__flash {
182   return {};
183 }
184
185 sub init__flash_detail {
186   return {};
187 }
188
189 sub init__error {
190   return '';
191 }
192
193 sub to_json {
194   my ($self) = @_;
195
196   return SL::JSON::to_json({ error        => $self->_error   }) if $self->_error;
197   return SL::JSON::to_json({ eval_actions => $self->_actions });
198 }
199
200 sub to_array {
201   my ($self) = @_;
202   return $self->_actions;
203 }
204
205 sub render {
206   my ($self, $controller) = @_;
207   $controller ||= $self->controller;
208   $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
209   return $controller->render(\$self->to_json, { type => 'json' });
210 }
211
212 sub jstree {
213   my ($self) = @_;
214   $self->{_prefix} = 'jstree:';
215   return $self;
216 }
217
218 sub dialog {
219   my ($self) = @_;
220   $self->{_prefix} = 'dialog:';
221   return $self;
222 }
223
224 sub ckeditor {
225   my ($self) = @_;
226   $self->{_prefix} = 'ckeditor:';
227   return $self;
228 }
229
230 sub flash {
231   my ($self, $type, @messages) = @_;
232
233   my $message = join ' ', grep { $_ } @messages;
234
235   if (!$self->_flash->{$type}) {
236     $self->_flash->{$type} = [ 'flash', $type, $message ];
237     push @{ $self->_actions }, $self->_flash->{$type};
238   } else {
239     $self->_flash->{$type}->[-1] .= ' ' . $message;
240   }
241
242   return $self;
243 }
244
245 sub flash_detail {
246   my ($self, $type, @messages) = @_;
247
248   my $message = join '<br>', grep { $_ } @messages;
249
250   if (!$self->_flash_detail->{$type}) {
251     $self->_flash_detail->{$type} = [ 'flash_detail', $type, $message ];
252     push @{ $self->_actions }, $self->_flash_detail->{$type};
253   } else {
254     $self->_flash_detail->{$type}->[-1] .= ' ' . $message;
255   }
256
257   return $self;
258 }
259
260 sub error {
261   my ($self, @messages) = @_;
262
263   $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
264
265   return $self;
266 }
267
268 sub init_controller {
269   # fallback
270   require SL::Controller::Base;
271   SL::Controller::Base->new;
272 }
273
274 1;
275 __END__
276
277 =pod
278
279 =encoding utf8
280
281 =head1 NAME
282
283 SL::ClientJS - Easy programmatic client-side JavaScript generation
284 with jQuery
285
286 =head1 SYNOPSIS
287
288 First some JavaScript code:
289
290   // In the client generate an AJAX request whose 'success' handler
291   // calls "eval_json_result(data)":
292   var data = {
293     action: "SomeController/the_action",
294     id:     $('#some_input_field').val()
295   };
296   $.post("controller.pl", data, eval_json_result);
297
298 Now some Perl code:
299
300   # In the controller itself. First, make sure that the "client_js.js"
301   # is loaded. This must be done when the whole side is loaded, so
302   # it's not in the action called by the AJAX request shown above.
303   $::request->layout->use_javascript('client_js.js');
304
305   # Now in that action called via AJAX:
306   sub action_the_action {
307     my ($self) = @_;
308
309     # Create a new client-side JS object and do stuff with it!
310     my $js = SL::ClientJS->new(controller => $self);
311
312     # Show some element on the page:
313     $js->show('#usually_hidden');
314
315     # Set to hidden inputs. Yes, calls can be chained!
316     $js->val('#hidden_id', $self->new_id)
317        ->val('#other_type', 'Unicorn');
318
319     # Replace some HTML code:
320     my $html = $self->render('SomeController/the_action', { output => 0 });
321     $js->html('#id_with_new_content', $html);
322
323     # Operations on a jstree: rename a node and select it
324     my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
325     $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
326        ->jstree->select_node('#tb-' . $text_block->id);
327
328     # Close a popup opened by kivi.popup_dialog():
329     $js->dialog->close('#jqueryui_popup_dialog');
330
331     # Finally render the JSON response:
332     $self->render($js);
333
334     # Rendering can also be chained, e.g.
335     $js->html('#selector', $html)
336        ->render;
337   }
338
339 =head1 OVERVIEW
340
341 This module enables the generation of jQuery-using JavaScript code on
342 the server side. That code is then evaluated in a safe way on the
343 client side.
344
345 The workflow is usally that the client creates an AJAX request, the
346 server creates some actions and sends them back, and the client then
347 implements each of these actions.
348
349 There are three things that need to be done for this to work:
350
351 =over 2
352
353 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
354
355 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
356
357 =item 3. The server must use this module.
358
359 =back
360
361 The functions called on the client side are mostly jQuery
362 functions. Other functionality may be added later.
363
364 Note that L<SL::Controller/render> is aware of this module which saves
365 you some boilerplate. The following two calls are equivalent:
366
367   $controller->render($client_js);
368   $controller->render(\$client_js->to_json, { type => 'json' });
369
370 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
371
372 =over 4
373
374 =item C<to_array>
375
376 Returns the actions gathered so far as an array reference. Each
377 element is an array reference containing at least two items: the
378 function's name and what it is called on. Additional array elements
379 are the function parameters.
380
381 =item C<to_json>
382
383 Returns the actions gathered so far as a JSON string ready to be sent
384 to the client.
385
386 =item C<render [$controller]>
387
388 Renders C<$self> via the controller. Useful for chaining. Equivalent
389 to the following:
390
391   $controller->render(\$self->to_json, { type => 'json' });
392
393 The controller instance to use can be set during object creation (see
394 synopsis) or as an argument to C<render>.
395
396 =item C<dialog>
397
398 Tells C<$self> that the next action is to be called on a jQuery UI
399 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
400 example:
401
402   $js->dialog->close('#jqueryui_popup_dialog');
403
404 =item C<jstree>
405
406 Tells C<$self> that the next action is to be called on a jstree
407 instance. For example:
408
409   $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
410
411 =back
412
413 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
414
415 =head2 GENERIC FUNCTION
416
417 All of the following functions can be invoked in two ways: either by
418 calling the function name directly on C<$self> or by calling
419 L</action> with the function name as the first parameter. Therefore
420 the following two calls are identical:
421
422   $js->insertAfter($html, '#some-id');
423   $js->action('insertAfter', $html, '#some-id');
424
425 The second form, calling L</action>, is more to type but can be useful
426 in situations in which you have to call one of two functions depending
427 on context. For example, when you want to insert new code in a
428 list. If the list is empty you might have to use C<appendTo>, if it
429 isn't you might have to use C<insertAfter>. Example:
430
431   my $html = $self->render(...);
432   $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
433
434 Instead of:
435
436   my $html = $self->render(...);
437   if ($list_is_empty) {
438     $js->appendTo($html, '#text-block-list');
439   } else {
440     $js->insertAfter($html, '#text-block-' . $self->text_block->id);
441   }
442
443 The first variation is obviously better suited for chaining.
444
445 =over 4
446
447 =item C<action $method, @args>
448
449 Call the function with the name C<$method> on C<$self> with arguments
450 C<@args>. Returns the return value of the actual function
451 called. Useful for chaining (see above).
452
453 =item C<action_if $condition, $method, @args>
454
455 Call the function with the name C<$method> on C<$self> with arguments
456 C<@args> if C<$condition> is trueish. Does nothing otherwise.
457
458 Returns the return value of the actual function called if
459 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
460 (see above).
461
462 This function is equivalent to the following:
463
464   if ($condition) {
465     $obj->$method(@args);
466   }
467
468 But it is easier to integrate into a method call chain, e.g.:
469
470   $js->html('#content', $html)
471      ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
472      ->render($self);
473
474 =back
475
476 =head2 ADDITIONAL FUNCTIONS
477
478 =over 4
479
480 =item C<flash $type, $message>
481
482 Display a C<$message> in the flash of type C<$type>. Multiple calls of
483 C<flash> on the same C<$self> will be merged by type.
484
485 On the client side the flashes of all types will be cleared after each
486 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
487
488 =item C<error $message>
489
490 Causes L<to_json> (and therefore L<render>) to output a JSON object
491 that only contains an C<error> field set to this C<$message>. The
492 client will then show the message in the 'error' flash.
493
494 The messages of multiple calls of C<error> on the same C<$self> will
495 be merged.
496
497 =item C<redirect_to $url>
498
499 Redirects the browser window to the new URL by setting the JavaScript
500 property C<window.location.href>. Note that
501 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
502 function if the current request is an AJAX request as determined by
503 L<SL::Request/is_ajax>.
504
505 =back
506
507 =head2 KIVITENDO FUNCTIONS
508
509 The following functions from the C<kivi> namespace are supported:
510
511 =over 4
512
513 =item Displaying stuff
514
515 C<flash> (don't call directly, use L</flash> instead)
516
517 =item Running functions
518
519 C<run>, C<run_once_for>
520
521 =item Widgets
522
523 C<reinit_widgets>
524
525 =back
526
527 =head2 JQUERY FUNCTIONS
528
529 The following jQuery functions are supported:
530
531 =over 4
532
533 =item Basic effects
534
535 C<hide>, C<show>, C<toggle>
536
537 =item DOM insertion, around
538
539 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
540
541 =item DOM insertion, inside
542
543 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
544
545 =item DOM insertion, outside
546
547 C<after>, C<before>, C<insertAfter>, C<insertBefore>
548
549 =item DOM removal
550
551 C<empty>, C<remove>
552
553 =item DOM replacement
554
555 C<replaceAll>, C<replaceWith>
556
557 =item General attributes
558
559 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
560
561 =item Class attributes
562
563 C<addClass>, C<removeClass>, C<toggleClass>
564
565 =item Data storage
566
567 C<data>, C<removeData>
568
569 =item Form Events
570
571 C<focus>
572
573 =item Generic Event Handlers
574
575 C<on>, C<off>, C<one>
576
577 These attach/detach event listeners to specific selectors. The first
578 argument is the selector, the second the name of the events and the
579 third argument is the name of the handler function. That function must
580 already exist when the handler is added.
581
582 =back
583
584 =head2 JQUERY POPUP DIALOG PLUGIN
585
586 Supported functions of the C<popup dialog> plugin to jQuery. They are
587 invoked by first calling C<dialog> in the ClientJS instance and then
588 the function itself:
589
590   $js->dialog->close(...);
591
592 =over 4
593
594 =item Closing and removing the popup
595
596 C<close>
597
598 =back
599
600 =head2 AJAXFORM JQUERY PLUGIN
601
602 The following functions of the C<ajaxForm> plugin to jQuery are
603 supported:
604
605 =over 4
606
607 =item All functions by the generic accessor function:
608
609 C<ajaxForm>
610
611 =back
612
613 =head2 JSTREE JQUERY PLUGIN
614
615 Supported functions of the C<jstree> plugin to jQuery. They are
616 invoked by first calling C<jstree> in the ClientJS instance and then
617 the function itself:
618
619   $js->jstree->open_node(...);
620
621 =over 4
622
623 =item Operations on the whole tree
624
625 C<lock>, C<unlock>
626
627 =item Opening and closing nodes
628
629 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
630 C<close_all>, C<save_opened>, C<reopen>
631
632 =item Modifying nodes
633
634 C<rename_node>, C<delete_node>, C<move_node>
635
636 =item Selecting nodes (from the 'ui' jstree plugin)
637
638 C<select_node>, C<deselect_node>, C<deselect_all>
639
640 =back
641
642 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
643
644 In order not having to maintain two files (this one and
645 C<js/client_js.js>) there's a script that can parse this file's
646 C<%supported_methods> definition and generate the file
647 C<js/client_js.js> accordingly. The steps are:
648
649 =over 2
650
651 =item 1. Add lines in this file to the C<%supported_methods> hash. The
652 key is the function name and the value is the number of expected
653 parameters. The value can be negative to indicate that the function
654 takes at least the absolute of this value as parameters and optionally
655 more. In such a case the C<E<lt>ARGSE<gt>> format expands to an actual
656 array (and the individual elements if the value is positive>.
657
658 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
659 generate C<js/client_js.js> automatically.
660
661 =item 3. Reload the files in your browser (cleaning its cache can also
662 help).
663
664 =back
665
666 The template file used for generated C<js/client_js.js> is
667 C<scripts/generate_client_js_actions.tpl>.
668
669 =head1 BUGS
670
671 Nothing here yet.
672
673 =head1 AUTHOR
674
675 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
676
677 =cut