5 use parent qw(Rose::Object);
10 use Rose::Object::MakeMethods::Generic
13 'scalar --get_set_init' => [ qw(controller _actions _flash _error) ],
16 my %supported_methods = (
24 # DOM insertion, around
30 # DOM insertion, inside
38 # DOM insertion, outside
69 focus => 1, # kivi.set_focus(<TARGET>)
71 # Generic Event Handling ## pattern: $(<TARGET>).<FUNCTION>(<ARG1>, kivi.get_function_by_name(<ARG2>))
76 # ## jQuery UI dialog plugin ## pattern: $(<TARGET>).dialog('<FUNCTION>')
78 # Opening and closing and closing a popup
79 'dialog:open' => 1, # kivi.popup_dialog(<TARGET>)
82 # ## jQuery Form plugin ##
83 'ajaxForm' => 1, # pattern: $(<TARGET>).ajaxForm({ success: eval_json_result })
85 # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
87 # Operations on the whole tree
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,
101 'jstree:create_node' => 4,
102 'jstree:rename_node' => 3,
103 'jstree:delete_node' => 2,
104 'jstree:move_node' => 5,
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,
111 # ## ckeditor stuff ##
112 'focus_ckeditor' => 1, # kivi.focus_ckeditor_when_ready(<TARGET>)
115 redirect_to => 1, # window.location.href = <TARGET>
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>)
122 scroll_into_view => 1, # $(<TARGET>)[0].scrollIntoView()
125 my %trim_target_for = map { ($_ => 1) } qw(insertAfter insertBefore appendTo prependTo);
130 my ($self, @args) = @_;
132 my $method = $AUTOLOAD;
134 return if $method eq 'DESTROY';
135 return $self->action($method, @args);
139 my ($self, $method, @args) = @_;
141 $method = (delete($self->{_prefix}) || '') . $method;
142 my $num_args = $supported_methods{$method};
144 croak "Unsupported jQuery action: $method" unless defined $num_args;
147 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
150 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted at least: $num_args)" if scalar(@args) < $num_args;
151 $num_args = scalar @args;
154 foreach my $idx (0..$num_args - 1) {
155 # Force flattening from SL::Presenter::EscapedText.
156 $args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
159 # Trim leading whitespaces for certain jQuery functions that operate
160 # on HTML code: $("<p>test</p>").appendTo('#some-id'). jQuery croaks
161 # on leading whitespaces, e.g. on $(" <p>test</p>").
162 $args[0] =~ s{^\s+}{} if $trim_target_for{$method};
164 push @{ $self->_actions }, [ $method, @args ];
170 my ($self, $condition, @args) = @_;
172 return $condition ? $self->action(@args) : $self;
190 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
191 return SL::JSON::to_json({ eval_actions => $self->_actions });
196 return $self->_actions;
200 my ($self, $controller) = @_;
201 $controller ||= $self->controller;
202 $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
203 return $controller->render(\$self->to_json, { type => 'json' });
208 $self->{_prefix} = 'jstree:';
214 $self->{_prefix} = 'dialog:';
220 $self->{_prefix} = 'ckeditor:';
225 my ($self, $type, @messages) = @_;
227 my $message = join ' ', grep { $_ } @messages;
229 if (!$self->_flash->{$type}) {
230 $self->_flash->{$type} = [ 'flash', $type, $message ];
231 push @{ $self->_actions }, $self->_flash->{$type};
233 $self->_flash->{$type}->[-1] .= ' ' . $message;
240 my ($self, @messages) = @_;
242 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
247 sub init_controller {
249 require SL::Controller::Base;
250 SL::Controller::Base->new;
262 SL::ClientJS - Easy programmatic client-side JavaScript generation
267 First some JavaScript code:
269 // In the client generate an AJAX request whose 'success' handler
270 // calls "eval_json_result(data)":
272 action: "SomeController/the_action",
273 id: $('#some_input_field').val()
275 $.post("controller.pl", data, eval_json_result);
279 # In the controller itself. First, make sure that the "client_js.js"
280 # is loaded. This must be done when the whole side is loaded, so
281 # it's not in the action called by the AJAX request shown above.
282 $::request->layout->use_javascript('client_js.js');
284 # Now in that action called via AJAX:
285 sub action_the_action {
288 # Create a new client-side JS object and do stuff with it!
289 my $js = SL::ClientJS->new(controller => $self);
291 # Show some element on the page:
292 $js->show('#usually_hidden');
294 # Set to hidden inputs. Yes, calls can be chained!
295 $js->val('#hidden_id', $self->new_id)
296 ->val('#other_type', 'Unicorn');
298 # Replace some HTML code:
299 my $html = $self->render('SomeController/the_action', { output => 0 });
300 $js->html('#id_with_new_content', $html);
302 # Operations on a jstree: rename a node and select it
303 my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
304 $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
305 ->jstree->select_node('#tb-' . $text_block->id);
307 # Close a popup opened by kivi.popup_dialog():
308 $js->dialog->close('#jqueryui_popup_dialog');
310 # Finally render the JSON response:
313 # Rendering can also be chained, e.g.
314 $js->html('#selector', $html)
320 This module enables the generation of jQuery-using JavaScript code on
321 the server side. That code is then evaluated in a safe way on the
324 The workflow is usally that the client creates an AJAX request, the
325 server creates some actions and sends them back, and the client then
326 implements each of these actions.
328 There are three things that need to be done for this to work:
332 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
334 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
336 =item 3. The server must use this module.
340 The functions called on the client side are mostly jQuery
341 functions. Other functionality may be added later.
343 Note that L<SL::Controller/render> is aware of this module which saves
344 you some boilerplate. The following two calls are equivalent:
346 $controller->render($client_js);
347 $controller->render(\$client_js->to_json, { type => 'json' });
349 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
355 Returns the actions gathered so far as an array reference. Each
356 element is an array reference containing at least two items: the
357 function's name and what it is called on. Additional array elements
358 are the function parameters.
362 Returns the actions gathered so far as a JSON string ready to be sent
365 =item C<render [$controller]>
367 Renders C<$self> via the controller. Useful for chaining. Equivalent
370 $controller->render(\$self->to_json, { type => 'json' });
372 The controller instance to use can be set during object creation (see
373 synopsis) or as an argument to C<render>.
377 Tells C<$self> that the next action is to be called on a jQuery UI
378 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
381 $js->dialog->close('#jqueryui_popup_dialog');
385 Tells C<$self> that the next action is to be called on a jstree
386 instance. For example:
388 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
392 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
394 =head2 GENERIC FUNCTION
396 All of the following functions can be invoked in two ways: either by
397 calling the function name directly on C<$self> or by calling
398 L</action> with the function name as the first parameter. Therefore
399 the following two calls are identical:
401 $js->insertAfter($html, '#some-id');
402 $js->action('insertAfter', $html, '#some-id');
404 The second form, calling L</action>, is more to type but can be useful
405 in situations in which you have to call one of two functions depending
406 on context. For example, when you want to insert new code in a
407 list. If the list is empty you might have to use C<appendTo>, if it
408 isn't you might have to use C<insertAfter>. Example:
410 my $html = $self->render(...);
411 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
415 my $html = $self->render(...);
416 if ($list_is_empty) {
417 $js->appendTo($html, '#text-block-list');
419 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
422 The first variation is obviously better suited for chaining.
426 =item C<action $method, @args>
428 Call the function with the name C<$method> on C<$self> with arguments
429 C<@args>. Returns the return value of the actual function
430 called. Useful for chaining (see above).
432 =item C<action_if $condition, $method, @args>
434 Call the function with the name C<$method> on C<$self> with arguments
435 C<@args> if C<$condition> is trueish. Does nothing otherwise.
437 Returns the return value of the actual function called if
438 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
441 This function is equivalent to the following:
444 $obj->$method(@args);
447 But it is easier to integrate into a method call chain, e.g.:
449 $js->html('#content', $html)
450 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
455 =head2 ADDITIONAL FUNCTIONS
459 =item C<flash $type, $message>
461 Display a C<$message> in the flash of type C<$type>. Multiple calls of
462 C<flash> on the same C<$self> will be merged by type.
464 On the client side the flashes of all types will be cleared after each
465 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
467 =item C<error $message>
469 Causes L<to_json> (and therefore L<render>) to output a JSON object
470 that only contains an C<error> field set to this C<$message>. The
471 client will then show the message in the 'error' flash.
473 The messages of multiple calls of C<error> on the same C<$self> will
476 =item C<redirect_to $url>
478 Redirects the browser window to the new URL by setting the JavaScript
479 property C<window.location.href>. Note that
480 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
481 function if the current request is an AJAX request as determined by
482 L<SL::Request/is_ajax>.
486 =head2 KIVITENDO FUNCTIONS
488 The following functions from the C<kivi> namespace are supported:
492 =item Displaying stuff
494 C<flash> (don't call directly, use L</flash> instead)
496 =item Running functions
498 C<run>, C<run_once_for>
506 =head2 JQUERY FUNCTIONS
508 The following jQuery functions are supported:
514 C<hide>, C<show>, C<toggle>
516 =item DOM insertion, around
518 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
520 =item DOM insertion, inside
522 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
524 =item DOM insertion, outside
526 C<after>, C<before>, C<insertAfter>, C<insertBefore>
532 =item DOM replacement
534 C<replaceAll>, C<replaceWith>
536 =item General attributes
538 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
540 =item Class attributes
542 C<addClass>, C<removeClass>, C<toggleClass>
546 C<data>, C<removeData>
552 =item Generic Event Handlers
554 C<on>, C<off>, C<one>
556 These attach/detach event listeners to specific selectors. The first
557 argument is the selector, the second the name of the events and the
558 third argument is the name of the handler function. That function must
559 already exist when the handler is added.
563 =head2 JQUERY POPUP DIALOG PLUGIN
565 Supported functions of the C<popup dialog> plugin to jQuery. They are
566 invoked by first calling C<dialog> in the ClientJS instance and then
569 $js->dialog->close(...);
573 =item Closing and removing the popup
579 =head2 AJAXFORM JQUERY PLUGIN
581 The following functions of the C<ajaxForm> plugin to jQuery are
586 =item All functions by the generic accessor function:
592 =head2 JSTREE JQUERY PLUGIN
594 Supported functions of the C<jstree> plugin to jQuery. They are
595 invoked by first calling C<jstree> in the ClientJS instance and then
598 $js->jstree->open_node(...);
602 =item Operations on the whole tree
606 =item Opening and closing nodes
608 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
609 C<close_all>, C<save_opened>, C<reopen>
611 =item Modifying nodes
613 C<rename_node>, C<delete_node>, C<move_node>
615 =item Selecting nodes (from the 'ui' jstree plugin)
617 C<select_node>, C<deselect_node>, C<deselect_all>
621 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
623 In order not having to maintain two files (this one and
624 C<js/client_js.js>) there's a script that can parse this file's
625 C<%supported_methods> definition and generate the file
626 C<js/client_js.js> accordingly. The steps are:
630 =item 1. Add lines in this file to the C<%supported_methods> hash. The
631 key is the function name and the value is the number of expected
632 parameters. The value can be negative to indicate that the function
633 takes at least the absolute of this value as parameters and optionally
634 more. In such a case the C<E<lt>ARGSE<gt>> format expands to an actual
635 array (and the individual elements if the value is positive>.
637 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
638 generate C<js/client_js.js> automatically.
640 =item 3. Reload the files in your browser (cleaning its cache can also
645 The template file used for generated C<js/client_js.js> is
646 C<scripts/generate_client_js_actions.tpl>.
654 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>