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   # Generic Event Handling ## pattern: $(<TARGET>).<FUNCTION>(<ARG1>, kivi.get_function_by_name(<ARG2>))
 
  78   # ## jQuery UI dialog plugin ## pattern: $(<TARGET>).dialog('<FUNCTION>')
 
  80   # Closing and removing the popup
 
  83   # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
 
  85   # Operations on the whole tree
 
  89   # Opening and closing nodes
 
  90   'jstree:open_node'     => 2,
 
  91   'jstree:open_all'      => 2,
 
  92   'jstree:close_node'    => 2,
 
  93   'jstree:close_all'     => 2,
 
  94   'jstree:toggle_node'   => 2,
 
  95   'jstree:save_opened'   => 1,
 
  99   'jstree:create_node'   => 4,
 
 100   'jstree:rename_node'   => 3,
 
 101   'jstree:delete_node'   => 2,
 
 102   'jstree:move_node'     => 5,
 
 104   # Selecting nodes (from the 'ui' plugin to jstree)
 
 105   'jstree:select_node'   => 2,  # $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>, true)
 
 106   'jstree:deselect_node' => 2,
 
 107   'jstree:deselect_all'  => 1,
 
 110   redirect_to            => 1,  # window.location.href = <TARGET>
 
 112   reinit_widgets         => 0,  # kivi.reinit_widgets()
 
 118   my ($self, @args) = @_;
 
 120   my $method        =  $AUTOLOAD;
 
 122   return if $method eq 'DESTROY';
 
 123   return $self->action($method, @args);
 
 127   my ($self, $method, @args) = @_;
 
 129   $method      =  (delete($self->{_prefix}) || '') . $method;
 
 130   my $num_args =  $supported_methods{$method};
 
 132   croak "Unsupported jQuery action: $method"                                                    unless defined $num_args;
 
 133   croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if     scalar(@args) != $num_args;
 
 135   foreach my $idx (0..$num_args - 1) {
 
 136     # Force flattening from SL::Presenter::EscapedText and trim leading whitespace for scalars
 
 137     $args[$idx] =  "" . $args[$idx] if  ref($args[$idx]) eq 'SL::Presenter::EscapedText';
 
 138     $args[$idx] =~ s/^\s+//         if !ref($args[$idx]);
 
 141   push @{ $self->_actions }, [ $method, @args ];
 
 147   my ($self, $condition, @args) = @_;
 
 149   return $condition ? $self->action(@args) : $self;
 
 167   return SL::JSON::to_json({ error        => $self->_error   }) if $self->_error;
 
 168   return SL::JSON::to_json({ eval_actions => $self->_actions });
 
 173   return $self->_actions;
 
 177   my ($self, $controller) = @_;
 
 178   $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
 
 179   return $controller->render(\$self->to_json, { type => 'json' });
 
 184   $self->{_prefix} = 'jstree:';
 
 190   $self->{_prefix} = 'dialog:';
 
 195   my ($self, $type, @messages) = @_;
 
 197   my $message = join ' ', grep { $_ } @messages;
 
 199   if (!$self->_flash->{$type}) {
 
 200     $self->_flash->{$type} = [ 'flash', $type, $message ];
 
 201     push @{ $self->_actions }, $self->_flash->{$type};
 
 203     $self->_flash->{$type}->[-1] .= ' ' . $message;
 
 210   my ($self, @messages) = @_;
 
 212   $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
 
 226 SL::ClientJS - Easy programmatic client-side JavaScript generation
 
 231 First some JavaScript code:
 
 233   // In the client generate an AJAX request whose 'success' handler
 
 234   // calls "eval_json_result(data)":
 
 236     action: "SomeController/the_action",
 
 237     id:     $('#some_input_field').val()
 
 239   $.post("controller.pl", data, eval_json_result);
 
 243   # In the controller itself. First, make sure that the "client_js.js"
 
 244   # is loaded. This must be done when the whole side is loaded, so
 
 245   # it's not in the action called by the AJAX request shown above.
 
 246   $::request->layout->use_javascript('client_js.js');
 
 248   # Now in that action called via AJAX:
 
 249   sub action_the_action {
 
 252     # Create a new client-side JS object and do stuff with it!
 
 253     my $js = SL::ClientJS->new;
 
 255     # Show some element on the page:
 
 256     $js->show('#usually_hidden');
 
 258     # Set to hidden inputs. Yes, calls can be chained!
 
 259     $js->val('#hidden_id', $self->new_id)
 
 260        ->val('#other_type', 'Unicorn');
 
 262     # Replace some HTML code:
 
 263     my $html = $self->render('SomeController/the_action', { output => 0 });
 
 264     $js->html('#id_with_new_content', $html);
 
 266     # Operations on a jstree: rename a node and select it
 
 267     my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
 
 268     $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
 
 269        ->jstree->select_node('#tb-' . $text_block->id);
 
 271     # Close a popup opened by kivi.popup_dialog():
 
 272     $js->dialog->close('#jqueryui_popup_dialog');
 
 274     # Finally render the JSON response:
 
 277     # Rendering can also be chained, e.g.
 
 278     $js->html('#selector', $html)
 
 284 This module enables the generation of jQuery-using JavaScript code on
 
 285 the server side. That code is then evaluated in a safe way on the
 
 288 The workflow is usally that the client creates an AJAX request, the
 
 289 server creates some actions and sends them back, and the client then
 
 290 implements each of these actions.
 
 292 There are three things that need to be done for this to work:
 
 296 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
 
 298 =item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
 
 300 =item 3. The server must use this module.
 
 304 The functions called on the client side are mostly jQuery
 
 305 functions. Other functionality may be added later.
 
 307 Note that L<SL::Controller/render> is aware of this module which saves
 
 308 you some boilerplate. The following two calls are equivalent:
 
 310   $controller->render($client_js);
 
 311   $controller->render(\$client_js->to_json, { type => 'json' });
 
 313 =head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE
 
 319 Returns the actions gathered so far as an array reference. Each
 
 320 element is an array reference containing at least two items: the
 
 321 function's name and what it is called on. Additional array elements
 
 322 are the function parameters.
 
 326 Returns the actions gathered so far as a JSON string ready to be sent
 
 329 =item C<render $controller>
 
 331 Renders C<$self> via the controller. Useful for chaining. Equivalent
 
 334   $controller->render(\$self->to_json, { type => 'json' });
 
 338 Tells C<$self> that the next action is to be called on a jQuery UI
 
 339 dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
 
 342   $js->dialog->close('#jqueryui_popup_dialog');
 
 346 Tells C<$self> that the next action is to be called on a jstree
 
 347 instance. For example:
 
 349   $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
 
 353 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
 
 355 =head2 GENERIC FUNCTION
 
 357 All of the following functions can be invoked in two ways: either by
 
 358 calling the function name directly on C<$self> or by calling
 
 359 L</action> with the function name as the first parameter. Therefore
 
 360 the following two calls are identical:
 
 362   $js->insertAfter($html, '#some-id');
 
 363   $js->action('insertAfter', $html, '#some-id');
 
 365 The second form, calling L</action>, is more to type but can be useful
 
 366 in situations in which you have to call one of two functions depending
 
 367 on context. For example, when you want to insert new code in a
 
 368 list. If the list is empty you might have to use C<appendTo>, if it
 
 369 isn't you might have to use C<insertAfter>. Example:
 
 371   my $html = $self->render(...);
 
 372   $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
 
 376   my $html = $self->render(...);
 
 377   if ($list_is_empty) {
 
 378     $js->appendTo($html, '#text-block-list');
 
 380     $js->insertAfter($html, '#text-block-' . $self->text_block->id);
 
 383 The first variation is obviously better suited for chaining.
 
 387 =item C<action $method, @args>
 
 389 Call the function with the name C<$method> on C<$self> with arguments
 
 390 C<@args>. Returns the return value of the actual function
 
 391 called. Useful for chaining (see above).
 
 393 =item C<action_if $condition, $method, @args>
 
 395 Call the function with the name C<$method> on C<$self> with arguments
 
 396 C<@args> if C<$condition> is trueish. Does nothing otherwise.
 
 398 Returns the return value of the actual function called if
 
 399 C<$condition> is trueish and C<$self> otherwise. Useful for chaining
 
 402 This function is equivalent to the following:
 
 405     $obj->$method(@args);
 
 408 But it is easier to integrate into a method call chain, e.g.:
 
 410   $js->html('#content', $html)
 
 411      ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
 
 416 =head2 ADDITIONAL FUNCTIONS
 
 420 =item C<flash $type, $message>
 
 422 Display a C<$message> in the flash of type C<$type>. Multiple calls of
 
 423 C<flash> on the same C<$self> will be merged by type.
 
 425 On the client side the flashes of all types will be cleared after each
 
 426 successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
 
 428 =item C<error $message>
 
 430 Causes L<to_json> (and therefore L<render>) to output a JSON object
 
 431 that only contains an C<error> field set to this C<$message>. The
 
 432 client will then show the message in the 'error' flash.
 
 434 The messages of multiple calls of C<error> on the same C<$self> will
 
 437 =item C<redirect_to $url>
 
 439 Redirects the browser window to the new URL by setting the JavaScript
 
 440 property C<window.location.href>. Note that
 
 441 L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
 
 442 function if the current request is an AJAX request as determined by
 
 443 L<SL::Request/is_ajax>.
 
 447 =head2 JQUERY FUNCTIONS
 
 449 The following jQuery functions are supported:
 
 455 C<hide>, C<show>, C<toggle>
 
 457 =item DOM insertion, around
 
 459 C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>
 
 461 =item DOM insertion, inside
 
 463 C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>
 
 465 =item DOM insertion, outside
 
 467 C<after>, C<before>, C<insertAfter>, C<insertBefore>
 
 473 =item DOM replacement
 
 475 C<replaceAll>, C<replaceWith>
 
 477 =item General attributes
 
 479 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
 
 483 C<data>, C<removeData>
 
 489 =item Generic Event Handlers
 
 491 C<on>, C<off>, C<one>
 
 493 These attach/detach event listeners to specific selectors. The first
 
 494 argument is the selector, the second the name of the events and the
 
 495 third argument is the name of the handler function. That function must
 
 496 already exist when the handler is added.
 
 500 =head2 JSTREE JQUERY PLUGIN
 
 502 The following functions of the C<jstree> plugin to jQuery are
 
 507 =item Operations on the whole tree
 
 511 =item Opening and closing nodes
 
 513 C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
 
 514 C<close_all>, C<save_opened>, C<reopen>
 
 516 =item Modifying nodes
 
 518 C<rename_node>, C<delete_node>, C<move_node>
 
 520 =item Selecting nodes (from the 'ui' jstree plugin)
 
 522 C<select_node>, C<deselect_node>, C<deselect_all>
 
 526 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
 
 528 In order not having to maintain two files (this one and
 
 529 C<js/client_js.js>) there's a script that can parse this file's
 
 530 C<%supported_methods> definition and generate the file
 
 531 C<js/client_js.js> accordingly. The steps are:
 
 535 =item 1. Add lines in this file to the C<%supported_methods> hash. The
 
 536 key is the function name and the value is the number of expected
 
 539 =item 2. Run C<scripts/generate_client_js_actions.pl>. It will
 
 540 generate C<js/client_js.js> automatically.
 
 542 =item 3. Reload the files in your browser (cleaning its cache can also
 
 547 The template file used for generated C<js/client_js.js> is
 
 548 C<scripts/generate_client_js_actions.tpl>.
 
 556 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>