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