ClientJS: Unterstützung für jstree.create_node
[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,            # 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   # Data storage
62   data         => 3,
63   removeData   => 2,
64
65   # Form Events
66   focus        => 1,
67
68   # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
69
70   # Operations on the whole tree
71   'jstree:lock'          => 1,
72   'jstree:unlock'        => 1,
73
74   # Opening and closing nodes
75   'jstree:open_node'     => 2,
76   'jstree:open_all'      => 2,
77   'jstree:close_node'    => 2,
78   'jstree:close_all'     => 2,
79   'jstree:toggle_node'   => 2,
80   'jstree:save_opened'   => 1,
81   'jstree:reopen'        => 1,
82
83   # Modifying nodes
84   'jstree:create_node'   => 4,
85   'jstree:rename_node'   => 3,
86   'jstree:delete_node'   => 2,
87   'jstree:move_node'     => 5,
88
89   # Selecting nodes (from the 'ui' plugin to jstree)
90   'jstree:select_node'   => 2,  # $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>, true)
91   'jstree:deselect_node' => 2,
92   'jstree:deselect_all'  => 1,
93 );
94
95 sub AUTOLOAD {
96   our $AUTOLOAD;
97
98   my ($self, @args) = @_;
99
100   my $method        =  $AUTOLOAD;
101   $method           =~ s/.*:://;
102   return if $method eq 'DESTROY';
103   return $self->action($method, @args);
104 }
105
106 sub action {
107   my ($self, $method, @args) = @_;
108
109   $method      =  (delete($self->{_prefix}) || '') . $method;
110   my $num_args =  $supported_methods{$method};
111
112   croak "Unsupported jQuery action: $method"                                                    unless defined $num_args;
113   croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if     scalar(@args) != $num_args;
114
115   if ($num_args) {
116     # Force flattening from SL::Presenter::EscapedText: "" . $...
117     $args[0] =  "" . $args[0];
118     $args[0] =~ s/^\s+//;
119   }
120
121   push @{ $self->_actions }, [ $method, @args ];
122
123   return $self;
124 }
125
126 sub init__actions {
127   return [];
128 }
129
130 sub init__flash {
131   return {};
132 }
133
134 sub init__error {
135   return '';
136 }
137
138 sub to_json {
139   my ($self) = @_;
140
141   return SL::JSON::to_json({ error        => $self->_error   }) if $self->_error;
142   return SL::JSON::to_json({ eval_actions => $self->_actions });
143 }
144
145 sub to_array {
146   my ($self) = @_;
147   return $self->_actions;
148 }
149
150 sub render {
151   my ($self, $controller) = @_;
152   return $controller->render(\$self->to_json, { type => 'json' });
153 }
154
155 sub jstree {
156   my ($self) = @_;
157   $self->{_prefix} = 'jstree:';
158   return $self;
159 }
160
161 sub flash {
162   my ($self, $type, @messages) = @_;
163
164   my $message = join ' ', grep { $_ } @messages;
165
166   if (!$self->_flash->{$type}) {
167     $self->_flash->{$type} = [ 'flash', $type, $message ];
168     push @{ $self->_actions }, $self->_flash->{$type};
169   } else {
170     $self->_flash->{$type}->[-1] .= ' ' . $message;
171   }
172
173   return $self;
174 }
175
176 sub error {
177   my ($self, @messages) = @_;
178
179   $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
180
181   return $self;
182 }
183
184 1;
185 __END__
186
187 =pod
188
189 =encoding utf8
190
191 =head1 NAME
192
193 SL::ClientJS - Easy programmatic client-side JavaScript generation
194 with jQuery
195
196 =head1 SYNOPSIS
197
198 First some JavaScript code:
199
200   // In the client generate an AJAX request whose 'success' handler
201   // calls "eval_json_response(data)":
202   var data = {
203     action: "SomeController/the_action",
204     id:     $('#some_input_field').val()
205   };
206   $.post("controller.pl", data, eval_json_response);
207
208 Now some Perl code:
209
210   # In the controller itself. First, make sure that the "client_js.js"
211   # is loaded. This must be done when the whole side is loaded, so
212   # it's not in the action called by the AJAX request shown above.
213   $::request->layout->use_javascript('client_js.js');
214
215   # Now in that action called via AJAX:
216   sub action_the_action {
217     my ($self) = @_;
218
219     # Create a new client-side JS object and do stuff with it!
220     my $js = SL::ClientJS->new;
221
222     # Show some element on the page:
223     $js->show('#usually_hidden');
224
225     # Set to hidden inputs. Yes, calls can be chained!
226     $js->val('#hidden_id', $self->new_id)
227        ->val('#other_type', 'Unicorn');
228
229     # Replace some HTML code:
230     my $html = $self->render('SomeController/the_action', { output => 0 });
231     $js->html('#id_with_new_content', $html);
232
233     # Operations on a jstree: rename a node and select it
234     my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
235     $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
236        ->jstree->select_node('#tb-' . $text_block->id);
237
238     # Finally render the JSON response:
239     $self->render($js);
240
241     # Rendering can also be chained, e.g.
242     $js->html('#selector', $html)
243        ->render($self);
244   }
245
246 =head1 OVERVIEW
247
248 This module enables the generation of jQuery-using JavaScript code on
249 the server side. That code is then evaluated in a safe way on the
250 client side.
251
252 The workflow is usally that the client creates an AJAX request, the
253 server creates some actions and sends them back, and the client then
254 implements each of these actions.
255
256 There are three things that need to be done for this to work:
257
258 =over 2
259
260 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
261
262 =item 2. The client code needs to call C<eval_json_response()> with the result returned from the server.
263
264 =item 3. The server must use this module.
265
266 =back
267
268 The functions called on the client side are mostly jQuery
269 functions. Other functionality may be added later.
270
271 Note that L<SL::Controller/render> is aware of this module which saves
272 you some boilerplate. The following two calls are equivalent:
273
274   $controller->render($client_js);
275   $controller->render(\$client_js->to_json, { type => 'json' });
276
277 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
278
279 =over 4
280
281 =item C<to_array>
282
283 Returns the actions gathered so far as an array reference. Each
284 element is an array reference containing at least two items: the
285 function's name and what it is called on. Additional array elements
286 are the function parameters.
287
288 =item C<to_json>
289
290 Returns the actions gathered so far as a JSON string ready to be sent
291 to the client.
292
293 =item C<render $controller>
294
295 Renders C<$self> via the controller. Useful for chaining. Equivalent
296 to the following:
297
298   $controller->render(\$self->to_json, { type => 'json' });
299
300 =item C<jstree>
301
302 Tells C<$self> that the next action is to be called on a jstree
303 instance. For example:
304
305   $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
306
307 =back
308
309 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
310
311 =head2 GENERIC FUNCTION
312
313 All of the following functions can be invoked in two ways: either by
314 calling the function name directly on C<$self> or by calling
315 L</action> with the function name as the first parameter. Therefore
316 the following two calls are identical:
317
318   $js->insertAfter($html, '#some-id');
319   $js->action('insertAfter', $html, '#some-id');
320
321 The second form, calling L</action>, is more to type but can be useful
322 in situations in which you have to call one of two functions depending
323 on context. For example, when you want to insert new code in a
324 list. If the list is empty you might have to use C<appendTo>, if it
325 isn't you might have to use C<insertAfter>. Example:
326
327   my $html = $self->render(...);
328   $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
329
330 Instead of:
331
332   my $html = $self->render(...);
333   if ($list_is_empty) {
334     $js->appendTo($html, '#text-block-list');
335   } else {
336     $js->insertAfter($html, '#text-block-' . $self->text_block->id);
337   }
338
339 The first variation is obviously better suited for chaining.
340
341 Additional functions:
342
343 =over 4
344
345 =item C<flash $type, $message>
346
347 Display a C<$message> in the flash of type C<$type>. Multiple calls of
348 C<flash> on the same C<$self> will be merged by type.
349
350 On the client side the flash of this type will be cleared before the
351 message is shown.
352
353 =item C<error $message>
354
355 Causes L<to_json> (and therefore L<render>) to output a JSON object
356 that only contains an C<error> field set to this C<$message>. The
357 client will then show the message in the 'error' flash.
358
359 The messages of multiple calls of C<error> on the same C<$self> will
360 be merged.
361
362 =back
363
364 =head2 JQUERY FUNCTIONS
365
366 The following jQuery functions are supported:
367
368 =over 4
369
370 =item Basic effects
371
372 C<hide>, C<show>, C<toggle>
373
374 =item DOM insertion, around
375
376 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
377
378 =item DOM insertion, inside
379
380 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
381
382 =item DOM insertion, outside
383
384 C<after>, C<before>, C<insertAfter>, C<insertBefore>
385
386 =item DOM removal
387
388 C<empty>, C<remove>
389
390 =item DOM replacement
391
392 C<replaceAll>, C<replaceWith>
393
394 =item General attributes
395
396 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
397
398 =item Data storage
399
400 C<data>, C<removeData>
401
402 =item Form Events
403
404 C<focus>
405
406 =back
407
408 =head2 JSTREE JQUERY PLUGIN
409
410 The following functions of the C<jstree> plugin to jQuery are
411 supported:
412
413 =over 4
414
415 =item Operations on the whole tree
416
417 C<lock>, C<unlock>
418
419 =item Opening and closing nodes
420
421 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
422 C<close_all>, C<save_opened>, C<reopen>
423
424 =item Modifying nodes
425
426 C<rename_node>, C<delete_node>, C<move_node>
427
428 =item Selecting nodes (from the 'ui' jstree plugin)
429
430 C<select_node>, C<deselect_node>, C<deselect_all>
431
432 =back
433
434 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
435
436 In order not having to maintain two files (this one and
437 C<js/client_js.js>) there's a script that can parse this file's
438 C<%supported_methods> definition and generate the file
439 C<js/client_js.js> accordingly. The steps are:
440
441 =over 2
442
443 =item 1. Add lines in this file to the C<%supported_methods> hash. The
444 key is the function name and the value is the number of expected
445 parameters.
446
447 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
448 generate C<js/client_js.js> automatically.
449
450 =item 3. Reload the files in your browser (cleaning its cache can also
451 help).
452
453 =back
454
455 The template file used for generated C<js/client_js.js> is
456 C<scripts/generate_client_js_actions.tpl>.
457
458 =head1 BUGS
459
460 Nothing here yet.
461
462 =head1 AUTHOR
463
464 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
465
466 =cut