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