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