5 use parent qw(Rose::Object);
10 use Rose::Object::MakeMethods::Generic
13 'scalar --get_set_init' => [ qw(controller _actions _flash _flash_detail _no_flash_clear _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 a popup
79 'dialog:open' => 1, # kivi.popup_dialog(<TARGET>)
82 # ## jQuery Form plugin ##
83 'ajaxForm' => 1, # $(<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 clear_flash => 2, # kivi.clear_flash(<TARGET>, <ARGS>)
120 reinit_widgets => 0, # kivi.reinit_widgets()
121 run => -1, # kivi.run(<TARGET>, <ARGS>)
122 run_once_for => 3, # kivi.run_once_for(<TARGET>, <ARGS>)
124 scroll_into_view => 1, # $(<TARGET>)[0].scrollIntoView()
127 my %trim_target_for = map { ($_ => 1) } qw(insertAfter insertBefore appendTo prependTo);
132 my ($self, @args) = @_;
134 my $method = $AUTOLOAD;
136 return if $method eq 'DESTROY';
137 return $self->action($method, @args);
141 my ($self, $method, @args) = @_;
143 $method = (delete($self->{_prefix}) || '') . $method;
144 my $num_args = $supported_methods{$method};
146 croak "Unsupported jQuery action: $method" unless defined $num_args;
149 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
152 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted at least: $num_args)" if scalar(@args) < $num_args;
153 $num_args = scalar @args;
156 foreach my $idx (0..$num_args - 1) {
157 # Force flattening from SL::Presenter::EscapedText.
158 $args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
161 # Trim leading whitespaces for certain jQuery functions that operate
162 # on HTML code: $("<p>test</p>").appendTo('#some-id'). jQuery croaks
163 # on leading whitespaces, e.g. on $(" <p>test</p>").
164 $args[0] =~ s{^\s+}{} if $trim_target_for{$method};
166 push @{ $self->_actions }, [ $method, @args ];
172 my ($self, $condition, @args) = @_;
174 return $condition ? $self->action(@args) : $self;
185 sub init__flash_detail {
193 sub init__no_flash_clear {
200 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
201 return SL::JSON::to_json({ no_flash_clear => $self->_no_flash_clear, eval_actions => $self->_actions });
206 return $self->_actions;
210 my ($self, $controller) = @_;
211 $controller ||= $self->controller;
212 $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
213 return $controller->render(\$self->to_json, { type => 'json' });
218 $self->{_prefix} = 'jstree:';
224 $self->{_prefix} = 'dialog:';
230 $self->{_prefix} = 'ckeditor:';
235 my ($self, $type, @messages) = @_;
237 my $message = join ' ', grep { $_ } @messages;
239 if (!$self->_flash->{$type}) {
240 $self->_flash->{$type} = [ 'flash', $type, $message ];
241 push @{ $self->_actions }, $self->_flash->{$type};
243 $self->_flash->{$type}->[-1] .= ' ' . $message;
250 my ($self, $type, @messages) = @_;
252 my $message = join '<br>', grep { $_ } @messages;
254 if (!$self->_flash_detail->{$type}) {
255 $self->_flash_detail->{$type} = [ 'flash_detail', $type, $message ];
256 push @{ $self->_actions }, $self->_flash_detail->{$type};
258 $self->_flash_detail->{$type}->[-1] .= ' ' . $message;
266 $self->_no_flash_clear('1');
271 my ($self, @messages) = @_;
273 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
278 sub init_controller {
280 require SL::Controller::Base;
281 SL::Controller::Base->new;
293 SL::ClientJS - Easy programmatic client-side JavaScript generation
298 First some JavaScript code:
300 // In the client generate an AJAX request whose 'success' handler
301 // calls "eval_json_result(data)":
303 action: "SomeController/the_action",
304 id: $('#some_input_field').val()
306 $.post("controller.pl", data, eval_json_result);
310 # In the controller itself. First, make sure that the "client_js.js"
311 # is loaded. This must be done when the whole side is loaded, so
312 # it's not in the action called by the AJAX request shown above.
313 $::request->layout->use_javascript('client_js.js');
315 # Now in that action called via AJAX:
316 sub action_the_action {
319 # Create a new client-side JS object and do stuff with it!
320 my $js = SL::ClientJS->new(controller => $self);
322 # Show some element on the page:
323 $js->show('#usually_hidden');
325 # Set to hidden inputs. Yes, calls can be chained!
326 $js->val('#hidden_id', $self->new_id)
327 ->val('#other_type', 'Unicorn');
329 # Replace some HTML code:
330 my $html = $self->render('SomeController/the_action', { output => 0 });
331 $js->html('#id_with_new_content', $html);
333 # Operations on a jstree: rename a node and select it
334 my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
335 $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
336 ->jstree->select_node('#tb-' . $text_block->id);
338 # Close a popup opened by kivi.popup_dialog():
339 $js->dialog->close('#jqueryui_popup_dialog');
341 # Finally render the JSON response:
344 # Rendering can also be chained, e.g.
345 $js->html('#selector', $html)
351 This module enables the generation of jQuery-using JavaScript code on
352 the server side. That code is then evaluated in a safe way on the
355 The workflow is usually that the client creates an AJAX request, the
356 server creates some actions and sends them back, and the client then
357 implements each of these actions.
359 There are three things that need to be done for this to work:
363 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
365 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
367 =item 3. The server must use this module.
371 The functions called on the client side are mostly jQuery
372 functions. Other functionality may be added later.
374 Note that L<SL::Controller/render> is aware of this module which saves
375 you some boilerplate. The following two calls are equivalent:
377 $controller->render($client_js);
378 $controller->render(\$client_js->to_json, { type => 'json' });
380 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
386 Returns the actions gathered so far as an array reference. Each
387 element is an array reference containing at least two items: the
388 function's name and what it is called on. Additional array elements
389 are the function parameters.
393 Returns the actions gathered so far as a JSON string ready to be sent
396 =item C<render [$controller]>
398 Renders C<$self> via the controller. Useful for chaining. Equivalent
401 $controller->render(\$self->to_json, { type => 'json' });
403 The controller instance to use can be set during object creation (see
404 synopsis) or as an argument to C<render>.
408 Tells C<$self> that the next action is to be called on a jQuery UI
409 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
412 $js->dialog->close('#jqueryui_popup_dialog');
416 Tells C<$self> that the next action is to be called on a jstree
417 instance. For example:
419 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
423 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
425 =head2 GENERIC FUNCTION
427 All of the following functions can be invoked in two ways: either by
428 calling the function name directly on C<$self> or by calling
429 L</action> with the function name as the first parameter. Therefore
430 the following two calls are identical:
432 $js->insertAfter($html, '#some-id');
433 $js->action('insertAfter', $html, '#some-id');
435 The second form, calling L</action>, is more to type but can be useful
436 in situations in which you have to call one of two functions depending
437 on context. For example, when you want to insert new code in a
438 list. If the list is empty you might have to use C<appendTo>, if it
439 isn't you might have to use C<insertAfter>. Example:
441 my $html = $self->render(...);
442 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
446 my $html = $self->render(...);
447 if ($list_is_empty) {
448 $js->appendTo($html, '#text-block-list');
450 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
453 The first variation is obviously better suited for chaining.
457 =item C<action $method, @args>
459 Call the function with the name C<$method> on C<$self> with arguments
460 C<@args>. Returns the return value of the actual function
461 called. Useful for chaining (see above).
463 =item C<action_if $condition, $method, @args>
465 Call the function with the name C<$method> on C<$self> with arguments
466 C<@args> if C<$condition> is trueish. Does nothing otherwise.
468 Returns the return value of the actual function called if
469 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
472 This function is equivalent to the following:
475 $obj->$method(@args);
478 But it is easier to integrate into a method call chain, e.g.:
480 $js->html('#content', $html)
481 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
486 =head2 ADDITIONAL FUNCTIONS
490 =item C<flash $type, $message>
492 Display a C<$message> in the flash of type C<$type>. Multiple calls of
493 C<flash> on the same C<$self> will be merged by type.
495 On the client side the flashes of all types will be cleared after each
496 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
497 This clearing can be switched of by the function C<no_flash_clear>
499 =item C<flash_detail $type, $message>
501 Display a detailed message C<$message> in the flash of type C<$type>. Multiple calls of
502 C<flash_detail> on the same C<$self> will be merged by type.
503 So the flash message can be hold short and the visibility of details can toggled by the user.
505 =item C<no_flash_clear>
507 No automatic clearing of flash after successful ClientJS call
509 =item C<error $message>
511 Causes L<to_json> (and therefore L<render>) to output a JSON object
512 that only contains an C<error> field set to this C<$message>. The
513 client will then show the message in the 'error' flash.
515 The messages of multiple calls of C<error> on the same C<$self> will
518 =item C<redirect_to $url>
520 Redirects the browser window to the new URL by setting the JavaScript
521 property C<window.location.href>. Note that
522 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
523 function if the current request is an AJAX request as determined by
524 L<SL::Request/is_ajax>.
528 =head2 KIVITENDO FUNCTIONS
530 The following functions from the C<kivi> namespace are supported:
534 =item Displaying stuff
536 C<flash> (don't call directly, use L</flash> instead)
538 =item Running functions
540 C<run>, C<run_once_for>
548 =head2 JQUERY FUNCTIONS
550 The following jQuery functions are supported:
556 C<hide>, C<show>, C<toggle>
558 =item DOM insertion, around
560 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
562 =item DOM insertion, inside
564 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
566 =item DOM insertion, outside
568 C<after>, C<before>, C<insertAfter>, C<insertBefore>
574 =item DOM replacement
576 C<replaceAll>, C<replaceWith>
578 =item General attributes
580 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
582 =item Class attributes
584 C<addClass>, C<removeClass>, C<toggleClass>
588 C<data>, C<removeData>
594 =item Generic Event Handlers
596 C<on>, C<off>, C<one>
598 These attach/detach event listeners to specific selectors. The first
599 argument is the selector, the second the name of the events and the
600 third argument is the name of the handler function. That function must
601 already exist when the handler is added.
605 =head2 JQUERY POPUP DIALOG PLUGIN
607 Supported functions of the C<popup dialog> plugin to jQuery. They are
608 invoked by first calling C<dialog> in the ClientJS instance and then
611 $js->dialog->close(...);
615 =item Closing and removing the popup
621 =head2 AJAXFORM JQUERY PLUGIN
623 The following functions of the C<ajaxForm> plugin to jQuery are
628 =item All functions by the generic accessor function:
634 =head2 JSTREE JQUERY PLUGIN
636 Supported functions of the C<jstree> plugin to jQuery. They are
637 invoked by first calling C<jstree> in the ClientJS instance and then
640 $js->jstree->open_node(...);
644 =item Operations on the whole tree
648 =item Opening and closing nodes
650 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
651 C<close_all>, C<save_opened>, C<reopen>
653 =item Modifying nodes
655 C<rename_node>, C<delete_node>, C<move_node>
657 =item Selecting nodes (from the 'ui' jstree plugin)
659 C<select_node>, C<deselect_node>, C<deselect_all>
663 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
665 In order to not have to maintain two files (this one and
666 C<js/client_js.js>) there's a script that can parse this file's
667 C<%supported_methods> definition and generate the file
668 C<js/client_js.js> accordingly. The steps are:
672 =item 1. Add lines in this file to the C<%supported_methods> hash. The
673 key is the function name and the value is the number of expected
674 parameters. The value can be negative to indicate that the function
675 takes at least the absolute of this value as parameters and optionally
676 more. In such a case the C<E<lt>ARGSE<gt>> format expands to an actual
677 array (and the individual elements if the value is positive>.
679 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
680 generate C<js/client_js.js> automatically.
682 =item 3. Reload the files in your browser (cleaning its cache can also
687 The template file used for generated C<js/client_js.js> is
688 C<scripts/generate_client_js_actions.tpl>.
696 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>