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