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