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,            # display_flash(<TARGET>, <ARGS>)
 
  26   # DOM insertion, around
 
  32   # DOM insertion, inside
 
  40   # DOM insertion, outside
 
  68   # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
 
  70   # Operations on the whole tree
 
  74   # Opening and closing nodes
 
  75   'jstree:open_node'     => 2,
 
  76   'jstree:open_all'      => 2,
 
  77   'jstree:close_node'    => 2,
 
  78   'jstree:close_all'     => 2,
 
  79   'jstree:toggle_node'   => 2,
 
  80   'jstree:save_opened'   => 1,
 
  84   'jstree:create_node'   => 4,
 
  85   'jstree:rename_node'   => 3,
 
  86   'jstree:delete_node'   => 2,
 
  87   'jstree:move_node'     => 5,
 
  89   # Selecting nodes (from the 'ui' plugin to jstree)
 
  90   'jstree:select_node'   => 2,  # $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>, true)
 
  91   'jstree:deselect_node' => 2,
 
  92   'jstree:deselect_all'  => 1,
 
  98   my ($self, @args) = @_;
 
 100   my $method        =  $AUTOLOAD;
 
 102   return if $method eq 'DESTROY';
 
 103   return $self->action($method, @args);
 
 107   my ($self, $method, @args) = @_;
 
 109   $method      =  (delete($self->{_prefix}) || '') . $method;
 
 110   my $num_args =  $supported_methods{$method};
 
 112   croak "Unsupported jQuery action: $method"                                                    unless defined $num_args;
 
 113   croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if     scalar(@args) != $num_args;
 
 116     # Force flattening from SL::Presenter::EscapedText: "" . $...
 
 117     $args[0] =  "" . $args[0];
 
 118     $args[0] =~ s/^\s+//;
 
 121   push @{ $self->_actions }, [ $method, @args ];
 
 141   return SL::JSON::to_json({ error        => $self->_error   }) if $self->_error;
 
 142   return SL::JSON::to_json({ eval_actions => $self->_actions });
 
 147   return $self->_actions;
 
 151   my ($self, $controller) = @_;
 
 152   return $controller->render(\$self->to_json, { type => 'json' });
 
 157   $self->{_prefix} = 'jstree:';
 
 162   my ($self, $type, @messages) = @_;
 
 164   my $message = join ' ', grep { $_ } @messages;
 
 166   if (!$self->_flash->{$type}) {
 
 167     $self->_flash->{$type} = [ 'flash', $type, $message ];
 
 168     push @{ $self->_actions }, $self->_flash->{$type};
 
 170     $self->_flash->{$type}->[-1] .= ' ' . $message;
 
 177   my ($self, @messages) = @_;
 
 179   $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
 
 193 SL::ClientJS - Easy programmatic client-side JavaScript generation
 
 198 First some JavaScript code:
 
 200   // In the client generate an AJAX request whose 'success' handler
 
 201   // calls "eval_json_response(data)":
 
 203     action: "SomeController/the_action",
 
 204     id:     $('#some_input_field').val()
 
 206   $.post("controller.pl", data, eval_json_response);
 
 210   # In the controller itself. First, make sure that the "client_js.js"
 
 211   # is loaded. This must be done when the whole side is loaded, so
 
 212   # it's not in the action called by the AJAX request shown above.
 
 213   $::request->layout->use_javascript('client_js.js');
 
 215   # Now in that action called via AJAX:
 
 216   sub action_the_action {
 
 219     # Create a new client-side JS object and do stuff with it!
 
 220     my $js = SL::ClientJS->new;
 
 222     # Show some element on the page:
 
 223     $js->show('#usually_hidden');
 
 225     # Set to hidden inputs. Yes, calls can be chained!
 
 226     $js->val('#hidden_id', $self->new_id)
 
 227        ->val('#other_type', 'Unicorn');
 
 229     # Replace some HTML code:
 
 230     my $html = $self->render('SomeController/the_action', { output => 0 });
 
 231     $js->html('#id_with_new_content', $html);
 
 233     # Operations on a jstree: rename a node and select it
 
 234     my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
 
 235     $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
 
 236        ->jstree->select_node('#tb-' . $text_block->id);
 
 238     # Finally render the JSON response:
 
 241     # Rendering can also be chained, e.g.
 
 242     $js->html('#selector', $html)
 
 248 This module enables the generation of jQuery-using JavaScript code on
 
 249 the server side. That code is then evaluated in a safe way on the
 
 252 The workflow is usally that the client creates an AJAX request, the
 
 253 server creates some actions and sends them back, and the client then
 
 254 implements each of these actions.
 
 256 There are three things that need to be done for this to work:
 
 260 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
 
 262 =item 2. The client code needs to call C<eval_json_response()> with the result returned from the server.
 
 264 =item 3. The server must use this module.
 
 268 The functions called on the client side are mostly jQuery
 
 269 functions. Other functionality may be added later.
 
 271 Note that L<SL::Controller/render> is aware of this module which saves
 
 272 you some boilerplate. The following two calls are equivalent:
 
 274   $controller->render($client_js);
 
 275   $controller->render(\$client_js->to_json, { type => 'json' });
 
 277 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
 
 283 Returns the actions gathered so far as an array reference. Each
 
 284 element is an array reference containing at least two items: the
 
 285 function's name and what it is called on. Additional array elements
 
 286 are the function parameters.
 
 290 Returns the actions gathered so far as a JSON string ready to be sent
 
 293 =item C<render $controller>
 
 295 Renders C<$self> via the controller. Useful for chaining. Equivalent
 
 298   $controller->render(\$self->to_json, { type => 'json' });
 
 302 Tells C<$self> that the next action is to be called on a jstree
 
 303 instance. For example:
 
 305   $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
 
 309 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
 
 311 =head2 GENERIC FUNCTION
 
 313 All of the following functions can be invoked in two ways: either by
 
 314 calling the function name directly on C<$self> or by calling
 
 315 L</action> with the function name as the first parameter. Therefore
 
 316 the following two calls are identical:
 
 318   $js->insertAfter($html, '#some-id');
 
 319   $js->action('insertAfter', $html, '#some-id');
 
 321 The second form, calling L</action>, is more to type but can be useful
 
 322 in situations in which you have to call one of two functions depending
 
 323 on context. For example, when you want to insert new code in a
 
 324 list. If the list is empty you might have to use C<appendTo>, if it
 
 325 isn't you might have to use C<insertAfter>. Example:
 
 327   my $html = $self->render(...);
 
 328   $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
 
 332   my $html = $self->render(...);
 
 333   if ($list_is_empty) {
 
 334     $js->appendTo($html, '#text-block-list');
 
 336     $js->insertAfter($html, '#text-block-' . $self->text_block->id);
 
 339 The first variation is obviously better suited for chaining.
 
 341 Additional functions:
 
 345 =item C<flash $type, $message>
 
 347 Display a C<$message> in the flash of type C<$type>. Multiple calls of
 
 348 C<flash> on the same C<$self> will be merged by type.
 
 350 On the client side the flash of this type will be cleared before the
 
 353 =item C<error $message>
 
 355 Causes L<to_json> (and therefore L<render>) to output a JSON object
 
 356 that only contains an C<error> field set to this C<$message>. The
 
 357 client will then show the message in the 'error' flash.
 
 359 The messages of multiple calls of C<error> on the same C<$self> will
 
 364 =head2 JQUERY FUNCTIONS
 
 366 The following jQuery functions are supported:
 
 372 C<hide>, C<show>, C<toggle>
 
 374 =item DOM insertion, around
 
 376 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
 
 378 =item DOM insertion, inside
 
 380 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
 
 382 =item DOM insertion, outside
 
 384 C<after>, C<before>, C<insertAfter>, C<insertBefore>
 
 390 =item DOM replacement
 
 392 C<replaceAll>, C<replaceWith>
 
 394 =item General attributes
 
 396 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
 
 400 C<data>, C<removeData>
 
 408 =head2 JSTREE JQUERY PLUGIN
 
 410 The following functions of the C<jstree> plugin to jQuery are
 
 415 =item Operations on the whole tree
 
 419 =item Opening and closing nodes
 
 421 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
 
 422 C<close_all>, C<save_opened>, C<reopen>
 
 424 =item Modifying nodes
 
 426 C<rename_node>, C<delete_node>, C<move_node>
 
 428 =item Selecting nodes (from the 'ui' jstree plugin)
 
 430 C<select_node>, C<deselect_node>, C<deselect_all>
 
 434 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
 
 436 In order not having to maintain two files (this one and
 
 437 C<js/client_js.js>) there's a script that can parse this file's
 
 438 C<%supported_methods> definition and generate the file
 
 439 C<js/client_js.js> accordingly. The steps are:
 
 443 =item 1. Add lines in this file to the C<%supported_methods> hash. The
 
 444 key is the function name and the value is the number of expected
 
 447 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
 
 448 generate C<js/client_js.js> automatically.
 
 450 =item 3. Reload the files in your browser (cleaning its cache can also
 
 455 The template file used for generated C<js/client_js.js> is
 
 456 C<scripts/generate_client_js_actions.tpl>.
 
 464 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>