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