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()
127 set_cursor_position => 2, # kivi.set_cursor_position(<TARGET>, <ARGS>)
130 my %trim_target_for = map { ($_ => 1) } qw(insertAfter insertBefore appendTo prependTo);
135 my ($self, @args) = @_;
137 my $method = $AUTOLOAD;
139 return if $method eq 'DESTROY';
140 return $self->action($method, @args);
144 my ($self, $method, @args) = @_;
146 $method = (delete($self->{_prefix}) || '') . $method;
147 my $num_args = $supported_methods{$method};
149 croak "Unsupported jQuery action: $method" unless defined $num_args;
152 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if scalar(@args) != $num_args;
155 croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted at least: $num_args)" if scalar(@args) < $num_args;
156 $num_args = scalar @args;
159 foreach my $idx (0..$num_args - 1) {
160 # Force flattening from SL::Presenter::EscapedText.
161 $args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
164 # Trim leading whitespaces for certain jQuery functions that operate
165 # on HTML code: $("<p>test</p>").appendTo('#some-id'). jQuery croaks
166 # on leading whitespaces, e.g. on $(" <p>test</p>").
167 $args[0] =~ s{^\s+}{} if $trim_target_for{$method};
169 push @{ $self->_actions }, [ $method, @args ];
175 my ($self, $condition, @args) = @_;
177 return $condition ? $self->action(@args) : $self;
188 sub init__flash_detail {
196 sub init__no_flash_clear {
203 return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
204 return SL::JSON::to_json({ no_flash_clear => $self->_no_flash_clear, eval_actions => $self->_actions });
209 return $self->_actions;
213 my ($self, $controller) = @_;
214 $controller ||= $self->controller;
215 $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
216 return $controller->render(\$self->to_json, { type => 'json' });
221 $self->{_prefix} = 'jstree:';
227 $self->{_prefix} = 'dialog:';
233 $self->{_prefix} = 'ckeditor:';
238 my ($self, $type, @messages) = @_;
240 my $message = join ' ', grep { $_ } @messages;
242 if (!$self->_flash->{$type}) {
243 $self->_flash->{$type} = [ 'flash', $type, $message ];
244 push @{ $self->_actions }, $self->_flash->{$type};
246 $self->_flash->{$type}->[-1] .= ' ' . $message;
253 my ($self, $type, @messages) = @_;
255 my $message = join '<br>', grep { $_ } @messages;
257 if (!$self->_flash_detail->{$type}) {
258 $self->_flash_detail->{$type} = [ 'flash_detail', $type, $message ];
259 push @{ $self->_actions }, $self->_flash_detail->{$type};
261 $self->_flash_detail->{$type}->[-1] .= ' ' . $message;
269 $self->_no_flash_clear('1');
274 my ($self, @messages) = @_;
276 $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
281 sub init_controller {
283 require SL::Controller::Base;
284 SL::Controller::Base->new;
296 SL::ClientJS - Easy programmatic client-side JavaScript generation
301 First some JavaScript code:
303 // In the client generate an AJAX request whose 'success' handler
304 // calls "eval_json_result(data)":
306 action: "SomeController/my_personal_action",
307 id: $('#some_input_field').val()
309 $.post("controller.pl", data, eval_json_result);
311 Now some Controller (perl) code for my personal action:
314 sub action_my_personal_action {
317 # Create a new client-side JS object and do stuff with it!
318 my $js = SL::ClientJS->new(controller => $self);
320 # Show some element on the page:
321 $js->show('#usually_hidden');
323 # Set to hidden inputs. Yes, calls can be chained!
324 $js->val('#hidden_id', $self->new_id)
325 ->val('#other_type', 'Unicorn');
327 # Replace some HTML code:
328 my $html = $self->render('SomeController/the_action', { output => 0 });
329 $js->html('#id_with_new_content', $html);
331 # Operations on a jstree: rename a node and select it
332 my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
333 $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
334 ->jstree->select_node('#tb-' . $text_block->id);
336 # Close a popup opened by kivi.popup_dialog():
337 $js->dialog->close('#jqueryui_popup_dialog');
339 # Finally render the JSON response:
342 # Rendering can also be chained, e.g.
343 $js->html('#selector', $html)
349 This module enables the generation of jQuery-using JavaScript code on
350 the server side. That code is then evaluated in a safe way on the
353 The workflow is usually that the client creates an AJAX request, the
354 server creates some actions and sends them back, and the client then
355 implements each of these actions.
357 There are three things that need to be done for this to work:
361 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
363 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
365 =item 3. The server must use this module.
369 The functions called on the client side are mostly jQuery
370 functions. Other functionality may be added later.
372 Note that L<SL::Controller/render> is aware of this module which saves
373 you some boilerplate. The following two calls are equivalent:
375 $controller->render($client_js);
376 $controller->render(\$client_js->to_json, { type => 'json' });
378 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
384 Returns the actions gathered so far as an array reference. Each
385 element is an array reference containing at least two items: the
386 function's name and what it is called on. Additional array elements
387 are the function parameters.
391 Returns the actions gathered so far as a JSON string ready to be sent
394 =item C<render [$controller]>
396 Renders C<$self> via the controller. Useful for chaining. Equivalent
399 $controller->render(\$self->to_json, { type => 'json' });
401 The controller instance to use can be set during object creation (see
402 synopsis) or as an argument to C<render>.
406 Tells C<$self> that the next action is to be called on a jQuery UI
407 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
410 $js->dialog->close('#jqueryui_popup_dialog');
414 Tells C<$self> that the next action is to be called on a jstree
415 instance. For example:
417 $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
421 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
423 =head2 GENERIC FUNCTION
425 All of the following functions can be invoked in two ways: either by
426 calling the function name directly on C<$self> or by calling
427 L</action> with the function name as the first parameter. Therefore
428 the following two calls are identical:
430 $js->insertAfter($html, '#some-id');
431 $js->action('insertAfter', $html, '#some-id');
433 The second form, calling L</action>, is more to type but can be useful
434 in situations in which you have to call one of two functions depending
435 on context. For example, when you want to insert new code in a
436 list. If the list is empty you might have to use C<appendTo>, if it
437 isn't you might have to use C<insertAfter>. Example:
439 my $html = $self->render(...);
440 $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
444 my $html = $self->render(...);
445 if ($list_is_empty) {
446 $js->appendTo($html, '#text-block-list');
448 $js->insertAfter($html, '#text-block-' . $self->text_block->id);
451 The first variation is obviously better suited for chaining.
455 =item C<action $method, @args>
457 Call the function with the name C<$method> on C<$self> with arguments
458 C<@args>. Returns the return value of the actual function
459 called. Useful for chaining (see above).
461 =item C<action_if $condition, $method, @args>
463 Call the function with the name C<$method> on C<$self> with arguments
464 C<@args> if C<$condition> is trueish. Does nothing otherwise.
466 Returns the return value of the actual function called if
467 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
470 This function is equivalent to the following:
473 $obj->$method(@args);
476 But it is easier to integrate into a method call chain, e.g.:
478 $js->html('#content', $html)
479 ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
484 =head2 ADDITIONAL FUNCTIONS
488 =item C<flash $type, $message>
490 Display a C<$message> in the flash of type C<$type>. Multiple calls of
491 C<flash> on the same C<$self> will be merged by type.
493 On the client side the flashes of all types will be cleared after each
494 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
495 This clearing can be switched of by the function C<no_flash_clear>
497 =item C<flash_detail $type, $message>
499 Display a detailed message C<$message> in the flash of type C<$type>. Multiple calls of
500 C<flash_detail> on the same C<$self> will be merged by type.
501 So the flash message can be hold short and the visibility of details can toggled by the user.
503 =item C<no_flash_clear>
505 No automatic clearing of flash after successful ClientJS call
507 =item C<error $message>
509 Causes L<to_json> (and therefore L<render>) to output a JSON object
510 that only contains an C<error> field set to this C<$message>. The
511 client will then show the message in the 'error' flash.
513 The messages of multiple calls of C<error> on the same C<$self> will
516 =item C<redirect_to $url>
518 Redirects the browser window to the new URL by setting the JavaScript
519 property C<window.location.href>. Note that
520 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
521 function if the current request is an AJAX request as determined by
522 L<SL::Request/is_ajax>.
526 =head2 KIVITENDO FUNCTIONS
528 The following functions from the C<kivi> namespace are supported:
532 =item Displaying stuff
534 C<flash> (don't call directly, use L</flash> instead)
536 =item Running functions
538 C<run>, C<run_once_for>
546 =head2 JQUERY FUNCTIONS
548 The following jQuery functions are supported:
554 C<hide>, C<show>, C<toggle>
556 =item DOM insertion, around
558 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
560 =item DOM insertion, inside
562 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
564 =item DOM insertion, outside
566 C<after>, C<before>, C<insertAfter>, C<insertBefore>
572 =item DOM replacement
574 C<replaceAll>, C<replaceWith>
576 =item General attributes
578 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
580 =item Class attributes
582 C<addClass>, C<removeClass>, C<toggleClass>
586 C<data>, C<removeData>
592 =item Generic Event Handlers
594 C<on>, C<off>, C<one>
596 These attach/detach event listeners to specific selectors. The first
597 argument is the selector, the second the name of the events and the
598 third argument is the name of the handler function. That function must
599 already exist when the handler is added.
603 =head2 JQUERY POPUP DIALOG PLUGIN
605 Supported functions of the C<popup dialog> plugin to jQuery. They are
606 invoked by first calling C<dialog> in the ClientJS instance and then
609 $js->dialog->close(...);
613 =item Closing and removing the popup
619 =head2 AJAXFORM JQUERY PLUGIN
621 The following functions of the C<ajaxForm> plugin to jQuery are
626 =item All functions by the generic accessor function:
632 =head2 JSTREE JQUERY PLUGIN
634 Supported functions of the C<jstree> plugin to jQuery. They are
635 invoked by first calling C<jstree> in the ClientJS instance and then
638 $js->jstree->open_node(...);
642 =item Operations on the whole tree
646 =item Opening and closing nodes
648 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
649 C<close_all>, C<save_opened>, C<reopen>
651 =item Modifying nodes
653 C<rename_node>, C<delete_node>, C<move_node>
655 =item Selecting nodes (from the 'ui' jstree plugin)
657 C<select_node>, C<deselect_node>, C<deselect_all>
661 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
663 In order to not have to maintain two files (this one and
664 C<js/client_js.js>) there's a script that can parse this file's
665 C<%supported_methods> definition and generate the file
666 C<js/client_js.js> accordingly. The steps are:
670 =item 1. Add lines in this file to the C<%supported_methods> hash. The
671 key is the function name and the value is the number of expected
672 parameters. The value can be negative to indicate that the function
673 takes at least the absolute of this value as parameters and optionally
674 more. In such a case the C<E<lt>ARGSE<gt>> format expands to an actual
675 array (and the individual elements if the value is positive>.
677 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
678 generate C<js/client_js.js> automatically.
680 =item 3. Reload the files in your browser (cleaning its cache can also
685 The template file used for generated C<js/client_js.js> is
686 C<scripts/generate_client_js_actions.tpl>.
694 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>