5 use parent qw(Rose::Object);
 
  10 use Rose::Object::MakeMethods::Generic
 
  12   'scalar --get_set_init' => [ qw(_actions _flash _error) ],
 
  15 my %supported_methods = (
 
  16   # ## Non-jQuery methods ##
 
  17   flash        => 2,            # kivi.display_flash(<TARGET>, <ARGS>)
 
  26   # DOM insertion, around
 
  32   # DOM insertion, inside
 
  40   # DOM insertion, outside
 
  73   # ## jqModal plugin ##
 
  75   # Closing and removing the popup
 
  78   # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
 
  80   # Operations on the whole tree
 
  84   # Opening and closing nodes
 
  85   'jstree:open_node'     => 2,
 
  86   'jstree:open_all'      => 2,
 
  87   'jstree:close_node'    => 2,
 
  88   'jstree:close_all'     => 2,
 
  89   'jstree:toggle_node'   => 2,
 
  90   'jstree:save_opened'   => 1,
 
  94   'jstree:create_node'   => 4,
 
  95   'jstree:rename_node'   => 3,
 
  96   'jstree:delete_node'   => 2,
 
  97   'jstree:move_node'     => 5,
 
  99   # Selecting nodes (from the 'ui' plugin to jstree)
 
 100   'jstree:select_node'   => 2,  # $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>, true)
 
 101   'jstree:deselect_node' => 2,
 
 102   'jstree:deselect_all'  => 1,
 
 105   redirect_to            => 1,  # window.location.href = <TARGET>
 
 107   reinit_widgets         => 0,  # kivi.reinit_widgets()
 
 113   my ($self, @args) = @_;
 
 115   my $method        =  $AUTOLOAD;
 
 117   return if $method eq 'DESTROY';
 
 118   return $self->action($method, @args);
 
 122   my ($self, $method, @args) = @_;
 
 124   $method      =  (delete($self->{_prefix}) || '') . $method;
 
 125   my $num_args =  $supported_methods{$method};
 
 127   croak "Unsupported jQuery action: $method"                                                    unless defined $num_args;
 
 128   croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if     scalar(@args) != $num_args;
 
 131     # Force flattening from SL::Presenter::EscapedText: "" . $...
 
 132     $args[0] =  "" . $args[0];
 
 133     $args[0] =~ s/^\s+//;
 
 136   push @{ $self->_actions }, [ $method, @args ];
 
 142   my ($self, $condition, @args) = @_;
 
 144   return $condition ? $self->action(@args) : $self;
 
 162   return SL::JSON::to_json({ error        => $self->_error   }) if $self->_error;
 
 163   return SL::JSON::to_json({ eval_actions => $self->_actions });
 
 168   return $self->_actions;
 
 172   my ($self, $controller) = @_;
 
 173   $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
 
 174   return $controller->render(\$self->to_json, { type => 'json' });
 
 179   $self->{_prefix} = 'jstree:';
 
 184   my ($self, $type, @messages) = @_;
 
 186   my $message = join ' ', grep { $_ } @messages;
 
 188   if (!$self->_flash->{$type}) {
 
 189     $self->_flash->{$type} = [ 'flash', $type, $message ];
 
 190     push @{ $self->_actions }, $self->_flash->{$type};
 
 192     $self->_flash->{$type}->[-1] .= ' ' . $message;
 
 199   my ($self, @messages) = @_;
 
 201   $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
 
 215 SL::ClientJS - Easy programmatic client-side JavaScript generation
 
 220 First some JavaScript code:
 
 222   // In the client generate an AJAX request whose 'success' handler
 
 223   // calls "eval_json_result(data)":
 
 225     action: "SomeController/the_action",
 
 226     id:     $('#some_input_field').val()
 
 228   $.post("controller.pl", data, eval_json_result);
 
 232   # In the controller itself. First, make sure that the "client_js.js"
 
 233   # is loaded. This must be done when the whole side is loaded, so
 
 234   # it's not in the action called by the AJAX request shown above.
 
 235   $::request->layout->use_javascript('client_js.js');
 
 237   # Now in that action called via AJAX:
 
 238   sub action_the_action {
 
 241     # Create a new client-side JS object and do stuff with it!
 
 242     my $js = SL::ClientJS->new;
 
 244     # Show some element on the page:
 
 245     $js->show('#usually_hidden');
 
 247     # Set to hidden inputs. Yes, calls can be chained!
 
 248     $js->val('#hidden_id', $self->new_id)
 
 249        ->val('#other_type', 'Unicorn');
 
 251     # Replace some HTML code:
 
 252     my $html = $self->render('SomeController/the_action', { output => 0 });
 
 253     $js->html('#id_with_new_content', $html);
 
 255     # Operations on a jstree: rename a node and select it
 
 256     my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
 
 257     $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
 
 258        ->jstree->select_node('#tb-' . $text_block->id);
 
 260     # Finally render the JSON response:
 
 263     # Rendering can also be chained, e.g.
 
 264     $js->html('#selector', $html)
 
 270 This module enables the generation of jQuery-using JavaScript code on
 
 271 the server side. That code is then evaluated in a safe way on the
 
 274 The workflow is usally that the client creates an AJAX request, the
 
 275 server creates some actions and sends them back, and the client then
 
 276 implements each of these actions.
 
 278 There are three things that need to be done for this to work:
 
 282 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
 
 284 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
 
 286 =item 3. The server must use this module.
 
 290 The functions called on the client side are mostly jQuery
 
 291 functions. Other functionality may be added later.
 
 293 Note that L<SL::Controller/render> is aware of this module which saves
 
 294 you some boilerplate. The following two calls are equivalent:
 
 296   $controller->render($client_js);
 
 297   $controller->render(\$client_js->to_json, { type => 'json' });
 
 299 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
 
 305 Returns the actions gathered so far as an array reference. Each
 
 306 element is an array reference containing at least two items: the
 
 307 function's name and what it is called on. Additional array elements
 
 308 are the function parameters.
 
 312 Returns the actions gathered so far as a JSON string ready to be sent
 
 315 =item C<render $controller>
 
 317 Renders C<$self> via the controller. Useful for chaining. Equivalent
 
 320   $controller->render(\$self->to_json, { type => 'json' });
 
 324 Tells C<$self> that the next action is to be called on a jstree
 
 325 instance. For example:
 
 327   $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
 
 331 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
 
 333 =head2 GENERIC FUNCTION
 
 335 All of the following functions can be invoked in two ways: either by
 
 336 calling the function name directly on C<$self> or by calling
 
 337 L</action> with the function name as the first parameter. Therefore
 
 338 the following two calls are identical:
 
 340   $js->insertAfter($html, '#some-id');
 
 341   $js->action('insertAfter', $html, '#some-id');
 
 343 The second form, calling L</action>, is more to type but can be useful
 
 344 in situations in which you have to call one of two functions depending
 
 345 on context. For example, when you want to insert new code in a
 
 346 list. If the list is empty you might have to use C<appendTo>, if it
 
 347 isn't you might have to use C<insertAfter>. Example:
 
 349   my $html = $self->render(...);
 
 350   $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
 
 354   my $html = $self->render(...);
 
 355   if ($list_is_empty) {
 
 356     $js->appendTo($html, '#text-block-list');
 
 358     $js->insertAfter($html, '#text-block-' . $self->text_block->id);
 
 361 The first variation is obviously better suited for chaining.
 
 365 =item C<action $method, @args>
 
 367 Call the function with the name C<$method> on C<$self> with arguments
 
 368 C<@args>. Returns the return value of the actual function
 
 369 called. Useful for chaining (see above).
 
 371 =item C<action_if $condition, $method, @args>
 
 373 Call the function with the name C<$method> on C<$self> with arguments
 
 374 C<@args> if C<$condition> is trueish. Does nothing otherwise.
 
 376 Returns the return value of the actual function called if
 
 377 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
 
 380 This function is equivalent to the following:
 
 383     $obj->$method(@args);
 
 386 But it is easier to integrate into a method call chain, e.g.:
 
 388   $js->html('#content', $html)
 
 389      ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
 
 394 =head2 ADDITIONAL FUNCTIONS
 
 398 =item C<flash $type, $message>
 
 400 Display a C<$message> in the flash of type C<$type>. Multiple calls of
 
 401 C<flash> on the same C<$self> will be merged by type.
 
 403 On the client side the flashes of all types will be cleared after each
 
 404 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
 
 406 =item C<error $message>
 
 408 Causes L<to_json> (and therefore L<render>) to output a JSON object
 
 409 that only contains an C<error> field set to this C<$message>. The
 
 410 client will then show the message in the 'error' flash.
 
 412 The messages of multiple calls of C<error> on the same C<$self> will
 
 415 =item C<redirect_to $url>
 
 417 Redirects the browser window to the new URL by setting the JavaScript
 
 418 property C<window.location.href>. Note that
 
 419 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
 
 420 function if the current request is an AJAX request as determined by
 
 421 L<SL::Request/is_ajax>.
 
 425 =head2 JQUERY FUNCTIONS
 
 427 The following jQuery functions are supported:
 
 433 C<hide>, C<show>, C<toggle>
 
 435 =item DOM insertion, around
 
 437 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
 
 439 =item DOM insertion, inside
 
 441 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
 
 443 =item DOM insertion, outside
 
 445 C<after>, C<before>, C<insertAfter>, C<insertBefore>
 
 451 =item DOM replacement
 
 453 C<replaceAll>, C<replaceWith>
 
 455 =item General attributes
 
 457 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
 
 461 C<data>, C<removeData>
 
 469 =head2 JSTREE JQUERY PLUGIN
 
 471 The following functions of the C<jstree> plugin to jQuery are
 
 476 =item Operations on the whole tree
 
 480 =item Opening and closing nodes
 
 482 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
 
 483 C<close_all>, C<save_opened>, C<reopen>
 
 485 =item Modifying nodes
 
 487 C<rename_node>, C<delete_node>, C<move_node>
 
 489 =item Selecting nodes (from the 'ui' jstree plugin)
 
 491 C<select_node>, C<deselect_node>, C<deselect_all>
 
 495 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
 
 497 In order not having to maintain two files (this one and
 
 498 C<js/client_js.js>) there's a script that can parse this file's
 
 499 C<%supported_methods> definition and generate the file
 
 500 C<js/client_js.js> accordingly. The steps are:
 
 504 =item 1. Add lines in this file to the C<%supported_methods> hash. The
 
 505 key is the function name and the value is the number of expected
 
 508 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
 
 509 generate C<js/client_js.js> automatically.
 
 511 =item 3. Reload the files in your browser (cleaning its cache can also
 
 516 The template file used for generated C<js/client_js.js> is
 
 517 C<scripts/generate_client_js_actions.tpl>.
 
 525 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>