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