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