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