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