epic-s6ts
[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/my_personal_action",
307     id:     $('#some_input_field').val()
308   };
309   $.post("controller.pl", data, eval_json_result);
310
311 Now some Controller (perl) code for my personal action:
312
313   # my personal action
314   sub action_my_personal_action {
315     my ($self) = @_;
316
317     # Create a new client-side JS object and do stuff with it!
318     my $js = SL::ClientJS->new(controller => $self);
319
320     # Show some element on the page:
321     $js->show('#usually_hidden');
322
323     # Set to hidden inputs. Yes, calls can be chained!
324     $js->val('#hidden_id', $self->new_id)
325        ->val('#other_type', 'Unicorn');
326
327     # Replace some HTML code:
328     my $html = $self->render('SomeController/the_action', { output => 0 });
329     $js->html('#id_with_new_content', $html);
330
331     # Operations on a jstree: rename a node and select it
332     my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
333     $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
334        ->jstree->select_node('#tb-' . $text_block->id);
335
336     # Close a popup opened by kivi.popup_dialog():
337     $js->dialog->close('#jqueryui_popup_dialog');
338
339     # Finally render the JSON response:
340     $self->render($js);
341
342     # Rendering can also be chained, e.g.
343     $js->html('#selector', $html)
344        ->render;
345   }
346
347 =head1 OVERVIEW
348
349 This module enables the generation of jQuery-using JavaScript code on
350 the server side. That code is then evaluated in a safe way on the
351 client side.
352
353 The workflow is usually that the client creates an AJAX request, the
354 server creates some actions and sends them back, and the client then
355 implements each of these actions.
356
357 There are three things that need to be done for this to work:
358
359 =over 2
360
361 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
362
363 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
364
365 =item 3. The server must use this module.
366
367 =back
368
369 The functions called on the client side are mostly jQuery
370 functions. Other functionality may be added later.
371
372 Note that L<SL::Controller/render> is aware of this module which saves
373 you some boilerplate. The following two calls are equivalent:
374
375   $controller->render($client_js);
376   $controller->render(\$client_js->to_json, { type => 'json' });
377
378 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
379
380 =over 4
381
382 =item C<to_array>
383
384 Returns the actions gathered so far as an array reference. Each
385 element is an array reference containing at least two items: the
386 function's name and what it is called on. Additional array elements
387 are the function parameters.
388
389 =item C<to_json>
390
391 Returns the actions gathered so far as a JSON string ready to be sent
392 to the client.
393
394 =item C<render [$controller]>
395
396 Renders C<$self> via the controller. Useful for chaining. Equivalent
397 to the following:
398
399   $controller->render(\$self->to_json, { type => 'json' });
400
401 The controller instance to use can be set during object creation (see
402 synopsis) or as an argument to C<render>.
403
404 =item C<dialog>
405
406 Tells C<$self> that the next action is to be called on a jQuery UI
407 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
408 example:
409
410   $js->dialog->close('#jqueryui_popup_dialog');
411
412 =item C<jstree>
413
414 Tells C<$self> that the next action is to be called on a jstree
415 instance. For example:
416
417   $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
418
419 =back
420
421 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
422
423 =head2 GENERIC FUNCTION
424
425 All of the following functions can be invoked in two ways: either by
426 calling the function name directly on C<$self> or by calling
427 L</action> with the function name as the first parameter. Therefore
428 the following two calls are identical:
429
430   $js->insertAfter($html, '#some-id');
431   $js->action('insertAfter', $html, '#some-id');
432
433 The second form, calling L</action>, is more to type but can be useful
434 in situations in which you have to call one of two functions depending
435 on context. For example, when you want to insert new code in a
436 list. If the list is empty you might have to use C<appendTo>, if it
437 isn't you might have to use C<insertAfter>. Example:
438
439   my $html = $self->render(...);
440   $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
441
442 Instead of:
443
444   my $html = $self->render(...);
445   if ($list_is_empty) {
446     $js->appendTo($html, '#text-block-list');
447   } else {
448     $js->insertAfter($html, '#text-block-' . $self->text_block->id);
449   }
450
451 The first variation is obviously better suited for chaining.
452
453 =over 4
454
455 =item C<action $method, @args>
456
457 Call the function with the name C<$method> on C<$self> with arguments
458 C<@args>. Returns the return value of the actual function
459 called. Useful for chaining (see above).
460
461 =item C<action_if $condition, $method, @args>
462
463 Call the function with the name C<$method> on C<$self> with arguments
464 C<@args> if C<$condition> is trueish. Does nothing otherwise.
465
466 Returns the return value of the actual function called if
467 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
468 (see above).
469
470 This function is equivalent to the following:
471
472   if ($condition) {
473     $obj->$method(@args);
474   }
475
476 But it is easier to integrate into a method call chain, e.g.:
477
478   $js->html('#content', $html)
479      ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
480      ->render($self);
481
482 =back
483
484 =head2 ADDITIONAL FUNCTIONS
485
486 =over 4
487
488 =item C<flash $type, $message>
489
490 Display a C<$message> in the flash of type C<$type>. Multiple calls of
491 C<flash> on the same C<$self> will be merged by type.
492
493 On the client side the flashes of all types will be cleared after each
494 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
495 This clearing can be switched of by the function C<no_flash_clear>
496
497 =item C<flash_detail $type, $message>
498
499 Display a detailed message C<$message> in the flash of type C<$type>. Multiple calls of
500 C<flash_detail> on the same C<$self> will be merged by type.
501 So the flash message can be hold short and the visibility of details can toggled by the user.
502
503 =item C<no_flash_clear>
504
505 No automatic clearing of flash after successful ClientJS call
506
507 =item C<error $message>
508
509 Causes L<to_json> (and therefore L<render>) to output a JSON object
510 that only contains an C<error> field set to this C<$message>. The
511 client will then show the message in the 'error' flash.
512
513 The messages of multiple calls of C<error> on the same C<$self> will
514 be merged.
515
516 =item C<redirect_to $url>
517
518 Redirects the browser window to the new URL by setting the JavaScript
519 property C<window.location.href>. Note that
520 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
521 function if the current request is an AJAX request as determined by
522 L<SL::Request/is_ajax>.
523
524 =back
525
526 =head2 KIVITENDO FUNCTIONS
527
528 The following functions from the C<kivi> namespace are supported:
529
530 =over 4
531
532 =item Displaying stuff
533
534 C<flash> (don't call directly, use L</flash> instead)
535
536 =item Running functions
537
538 C<run>, C<run_once_for>
539
540 =item Widgets
541
542 C<reinit_widgets>
543
544 =back
545
546 =head2 JQUERY FUNCTIONS
547
548 The following jQuery functions are supported:
549
550 =over 4
551
552 =item Basic effects
553
554 C<hide>, C<show>, C<toggle>
555
556 =item DOM insertion, around
557
558 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
559
560 =item DOM insertion, inside
561
562 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
563
564 =item DOM insertion, outside
565
566 C<after>, C<before>, C<insertAfter>, C<insertBefore>
567
568 =item DOM removal
569
570 C<empty>, C<remove>
571
572 =item DOM replacement
573
574 C<replaceAll>, C<replaceWith>
575
576 =item General attributes
577
578 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
579
580 =item Class attributes
581
582 C<addClass>, C<removeClass>, C<toggleClass>
583
584 =item Data storage
585
586 C<data>, C<removeData>
587
588 =item Form Events
589
590 C<focus>
591
592 =item Generic Event Handlers
593
594 C<on>, C<off>, C<one>
595
596 These attach/detach event listeners to specific selectors. The first
597 argument is the selector, the second the name of the events and the
598 third argument is the name of the handler function. That function must
599 already exist when the handler is added.
600
601 =back
602
603 =head2 JQUERY POPUP DIALOG PLUGIN
604
605 Supported functions of the C<popup dialog> plugin to jQuery. They are
606 invoked by first calling C<dialog> in the ClientJS instance and then
607 the function itself:
608
609   $js->dialog->close(...);
610
611 =over 4
612
613 =item Closing and removing the popup
614
615 C<close>
616
617 =back
618
619 =head2 AJAXFORM JQUERY PLUGIN
620
621 The following functions of the C<ajaxForm> plugin to jQuery are
622 supported:
623
624 =over 4
625
626 =item All functions by the generic accessor function:
627
628 C<ajaxForm>
629
630 =back
631
632 =head2 JSTREE JQUERY PLUGIN
633
634 Supported functions of the C<jstree> plugin to jQuery. They are
635 invoked by first calling C<jstree> in the ClientJS instance and then
636 the function itself:
637
638   $js->jstree->open_node(...);
639
640 =over 4
641
642 =item Operations on the whole tree
643
644 C<lock>, C<unlock>
645
646 =item Opening and closing nodes
647
648 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
649 C<close_all>, C<save_opened>, C<reopen>
650
651 =item Modifying nodes
652
653 C<rename_node>, C<delete_node>, C<move_node>
654
655 =item Selecting nodes (from the 'ui' jstree plugin)
656
657 C<select_node>, C<deselect_node>, C<deselect_all>
658
659 =back
660
661 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
662
663 In order to not have to maintain two files (this one and
664 C<js/client_js.js>) there's a script that can parse this file's
665 C<%supported_methods> definition and generate the file
666 C<js/client_js.js> accordingly. The steps are:
667
668 =over 2
669
670 =item 1. Add lines in this file to the C<%supported_methods> hash. The
671 key is the function name and the value is the number of expected
672 parameters. The value can be negative to indicate that the function
673 takes at least the absolute of this value as parameters and optionally
674 more. In such a case the C<E<lt>ARGSE<gt>> format expands to an actual
675 array (and the individual elements if the value is positive>.
676
677 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
678 generate C<js/client_js.js> automatically.
679
680 =item 3. Reload the files in your browser (cleaning its cache can also
681 help).
682
683 =back
684
685 The template file used for generated C<js/client_js.js> is
686 C<scripts/generate_client_js_actions.tpl>.
687
688 =head1 BUGS
689
690 Nothing here yet.
691
692 =head1 AUTHOR
693
694 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
695
696 =cut