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