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