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