5 use parent qw(Rose::Object);
10 use Rose::Object::MakeMethods::Generic
12 'scalar --get_set_init' => [ qw(_actions _flash _error) ],
15 my %supported_methods = (
23 # DOM insertion, around
29 # DOM insertion, inside
37 # DOM insertion, outside
68 focus => 1, # kivi.set_focus(<TARGET>)
70 # Generic Event Handling ## pattern: $(<TARGET>).<FUNCTION>(<ARG1>, kivi.get_function_by_name(<ARG2>))
75 # ## jQuery UI dialog plugin ## pattern: $(<TARGET>).dialog('<FUNCTION>')
77 # Closing and removing the popup
80 # ## jQuery Form plugin ##
81 'ajaxForm' => 1, # pattern: $(<TARGET>).ajaxForm({ success: eval_json_result })
83 # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
85 # Operations on the whole tree
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,
99 'jstree:create_node' => 4,
100 'jstree:rename_node' => 3,
101 'jstree:delete_node' => 2,
102 'jstree:move_node' => 5,
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,
109 # ## ckeditor stuff ##
110 'focus_ckeditor' => 1, # kivi.focus_ckeditor_when_ready(<TARGET>)
113 redirect_to => 1, # window.location.href = <TARGET>
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>)
124 my ($self, @args) = @_;
126 my $method = $AUTOLOAD;
128 return if $method eq 'DESTROY';
129 return $self->action($method, @args);
133 my ($self, $method, @args) = @_;
135 $method = (delete($self->{_prefix}) || '') . $method;
136 my $num_args = $supported_methods{$method};
138 croak "Unsupported jQuery action: $method" unless defined $num_args;
141 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
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;
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]);
154 push @{ $self->_actions }, [ $method, @args ];
160 my ($self, $condition, @args) = @_;
162 return $condition ? $self->action(@args) : $self;
180 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
181 return SL::JSON::to_json({ eval_actions => $self->_actions });
186 return $self->_actions;
190 my ($self, $controller) = @_;
191 $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
192 return $controller->render(\$self->to_json, { type => 'json' });
197 $self->{_prefix} = 'jstree:';
203 $self->{_prefix} = 'dialog:';
209 $self->{_prefix} = 'ckeditor:';
214 my ($self, $type, @messages) = @_;
216 my $message = join ' ', grep { $_ } @messages;
218 if (!$self->_flash->{$type}) {
219 $self->_flash->{$type} = [ 'flash', $type, $message ];
220 push @{ $self->_actions }, $self->_flash->{$type};
222 $self->_flash->{$type}->[-1] .= ' ' . $message;
229 my ($self, @messages) = @_;
231 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
245 SL::ClientJS - Easy programmatic client-side JavaScript generation
250 First some JavaScript code:
252 // In the client generate an AJAX request whose 'success' handler
253 // calls "eval_json_result(data)":
255 action: "SomeController/the_action",
256 id: $('#some_input_field').val()
258 $.post("controller.pl", data, eval_json_result);
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');
267 # Now in that action called via AJAX:
268 sub action_the_action {
271 # Create a new client-side JS object and do stuff with it!
272 my $js = SL::ClientJS->new;
274 # Show some element on the page:
275 $js->show('#usually_hidden');
277 # Set to hidden inputs. Yes, calls can be chained!
278 $js->val('#hidden_id', $self->new_id)
279 ->val('#other_type', 'Unicorn');
281 # Replace some HTML code:
282 my $html = $self->render('SomeController/the_action', { output => 0 });
283 $js->html('#id_with_new_content', $html);
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);
290 # Close a popup opened by kivi.popup_dialog():
291 $js->dialog->close('#jqueryui_popup_dialog');
293 # Finally render the JSON response:
296 # Rendering can also be chained, e.g.
297 $js->html('#selector', $html)
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
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.
311 There are three things that need to be done for this to work:
315 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
317 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
319 =item 3. The server must use this module.
323 The functions called on the client side are mostly jQuery
324 functions. Other functionality may be added later.
326 Note that L<SL::Controller/render> is aware of this module which saves
327 you some boilerplate. The following two calls are equivalent:
329 $controller->render($client_js);
330 $controller->render(\$client_js->to_json, { type => 'json' });
332 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
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.
345 Returns the actions gathered so far as a JSON string ready to be sent
348 =item C<render $controller>
350 Renders C<$self> via the controller. Useful for chaining. Equivalent
353 $controller->render(\$self->to_json, { type => 'json' });
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
361 $js->dialog->close('#jqueryui_popup_dialog');
365 Tells C<$self> that the next action is to be called on a jstree
366 instance. For example:
368 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
372 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
374 =head2 GENERIC FUNCTION
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:
381 $js->insertAfter($html, '#some-id');
382 $js->action('insertAfter', $html, '#some-id');
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:
390 my $html = $self->render(...);
391 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
395 my $html = $self->render(...);
396 if ($list_is_empty) {
397 $js->appendTo($html, '#text-block-list');
399 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
402 The first variation is obviously better suited for chaining.
406 =item C<action $method, @args>
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).
412 =item C<action_if $condition, $method, @args>
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.
417 Returns the return value of the actual function called if
418 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
421 This function is equivalent to the following:
424 $obj->$method(@args);
427 But it is easier to integrate into a method call chain, e.g.:
429 $js->html('#content', $html)
430 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
435 =head2 ADDITIONAL FUNCTIONS
439 =item C<flash $type, $message>
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.
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(...)>.
447 =item C<error $message>
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.
453 The messages of multiple calls of C<error> on the same C<$self> will
456 =item C<redirect_to $url>
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>.
466 =head2 KIVITENDO FUNCTIONS
468 The following functions from the C<kivi> namespace are supported:
472 =item Displaying stuff
474 C<flash> (don't call directly, use L</flash> instead)
476 =item Running functions
478 C<run>, C<run_once_for>
486 =head2 JQUERY FUNCTIONS
488 The following jQuery functions are supported:
494 C<hide>, C<show>, C<toggle>
496 =item DOM insertion, around
498 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
500 =item DOM insertion, inside
502 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
504 =item DOM insertion, outside
506 C<after>, C<before>, C<insertAfter>, C<insertBefore>
512 =item DOM replacement
514 C<replaceAll>, C<replaceWith>
516 =item General attributes
518 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
520 =item Class attributes
522 C<addClass>, C<removeClass>, C<toggleClass>
526 C<data>, C<removeData>
532 =item Generic Event Handlers
534 C<on>, C<off>, C<one>
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.
543 =head2 JQUERY POPUP DIALOG PLUGIN
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
549 $js->dialog->close(...);
553 =item Closing and removing the popup
559 =head2 AJAXFORM JQUERY PLUGIN
561 The following functions of the C<ajaxForm> plugin to jQuery are
566 =item All functions by the generic accessor function:
572 =head2 JSTREE JQUERY PLUGIN
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
578 $js->jstree->open_node(...);
582 =item Operations on the whole tree
586 =item Opening and closing nodes
588 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
589 C<close_all>, C<save_opened>, C<reopen>
591 =item Modifying nodes
593 C<rename_node>, C<delete_node>, C<move_node>
595 =item Selecting nodes (from the 'ui' jstree plugin)
597 C<select_node>, C<deselect_node>, C<deselect_all>
601 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
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:
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>.
617 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
618 generate C<js/client_js.js> automatically.
620 =item 3. Reload the files in your browser (cleaning its cache can also
625 The template file used for generated C<js/client_js.js> is
626 C<scripts/generate_client_js_actions.tpl>.
634 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>