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