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