5 use parent qw(Rose::Object);
10 use Rose::Object::MakeMethods::Generic
12 scalar => [ qw(controller) ],
13 'scalar --get_set_init' => [ qw(_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()
128 my ($self, @args) = @_;
130 my $method = $AUTOLOAD;
132 return if $method eq 'DESTROY';
133 return $self->action($method, @args);
137 my ($self, $method, @args) = @_;
139 $method = (delete($self->{_prefix}) || '') . $method;
140 my $num_args = $supported_methods{$method};
142 croak "Unsupported jQuery action: $method" unless defined $num_args;
145 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
148 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted at least: $num_args)" if scalar(@args) < $num_args;
149 $num_args = scalar @args;
152 foreach my $idx (0..$num_args - 1) {
153 # Force flattening from SL::Presenter::EscapedText and trim leading whitespace for scalars
154 $args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
155 $args[$idx] =~ s/^\s+// if !ref($args[$idx]);
158 push @{ $self->_actions }, [ $method, @args ];
164 my ($self, $condition, @args) = @_;
166 return $condition ? $self->action(@args) : $self;
184 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
185 return SL::JSON::to_json({ eval_actions => $self->_actions });
190 return $self->_actions;
194 my ($self, $controller) = @_;
195 $controller ||= $self->controller;
196 $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
197 return $controller->render(\$self->to_json, { type => 'json' });
202 $self->{_prefix} = 'jstree:';
208 $self->{_prefix} = 'dialog:';
214 $self->{_prefix} = 'ckeditor:';
219 my ($self, $type, @messages) = @_;
221 my $message = join ' ', grep { $_ } @messages;
223 if (!$self->_flash->{$type}) {
224 $self->_flash->{$type} = [ 'flash', $type, $message ];
225 push @{ $self->_actions }, $self->_flash->{$type};
227 $self->_flash->{$type}->[-1] .= ' ' . $message;
234 my ($self, @messages) = @_;
236 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
250 SL::ClientJS - Easy programmatic client-side JavaScript generation
255 First some JavaScript code:
257 // In the client generate an AJAX request whose 'success' handler
258 // calls "eval_json_result(data)":
260 action: "SomeController/the_action",
261 id: $('#some_input_field').val()
263 $.post("controller.pl", data, eval_json_result);
267 # In the controller itself. First, make sure that the "client_js.js"
268 # is loaded. This must be done when the whole side is loaded, so
269 # it's not in the action called by the AJAX request shown above.
270 $::request->layout->use_javascript('client_js.js');
272 # Now in that action called via AJAX:
273 sub action_the_action {
276 # Create a new client-side JS object and do stuff with it!
277 my $js = SL::ClientJS->new(controller => $self);
279 # Show some element on the page:
280 $js->show('#usually_hidden');
282 # Set to hidden inputs. Yes, calls can be chained!
283 $js->val('#hidden_id', $self->new_id)
284 ->val('#other_type', 'Unicorn');
286 # Replace some HTML code:
287 my $html = $self->render('SomeController/the_action', { output => 0 });
288 $js->html('#id_with_new_content', $html);
290 # Operations on a jstree: rename a node and select it
291 my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
292 $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
293 ->jstree->select_node('#tb-' . $text_block->id);
295 # Close a popup opened by kivi.popup_dialog():
296 $js->dialog->close('#jqueryui_popup_dialog');
298 # Finally render the JSON response:
301 # Rendering can also be chained, e.g.
302 $js->html('#selector', $html)
308 This module enables the generation of jQuery-using JavaScript code on
309 the server side. That code is then evaluated in a safe way on the
312 The workflow is usally that the client creates an AJAX request, the
313 server creates some actions and sends them back, and the client then
314 implements each of these actions.
316 There are three things that need to be done for this to work:
320 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
322 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
324 =item 3. The server must use this module.
328 The functions called on the client side are mostly jQuery
329 functions. Other functionality may be added later.
331 Note that L<SL::Controller/render> is aware of this module which saves
332 you some boilerplate. The following two calls are equivalent:
334 $controller->render($client_js);
335 $controller->render(\$client_js->to_json, { type => 'json' });
337 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
343 Returns the actions gathered so far as an array reference. Each
344 element is an array reference containing at least two items: the
345 function's name and what it is called on. Additional array elements
346 are the function parameters.
350 Returns the actions gathered so far as a JSON string ready to be sent
353 =item C<render [$controller]>
355 Renders C<$self> via the controller. Useful for chaining. Equivalent
358 $controller->render(\$self->to_json, { type => 'json' });
360 The controller instance to use can be set during object creation (see
361 synopsis) or as an argument to C<render>.
365 Tells C<$self> that the next action is to be called on a jQuery UI
366 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
369 $js->dialog->close('#jqueryui_popup_dialog');
373 Tells C<$self> that the next action is to be called on a jstree
374 instance. For example:
376 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
380 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
382 =head2 GENERIC FUNCTION
384 All of the following functions can be invoked in two ways: either by
385 calling the function name directly on C<$self> or by calling
386 L</action> with the function name as the first parameter. Therefore
387 the following two calls are identical:
389 $js->insertAfter($html, '#some-id');
390 $js->action('insertAfter', $html, '#some-id');
392 The second form, calling L</action>, is more to type but can be useful
393 in situations in which you have to call one of two functions depending
394 on context. For example, when you want to insert new code in a
395 list. If the list is empty you might have to use C<appendTo>, if it
396 isn't you might have to use C<insertAfter>. Example:
398 my $html = $self->render(...);
399 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
403 my $html = $self->render(...);
404 if ($list_is_empty) {
405 $js->appendTo($html, '#text-block-list');
407 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
410 The first variation is obviously better suited for chaining.
414 =item C<action $method, @args>
416 Call the function with the name C<$method> on C<$self> with arguments
417 C<@args>. Returns the return value of the actual function
418 called. Useful for chaining (see above).
420 =item C<action_if $condition, $method, @args>
422 Call the function with the name C<$method> on C<$self> with arguments
423 C<@args> if C<$condition> is trueish. Does nothing otherwise.
425 Returns the return value of the actual function called if
426 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
429 This function is equivalent to the following:
432 $obj->$method(@args);
435 But it is easier to integrate into a method call chain, e.g.:
437 $js->html('#content', $html)
438 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
443 =head2 ADDITIONAL FUNCTIONS
447 =item C<flash $type, $message>
449 Display a C<$message> in the flash of type C<$type>. Multiple calls of
450 C<flash> on the same C<$self> will be merged by type.
452 On the client side the flashes of all types will be cleared after each
453 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
455 =item C<error $message>
457 Causes L<to_json> (and therefore L<render>) to output a JSON object
458 that only contains an C<error> field set to this C<$message>. The
459 client will then show the message in the 'error' flash.
461 The messages of multiple calls of C<error> on the same C<$self> will
464 =item C<redirect_to $url>
466 Redirects the browser window to the new URL by setting the JavaScript
467 property C<window.location.href>. Note that
468 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
469 function if the current request is an AJAX request as determined by
470 L<SL::Request/is_ajax>.
474 =head2 KIVITENDO FUNCTIONS
476 The following functions from the C<kivi> namespace are supported:
480 =item Displaying stuff
482 C<flash> (don't call directly, use L</flash> instead)
484 =item Running functions
486 C<run>, C<run_once_for>
494 =head2 JQUERY FUNCTIONS
496 The following jQuery functions are supported:
502 C<hide>, C<show>, C<toggle>
504 =item DOM insertion, around
506 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
508 =item DOM insertion, inside
510 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
512 =item DOM insertion, outside
514 C<after>, C<before>, C<insertAfter>, C<insertBefore>
520 =item DOM replacement
522 C<replaceAll>, C<replaceWith>
524 =item General attributes
526 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
528 =item Class attributes
530 C<addClass>, C<removeClass>, C<toggleClass>
534 C<data>, C<removeData>
540 =item Generic Event Handlers
542 C<on>, C<off>, C<one>
544 These attach/detach event listeners to specific selectors. The first
545 argument is the selector, the second the name of the events and the
546 third argument is the name of the handler function. That function must
547 already exist when the handler is added.
551 =head2 JQUERY POPUP DIALOG PLUGIN
553 Supported functions of the C<popup dialog> plugin to jQuery. They are
554 invoked by first calling C<dialog> in the ClientJS instance and then
557 $js->dialog->close(...);
561 =item Closing and removing the popup
567 =head2 AJAXFORM JQUERY PLUGIN
569 The following functions of the C<ajaxForm> plugin to jQuery are
574 =item All functions by the generic accessor function:
580 =head2 JSTREE JQUERY PLUGIN
582 Supported functions of the C<jstree> plugin to jQuery. They are
583 invoked by first calling C<jstree> in the ClientJS instance and then
586 $js->jstree->open_node(...);
590 =item Operations on the whole tree
594 =item Opening and closing nodes
596 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
597 C<close_all>, C<save_opened>, C<reopen>
599 =item Modifying nodes
601 C<rename_node>, C<delete_node>, C<move_node>
603 =item Selecting nodes (from the 'ui' jstree plugin)
605 C<select_node>, C<deselect_node>, C<deselect_all>
609 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
611 In order not having to maintain two files (this one and
612 C<js/client_js.js>) there's a script that can parse this file's
613 C<%supported_methods> definition and generate the file
614 C<js/client_js.js> accordingly. The steps are:
618 =item 1. Add lines in this file to the C<%supported_methods> hash. The
619 key is the function name and the value is the number of expected
620 parameters. The value can be negative to indicate that the function
621 takes at least the absolute of this value as parameters and optionally
622 more. In such a case the C<E<lt>ARGSE<gt>> format expands to an actual
623 array (and the individual elements if the value is positive>.
625 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
626 generate C<js/client_js.js> automatically.
628 =item 3. Reload the files in your browser (cleaning its cache can also
633 The template file used for generated C<js/client_js.js> is
634 C<scripts/generate_client_js_actions.tpl>.
642 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>