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