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