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>
116 save_file => 4, # kivi.save_file(<TARGET>, <ARGS>)
118 flash => 2, # kivi.display_flash(<TARGET>, <ARGS>)
119 flash_detail => 2, # kivi.display_flash_detail(<TARGET>, <ARGS>)
120 clear_flash => 2, # kivi.clear_flash(<TARGET>, <ARGS>)
121 reinit_widgets => 0, # kivi.reinit_widgets()
122 run => -1, # kivi.run(<TARGET>, <ARGS>)
123 run_once_for => 3, # kivi.run_once_for(<TARGET>, <ARGS>)
125 scroll_into_view => 1, # $(<TARGET>)[0].scrollIntoView()
128 my %trim_target_for = map { ($_ => 1) } qw(insertAfter insertBefore appendTo prependTo);
133 my ($self, @args) = @_;
135 my $method = $AUTOLOAD;
137 return if $method eq 'DESTROY';
138 return $self->action($method, @args);
142 my ($self, $method, @args) = @_;
144 $method = (delete($self->{_prefix}) || '') . $method;
145 my $num_args = $supported_methods{$method};
147 croak "Unsupported jQuery action: $method" unless defined $num_args;
150 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
153 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted at least: $num_args)" if scalar(@args) < $num_args;
154 $num_args = scalar @args;
157 foreach my $idx (0..$num_args - 1) {
158 # Force flattening from SL::Presenter::EscapedText.
159 $args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
162 # Trim leading whitespaces for certain jQuery functions that operate
163 # on HTML code: $("<p>test</p>").appendTo('#some-id'). jQuery croaks
164 # on leading whitespaces, e.g. on $(" <p>test</p>").
165 $args[0] =~ s{^\s+}{} if $trim_target_for{$method};
167 push @{ $self->_actions }, [ $method, @args ];
173 my ($self, $condition, @args) = @_;
175 return $condition ? $self->action(@args) : $self;
186 sub init__flash_detail {
194 sub init__no_flash_clear {
201 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
202 return SL::JSON::to_json({ no_flash_clear => $self->_no_flash_clear, eval_actions => $self->_actions });
207 return $self->_actions;
211 my ($self, $controller) = @_;
212 $controller ||= $self->controller;
213 $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
214 return $controller->render(\$self->to_json, { type => 'json' });
219 $self->{_prefix} = 'jstree:';
225 $self->{_prefix} = 'dialog:';
231 $self->{_prefix} = 'ckeditor:';
236 my ($self, $type, @messages) = @_;
238 my $message = join ' ', grep { $_ } @messages;
240 if (!$self->_flash->{$type}) {
241 $self->_flash->{$type} = [ 'flash', $type, $message ];
242 push @{ $self->_actions }, $self->_flash->{$type};
244 $self->_flash->{$type}->[-1] .= ' ' . $message;
251 my ($self, $type, @messages) = @_;
253 my $message = join '<br>', grep { $_ } @messages;
255 if (!$self->_flash_detail->{$type}) {
256 $self->_flash_detail->{$type} = [ 'flash_detail', $type, $message ];
257 push @{ $self->_actions }, $self->_flash_detail->{$type};
259 $self->_flash_detail->{$type}->[-1] .= ' ' . $message;
267 $self->_no_flash_clear('1');
272 my ($self, @messages) = @_;
274 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
279 sub init_controller {
281 require SL::Controller::Base;
282 SL::Controller::Base->new;
294 SL::ClientJS - Easy programmatic client-side JavaScript generation
299 First some JavaScript code:
301 // In the client generate an AJAX request whose 'success' handler
302 // calls "eval_json_result(data)":
304 action: "SomeController/the_action",
305 id: $('#some_input_field').val()
307 $.post("controller.pl", data, eval_json_result);
311 # In the controller itself. First, make sure that the "client_js.js"
312 # is loaded. This must be done when the whole side is loaded, so
313 # it's not in the action called by the AJAX request shown above.
314 $::request->layout->use_javascript('client_js.js');
316 # Now in that action called via AJAX:
317 sub action_the_action {
320 # Create a new client-side JS object and do stuff with it!
321 my $js = SL::ClientJS->new(controller => $self);
323 # Show some element on the page:
324 $js->show('#usually_hidden');
326 # Set to hidden inputs. Yes, calls can be chained!
327 $js->val('#hidden_id', $self->new_id)
328 ->val('#other_type', 'Unicorn');
330 # Replace some HTML code:
331 my $html = $self->render('SomeController/the_action', { output => 0 });
332 $js->html('#id_with_new_content', $html);
334 # Operations on a jstree: rename a node and select it
335 my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
336 $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
337 ->jstree->select_node('#tb-' . $text_block->id);
339 # Close a popup opened by kivi.popup_dialog():
340 $js->dialog->close('#jqueryui_popup_dialog');
342 # Finally render the JSON response:
345 # Rendering can also be chained, e.g.
346 $js->html('#selector', $html)
352 This module enables the generation of jQuery-using JavaScript code on
353 the server side. That code is then evaluated in a safe way on the
356 The workflow is usually that the client creates an AJAX request, the
357 server creates some actions and sends them back, and the client then
358 implements each of these actions.
360 There are three things that need to be done for this to work:
364 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
366 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
368 =item 3. The server must use this module.
372 The functions called on the client side are mostly jQuery
373 functions. Other functionality may be added later.
375 Note that L<SL::Controller/render> is aware of this module which saves
376 you some boilerplate. The following two calls are equivalent:
378 $controller->render($client_js);
379 $controller->render(\$client_js->to_json, { type => 'json' });
381 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
387 Returns the actions gathered so far as an array reference. Each
388 element is an array reference containing at least two items: the
389 function's name and what it is called on. Additional array elements
390 are the function parameters.
394 Returns the actions gathered so far as a JSON string ready to be sent
397 =item C<render [$controller]>
399 Renders C<$self> via the controller. Useful for chaining. Equivalent
402 $controller->render(\$self->to_json, { type => 'json' });
404 The controller instance to use can be set during object creation (see
405 synopsis) or as an argument to C<render>.
409 Tells C<$self> that the next action is to be called on a jQuery UI
410 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
413 $js->dialog->close('#jqueryui_popup_dialog');
417 Tells C<$self> that the next action is to be called on a jstree
418 instance. For example:
420 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
424 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
426 =head2 GENERIC FUNCTION
428 All of the following functions can be invoked in two ways: either by
429 calling the function name directly on C<$self> or by calling
430 L</action> with the function name as the first parameter. Therefore
431 the following two calls are identical:
433 $js->insertAfter($html, '#some-id');
434 $js->action('insertAfter', $html, '#some-id');
436 The second form, calling L</action>, is more to type but can be useful
437 in situations in which you have to call one of two functions depending
438 on context. For example, when you want to insert new code in a
439 list. If the list is empty you might have to use C<appendTo>, if it
440 isn't you might have to use C<insertAfter>. Example:
442 my $html = $self->render(...);
443 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
447 my $html = $self->render(...);
448 if ($list_is_empty) {
449 $js->appendTo($html, '#text-block-list');
451 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
454 The first variation is obviously better suited for chaining.
458 =item C<action $method, @args>
460 Call the function with the name C<$method> on C<$self> with arguments
461 C<@args>. Returns the return value of the actual function
462 called. Useful for chaining (see above).
464 =item C<action_if $condition, $method, @args>
466 Call the function with the name C<$method> on C<$self> with arguments
467 C<@args> if C<$condition> is trueish. Does nothing otherwise.
469 Returns the return value of the actual function called if
470 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
473 This function is equivalent to the following:
476 $obj->$method(@args);
479 But it is easier to integrate into a method call chain, e.g.:
481 $js->html('#content', $html)
482 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
487 =head2 ADDITIONAL FUNCTIONS
491 =item C<flash $type, $message>
493 Display a C<$message> in the flash of type C<$type>. Multiple calls of
494 C<flash> on the same C<$self> will be merged by type.
496 On the client side the flashes of all types will be cleared after each
497 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
498 This clearing can be switched of by the function C<no_flash_clear>
500 =item C<flash_detail $type, $message>
502 Display a detailed message C<$message> in the flash of type C<$type>. Multiple calls of
503 C<flash_detail> on the same C<$self> will be merged by type.
504 So the flash message can be hold short and the visibility of details can toggled by the user.
506 =item C<no_flash_clear>
508 No automatic clearing of flash after successful ClientJS call
510 =item C<error $message>
512 Causes L<to_json> (and therefore L<render>) to output a JSON object
513 that only contains an C<error> field set to this C<$message>. The
514 client will then show the message in the 'error' flash.
516 The messages of multiple calls of C<error> on the same C<$self> will
519 =item C<redirect_to $url>
521 Redirects the browser window to the new URL by setting the JavaScript
522 property C<window.location.href>. Note that
523 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
524 function if the current request is an AJAX request as determined by
525 L<SL::Request/is_ajax>.
529 =head2 KIVITENDO FUNCTIONS
531 The following functions from the C<kivi> namespace are supported:
535 =item Displaying stuff
537 C<flash> (don't call directly, use L</flash> instead)
539 =item Running functions
541 C<run>, C<run_once_for>
549 =head2 JQUERY FUNCTIONS
551 The following jQuery functions are supported:
557 C<hide>, C<show>, C<toggle>
559 =item DOM insertion, around
561 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
563 =item DOM insertion, inside
565 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
567 =item DOM insertion, outside
569 C<after>, C<before>, C<insertAfter>, C<insertBefore>
575 =item DOM replacement
577 C<replaceAll>, C<replaceWith>
579 =item General attributes
581 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
583 =item Class attributes
585 C<addClass>, C<removeClass>, C<toggleClass>
589 C<data>, C<removeData>
595 =item Generic Event Handlers
597 C<on>, C<off>, C<one>
599 These attach/detach event listeners to specific selectors. The first
600 argument is the selector, the second the name of the events and the
601 third argument is the name of the handler function. That function must
602 already exist when the handler is added.
606 =head2 JQUERY POPUP DIALOG PLUGIN
608 Supported functions of the C<popup dialog> plugin to jQuery. They are
609 invoked by first calling C<dialog> in the ClientJS instance and then
612 $js->dialog->close(...);
616 =item Closing and removing the popup
622 =head2 AJAXFORM JQUERY PLUGIN
624 The following functions of the C<ajaxForm> plugin to jQuery are
629 =item All functions by the generic accessor function:
635 =head2 JSTREE JQUERY PLUGIN
637 Supported functions of the C<jstree> plugin to jQuery. They are
638 invoked by first calling C<jstree> in the ClientJS instance and then
641 $js->jstree->open_node(...);
645 =item Operations on the whole tree
649 =item Opening and closing nodes
651 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
652 C<close_all>, C<save_opened>, C<reopen>
654 =item Modifying nodes
656 C<rename_node>, C<delete_node>, C<move_node>
658 =item Selecting nodes (from the 'ui' jstree plugin)
660 C<select_node>, C<deselect_node>, C<deselect_all>
664 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
666 In order to not have to maintain two files (this one and
667 C<js/client_js.js>) there's a script that can parse this file's
668 C<%supported_methods> definition and generate the file
669 C<js/client_js.js> accordingly. The steps are:
673 =item 1. Add lines in this file to the C<%supported_methods> hash. The
674 key is the function name and the value is the number of expected
675 parameters. The value can be negative to indicate that the function
676 takes at least the absolute of this value as parameters and optionally
677 more. In such a case the C<E<lt>ARGSE<gt>> format expands to an actual
678 array (and the individual elements if the value is positive>.
680 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
681 generate C<js/client_js.js> automatically.
683 =item 3. Reload the files in your browser (cleaning its cache can also
688 The template file used for generated C<js/client_js.js> is
689 C<scripts/generate_client_js_actions.tpl>.
697 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>