5 use parent qw(Rose::Object);
10 use Rose::Object::MakeMethods::Generic
13 'scalar --get_set_init' => [ qw(controller _actions _flash _flash_detail _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 flash_detail => 2, # kivi.display_flash_detail(<TARGET>, <ARGS>)
119 reinit_widgets => 0, # kivi.reinit_widgets()
120 run => -1, # kivi.run(<TARGET>, <ARGS>)
121 run_once_for => 3, # kivi.run_once_for(<TARGET>, <ARGS>)
123 scroll_into_view => 1, # $(<TARGET>)[0].scrollIntoView()
126 my %trim_target_for = map { ($_ => 1) } qw(insertAfter insertBefore appendTo prependTo);
131 my ($self, @args) = @_;
133 my $method = $AUTOLOAD;
135 return if $method eq 'DESTROY';
136 return $self->action($method, @args);
140 my ($self, $method, @args) = @_;
142 $method = (delete($self->{_prefix}) || '') . $method;
143 my $num_args = $supported_methods{$method};
145 croak "Unsupported jQuery action: $method" unless defined $num_args;
148 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
151 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted at least: $num_args)" if scalar(@args) < $num_args;
152 $num_args = scalar @args;
155 foreach my $idx (0..$num_args - 1) {
156 # Force flattening from SL::Presenter::EscapedText.
157 $args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
160 # Trim leading whitespaces for certain jQuery functions that operate
161 # on HTML code: $("<p>test</p>").appendTo('#some-id'). jQuery croaks
162 # on leading whitespaces, e.g. on $(" <p>test</p>").
163 $args[0] =~ s{^\s+}{} if $trim_target_for{$method};
165 push @{ $self->_actions }, [ $method, @args ];
171 my ($self, $condition, @args) = @_;
173 return $condition ? $self->action(@args) : $self;
184 sub init__flash_detail {
195 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
196 return SL::JSON::to_json({ eval_actions => $self->_actions });
201 return $self->_actions;
205 my ($self, $controller) = @_;
206 $controller ||= $self->controller;
207 $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
208 return $controller->render(\$self->to_json, { type => 'json' });
213 $self->{_prefix} = 'jstree:';
219 $self->{_prefix} = 'dialog:';
225 $self->{_prefix} = 'ckeditor:';
230 my ($self, $type, @messages) = @_;
232 my $message = join ' ', grep { $_ } @messages;
234 if (!$self->_flash->{$type}) {
235 $self->_flash->{$type} = [ 'flash', $type, $message ];
236 push @{ $self->_actions }, $self->_flash->{$type};
238 $self->_flash->{$type}->[-1] .= ' ' . $message;
245 my ($self, $type, @messages) = @_;
247 my $message = join '<br>', grep { $_ } @messages;
249 if (!$self->_flash_detail->{$type}) {
250 $self->_flash_detail->{$type} = [ 'flash_detail', $type, $message ];
251 push @{ $self->_actions }, $self->_flash_detail->{$type};
253 $self->_flash_detail->{$type}->[-1] .= ' ' . $message;
260 my ($self, @messages) = @_;
262 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
267 sub init_controller {
269 require SL::Controller::Base;
270 SL::Controller::Base->new;
282 SL::ClientJS - Easy programmatic client-side JavaScript generation
287 First some JavaScript code:
289 // In the client generate an AJAX request whose 'success' handler
290 // calls "eval_json_result(data)":
292 action: "SomeController/the_action",
293 id: $('#some_input_field').val()
295 $.post("controller.pl", data, eval_json_result);
299 # In the controller itself. First, make sure that the "client_js.js"
300 # is loaded. This must be done when the whole side is loaded, so
301 # it's not in the action called by the AJAX request shown above.
302 $::request->layout->use_javascript('client_js.js');
304 # Now in that action called via AJAX:
305 sub action_the_action {
308 # Create a new client-side JS object and do stuff with it!
309 my $js = SL::ClientJS->new(controller => $self);
311 # Show some element on the page:
312 $js->show('#usually_hidden');
314 # Set to hidden inputs. Yes, calls can be chained!
315 $js->val('#hidden_id', $self->new_id)
316 ->val('#other_type', 'Unicorn');
318 # Replace some HTML code:
319 my $html = $self->render('SomeController/the_action', { output => 0 });
320 $js->html('#id_with_new_content', $html);
322 # Operations on a jstree: rename a node and select it
323 my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
324 $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
325 ->jstree->select_node('#tb-' . $text_block->id);
327 # Close a popup opened by kivi.popup_dialog():
328 $js->dialog->close('#jqueryui_popup_dialog');
330 # Finally render the JSON response:
333 # Rendering can also be chained, e.g.
334 $js->html('#selector', $html)
340 This module enables the generation of jQuery-using JavaScript code on
341 the server side. That code is then evaluated in a safe way on the
344 The workflow is usally that the client creates an AJAX request, the
345 server creates some actions and sends them back, and the client then
346 implements each of these actions.
348 There are three things that need to be done for this to work:
352 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
354 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
356 =item 3. The server must use this module.
360 The functions called on the client side are mostly jQuery
361 functions. Other functionality may be added later.
363 Note that L<SL::Controller/render> is aware of this module which saves
364 you some boilerplate. The following two calls are equivalent:
366 $controller->render($client_js);
367 $controller->render(\$client_js->to_json, { type => 'json' });
369 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
375 Returns the actions gathered so far as an array reference. Each
376 element is an array reference containing at least two items: the
377 function's name and what it is called on. Additional array elements
378 are the function parameters.
382 Returns the actions gathered so far as a JSON string ready to be sent
385 =item C<render [$controller]>
387 Renders C<$self> via the controller. Useful for chaining. Equivalent
390 $controller->render(\$self->to_json, { type => 'json' });
392 The controller instance to use can be set during object creation (see
393 synopsis) or as an argument to C<render>.
397 Tells C<$self> that the next action is to be called on a jQuery UI
398 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
401 $js->dialog->close('#jqueryui_popup_dialog');
405 Tells C<$self> that the next action is to be called on a jstree
406 instance. For example:
408 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
412 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
414 =head2 GENERIC FUNCTION
416 All of the following functions can be invoked in two ways: either by
417 calling the function name directly on C<$self> or by calling
418 L</action> with the function name as the first parameter. Therefore
419 the following two calls are identical:
421 $js->insertAfter($html, '#some-id');
422 $js->action('insertAfter', $html, '#some-id');
424 The second form, calling L</action>, is more to type but can be useful
425 in situations in which you have to call one of two functions depending
426 on context. For example, when you want to insert new code in a
427 list. If the list is empty you might have to use C<appendTo>, if it
428 isn't you might have to use C<insertAfter>. Example:
430 my $html = $self->render(...);
431 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
435 my $html = $self->render(...);
436 if ($list_is_empty) {
437 $js->appendTo($html, '#text-block-list');
439 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
442 The first variation is obviously better suited for chaining.
446 =item C<action $method, @args>
448 Call the function with the name C<$method> on C<$self> with arguments
449 C<@args>. Returns the return value of the actual function
450 called. Useful for chaining (see above).
452 =item C<action_if $condition, $method, @args>
454 Call the function with the name C<$method> on C<$self> with arguments
455 C<@args> if C<$condition> is trueish. Does nothing otherwise.
457 Returns the return value of the actual function called if
458 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
461 This function is equivalent to the following:
464 $obj->$method(@args);
467 But it is easier to integrate into a method call chain, e.g.:
469 $js->html('#content', $html)
470 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
475 =head2 ADDITIONAL FUNCTIONS
479 =item C<flash $type, $message>
481 Display a C<$message> in the flash of type C<$type>. Multiple calls of
482 C<flash> on the same C<$self> will be merged by type.
484 On the client side the flashes of all types will be cleared after each
485 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
487 =item C<error $message>
489 Causes L<to_json> (and therefore L<render>) to output a JSON object
490 that only contains an C<error> field set to this C<$message>. The
491 client will then show the message in the 'error' flash.
493 The messages of multiple calls of C<error> on the same C<$self> will
496 =item C<redirect_to $url>
498 Redirects the browser window to the new URL by setting the JavaScript
499 property C<window.location.href>. Note that
500 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
501 function if the current request is an AJAX request as determined by
502 L<SL::Request/is_ajax>.
506 =head2 KIVITENDO FUNCTIONS
508 The following functions from the C<kivi> namespace are supported:
512 =item Displaying stuff
514 C<flash> (don't call directly, use L</flash> instead)
516 =item Running functions
518 C<run>, C<run_once_for>
526 =head2 JQUERY FUNCTIONS
528 The following jQuery functions are supported:
534 C<hide>, C<show>, C<toggle>
536 =item DOM insertion, around
538 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
540 =item DOM insertion, inside
542 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
544 =item DOM insertion, outside
546 C<after>, C<before>, C<insertAfter>, C<insertBefore>
552 =item DOM replacement
554 C<replaceAll>, C<replaceWith>
556 =item General attributes
558 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
560 =item Class attributes
562 C<addClass>, C<removeClass>, C<toggleClass>
566 C<data>, C<removeData>
572 =item Generic Event Handlers
574 C<on>, C<off>, C<one>
576 These attach/detach event listeners to specific selectors. The first
577 argument is the selector, the second the name of the events and the
578 third argument is the name of the handler function. That function must
579 already exist when the handler is added.
583 =head2 JQUERY POPUP DIALOG PLUGIN
585 Supported functions of the C<popup dialog> plugin to jQuery. They are
586 invoked by first calling C<dialog> in the ClientJS instance and then
589 $js->dialog->close(...);
593 =item Closing and removing the popup
599 =head2 AJAXFORM JQUERY PLUGIN
601 The following functions of the C<ajaxForm> plugin to jQuery are
606 =item All functions by the generic accessor function:
612 =head2 JSTREE JQUERY PLUGIN
614 Supported functions of the C<jstree> plugin to jQuery. They are
615 invoked by first calling C<jstree> in the ClientJS instance and then
618 $js->jstree->open_node(...);
622 =item Operations on the whole tree
626 =item Opening and closing nodes
628 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
629 C<close_all>, C<save_opened>, C<reopen>
631 =item Modifying nodes
633 C<rename_node>, C<delete_node>, C<move_node>
635 =item Selecting nodes (from the 'ui' jstree plugin)
637 C<select_node>, C<deselect_node>, C<deselect_all>
641 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
643 In order not having to maintain two files (this one and
644 C<js/client_js.js>) there's a script that can parse this file's
645 C<%supported_methods> definition and generate the file
646 C<js/client_js.js> accordingly. The steps are:
650 =item 1. Add lines in this file to the C<%supported_methods> hash. The
651 key is the function name and the value is the number of expected
652 parameters. The value can be negative to indicate that the function
653 takes at least the absolute of this value as parameters and optionally
654 more. In such a case the C<E<lt>ARGSE<gt>> format expands to an actual
655 array (and the individual elements if the value is positive>.
657 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
658 generate C<js/client_js.js> automatically.
660 =item 3. Reload the files in your browser (cleaning its cache can also
665 The template file used for generated C<js/client_js.js> is
666 C<scripts/generate_client_js_actions.tpl>.
674 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>