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