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