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