Upgrade-Doku: Hinweis auf benötigtes Perl-Modul IPC::Run
[kivitendo-erp.git] / SL / ClientJS.pm
index 47f8a22..49bde80 100644 (file)
@@ -9,10 +9,13 @@ use SL::JSON ();
 
 use Rose::Object::MakeMethods::Generic
 (
 
 use Rose::Object::MakeMethods::Generic
 (
-  'scalar --get_set_init' => [ qw(_actions) ],
+  scalar                  => [ qw() ],
+  'scalar --get_set_init' => [ qw(controller _actions _flash _flash_detail _no_flash_clear _error) ],
 );
 
 my %supported_methods = (
 );
 
 my %supported_methods = (
+  # ## jQuery basics ##
+
   # Basic effects
   hide         => 1,
   show         => 1,
   # Basic effects
   hide         => 1,
   show         => 1,
@@ -53,11 +56,79 @@ my %supported_methods = (
   removeProp   => 2,
   val          => 2,
 
   removeProp   => 2,
   val          => 2,
 
+  # Class attribute
+  addClass     => 2,
+  removeClass  => 2,
+  toggleClass  => 2,
+
   # Data storage
   data         => 3,
   removeData   => 2,
   # Data storage
   data         => 3,
   removeData   => 2,
+
+  # Form Events
+  focus        => 1, # kivi.set_focus(<TARGET>)
+
+  # Generic Event Handling ## pattern: $(<TARGET>).<FUNCTION>(<ARG1>, kivi.get_function_by_name(<ARG2>))
+  on           => 3,
+  off          => 3,
+  one          => 3,
+
+  # ## jQuery UI dialog plugin ## pattern: $(<TARGET>).dialog('<FUNCTION>')
+
+  # Opening and closing a popup
+  'dialog:open'          => 1, # kivi.popup_dialog(<TARGET>)
+  'dialog:close'         => 1,
+
+  # ## jQuery Form plugin ##
+  'ajaxForm'             => 1, # $(<TARGET>).ajaxForm({ success: eval_json_result })
+
+  # ## jstree plugin ## pattern: $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>)
+
+  # Operations on the whole tree
+  'jstree:lock'          => 1,
+  'jstree:unlock'        => 1,
+
+  # Opening and closing nodes
+  'jstree:open_node'     => 2,
+  'jstree:open_all'      => 2,
+  'jstree:close_node'    => 2,
+  'jstree:close_all'     => 2,
+  'jstree:toggle_node'   => 2,
+  'jstree:save_opened'   => 1,
+  'jstree:reopen'        => 1,
+
+  # Modifying nodes
+  'jstree:create_node'   => 4,
+  'jstree:rename_node'   => 3,
+  'jstree:delete_node'   => 2,
+  'jstree:move_node'     => 5,
+
+  # Selecting nodes (from the 'ui' plugin to jstree)
+  'jstree:select_node'   => 2,  # $.jstree._reference($(<TARGET>)).<FUNCTION>(<ARGS>, true)
+  'jstree:deselect_node' => 2,
+  'jstree:deselect_all'  => 1,
+
+  # ## ckeditor stuff ##
+  'focus_ckeditor'       => 1,  # kivi.focus_ckeditor_when_ready(<TARGET>)
+
+  # ## other stuff ##
+  redirect_to            => 1,  # window.location.href = <TARGET>
+  save_file              => 4,  # kivi.save_file(<TARGET>, <ARGS>)
+
+  flash                  => 2,  # kivi.display_flash(<TARGET>, <ARGS>)
+  flash_detail           => 2,  # kivi.display_flash_detail(<TARGET>, <ARGS>)
+  clear_flash            => 2,  # kivi.clear_flash(<TARGET>, <ARGS>)
+  reinit_widgets         => 0,  # kivi.reinit_widgets()
+  run                    => -1, # kivi.run(<TARGET>, <ARGS>)
+  run_once_for           => 3,  # kivi.run_once_for(<TARGET>, <ARGS>)
+
+  scroll_into_view       => 1,  # $(<TARGET>)[0].scrollIntoView()
+
+  set_cursor_position    => 2,  # kivi.set_cursor_position(<TARGET>, <ARGS>)
 );
 
 );
 
+my %trim_target_for = map { ($_ => 1) } qw(insertAfter insertBefore appendTo prependTo);
+
 sub AUTOLOAD {
   our $AUTOLOAD;
 
 sub AUTOLOAD {
   our $AUTOLOAD;
 
@@ -66,31 +137,71 @@ sub AUTOLOAD {
   my $method        =  $AUTOLOAD;
   $method           =~ s/.*:://;
   return if $method eq 'DESTROY';
   my $method        =  $AUTOLOAD;
   $method           =~ s/.*:://;
   return if $method eq 'DESTROY';
+  return $self->action($method, @args);
+}
+
+sub action {
+  my ($self, $method, @args) = @_;
 
 
+  $method      =  (delete($self->{_prefix}) || '') . $method;
   my $num_args =  $supported_methods{$method};
   my $num_args =  $supported_methods{$method};
-  $::lxdebug->message(0, "autoload method $method");
 
 
-  croak "Unsupported jQuery action: $method"                                                    unless defined $num_args;
-  croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)" if     scalar(@args) != $num_args;
+  croak "Unsupported jQuery action: $method" unless defined $num_args;
 
 
-  if ($num_args) {
-    # Force flattening from SL::Presenter::EscapedText: "" . $...
-    $args[0] =  "" . $args[0];
-    $args[0] =~ s/^\s+//;
+  if ($num_args > 0) {
+    croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted: $num_args)"          if scalar(@args) != $num_args;
+  } else {
+    $num_args *= -1;
+    croak "Parameter count mismatch for $method(actual: " . scalar(@args) . " wanted at least: $num_args)" if scalar(@args) < $num_args;
+    $num_args  = scalar @args;
   }
 
   }
 
+  foreach my $idx (0..$num_args - 1) {
+    # Force flattening from SL::Presenter::EscapedText.
+    $args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
+  }
+
+  # Trim leading whitespaces for certain jQuery functions that operate
+  # on HTML code: $("<p>test</p>").appendTo('#some-id'). jQuery croaks
+  # on leading whitespaces, e.g. on $(" <p>test</p>").
+  $args[0] =~ s{^\s+}{} if $trim_target_for{$method};
+
   push @{ $self->_actions }, [ $method, @args ];
 
   return $self;
 }
 
   push @{ $self->_actions }, [ $method, @args ];
 
   return $self;
 }
 
+sub action_if {
+  my ($self, $condition, @args) = @_;
+
+  return $condition ? $self->action(@args) : $self;
+}
+
 sub init__actions {
   return [];
 }
 
 sub init__actions {
   return [];
 }
 
+sub init__flash {
+  return {};
+}
+
+sub init__flash_detail {
+  return {};
+}
+
+sub init__error {
+  return '';
+}
+
+sub init__no_flash_clear {
+  return '';
+}
+
 sub to_json {
   my ($self) = @_;
 sub to_json {
   my ($self) = @_;
-  return SL::JSON::to_json({ eval_actions => $self->_actions });
+
+  return SL::JSON::to_json({ error          => $self->_error   }) if $self->_error;
+  return SL::JSON::to_json({ no_flash_clear => $self->_no_flash_clear, eval_actions => $self->_actions });
 }
 
 sub to_array {
 }
 
 sub to_array {
@@ -98,6 +209,81 @@ sub to_array {
   return $self->_actions;
 }
 
   return $self->_actions;
 }
 
+sub render {
+  my ($self, $controller) = @_;
+  $controller ||= $self->controller;
+  $self->reinit_widgets if $::request->presenter->need_reinit_widgets;
+  return $controller->render(\$self->to_json, { type => 'json' });
+}
+
+sub jstree {
+  my ($self) = @_;
+  $self->{_prefix} = 'jstree:';
+  return $self;
+}
+
+sub dialog {
+  my ($self) = @_;
+  $self->{_prefix} = 'dialog:';
+  return $self;
+}
+
+sub ckeditor {
+  my ($self) = @_;
+  $self->{_prefix} = 'ckeditor:';
+  return $self;
+}
+
+sub flash {
+  my ($self, $type, @messages) = @_;
+
+  my $message = join ' ', grep { $_ } @messages;
+
+  if (!$self->_flash->{$type}) {
+    $self->_flash->{$type} = [ 'flash', $type, $message ];
+    push @{ $self->_actions }, $self->_flash->{$type};
+  } else {
+    $self->_flash->{$type}->[-1] .= ' ' . $message;
+  }
+
+  return $self;
+}
+
+sub flash_detail {
+  my ($self, $type, @messages) = @_;
+
+  my $message = join '<br>', grep { $_ } @messages;
+
+  if (!$self->_flash_detail->{$type}) {
+    $self->_flash_detail->{$type} = [ 'flash_detail', $type, $message ];
+    push @{ $self->_actions }, $self->_flash_detail->{$type};
+  } else {
+    $self->_flash_detail->{$type}->[-1] .= ' ' . $message;
+  }
+
+  return $self;
+}
+
+sub no_flash_clear{
+  my ($self) = @_;
+  $self->_no_flash_clear('1');
+  return $self;
+}
+
+sub error {
+  my ($self, @messages) = @_;
+
+  $self->_error(join ' ', grep { $_ } ($self->_error, @messages));
+
+  return $self;
+}
+
+sub init_controller {
+  # fallback
+  require SL::Controller::Base;
+  SL::Controller::Base->new;
+}
+
 1;
 __END__
 
 1;
 __END__
 
@@ -115,26 +301,21 @@ with jQuery
 First some JavaScript code:
 
   // In the client generate an AJAX request whose 'success' handler
 First some JavaScript code:
 
   // In the client generate an AJAX request whose 'success' handler
-  // calls "eval_json_response(data)":
+  // calls "eval_json_result(data)":
   var data = {
   var data = {
-    action: "SomeController/the_action",
+    action: "SomeController/my_personal_action",
     id:     $('#some_input_field').val()
   };
     id:     $('#some_input_field').val()
   };
-  $.post("controller.pl", data, eval_json_response);
+  $.post("controller.pl", data, eval_json_result);
 
 
-Now some Perl code:
+Now some Controller (perl) code for my personal action:
 
 
-  # In the controller itself. First, make sure that the "client_js.js"
-  # is loaded. This must be done when the whole side is loaded, so
-  # it's not in the action called by the AJAX request shown above.
-  $::request->layout->use_javascript('client_js.js');
-
-  # Now in that action called via AJAX:
-  sub action_the_action {
+  # my personal action
+  sub action_my_personal_action {
     my ($self) = @_;
 
     # Create a new client-side JS object and do stuff with it!
     my ($self) = @_;
 
     # Create a new client-side JS object and do stuff with it!
-    my $js = SL::ClientJS->new;
+    my $js = SL::ClientJS->new(controller => $self);
 
     # Show some element on the page:
     $js->show('#usually_hidden');
 
     # Show some element on the page:
     $js->show('#usually_hidden');
@@ -147,8 +328,20 @@ Now some Perl code:
     my $html = $self->render('SomeController/the_action', { output => 0 });
     $js->html('#id_with_new_content', $html);
 
     my $html = $self->render('SomeController/the_action', { output => 0 });
     $js->html('#id_with_new_content', $html);
 
+    # Operations on a jstree: rename a node and select it
+    my $text_block = SL::DB::RequirementSpecTextBlock->new(id => 4711)->load;
+    $js->jstree->rename_node('#tb-' . $text_block->id, $text_block->title)
+       ->jstree->select_node('#tb-' . $text_block->id);
+
+    # Close a popup opened by kivi.popup_dialog():
+    $js->dialog->close('#jqueryui_popup_dialog');
+
     # Finally render the JSON response:
     $self->render($js);
     # Finally render the JSON response:
     $self->render($js);
+
+    # Rendering can also be chained, e.g.
+    $js->html('#selector', $html)
+       ->render;
   }
 
 =head1 OVERVIEW
   }
 
 =head1 OVERVIEW
@@ -157,7 +350,7 @@ This module enables the generation of jQuery-using JavaScript code on
 the server side. That code is then evaluated in a safe way on the
 client side.
 
 the server side. That code is then evaluated in a safe way on the
 client side.
 
-The workflow is usally that the client creates an AJAX request, the
+The workflow is usually that the client creates an AJAX request, the
 server creates some actions and sends them back, and the client then
 implements each of these actions.
 
 server creates some actions and sends them back, and the client then
 implements each of these actions.
 
@@ -167,7 +360,7 @@ There are three things that need to be done for this to work:
 
 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
 
 
 =item 1. The "client_js.js" has to be loaded before the AJAX request is started.
 
-=item 2. The client code needs to call C<eval_json_response()> with the result returned from the server.
+=item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
 
 =item 3. The server must use this module.
 
 
 =item 3. The server must use this module.
 
@@ -198,10 +391,158 @@ are the function parameters.
 Returns the actions gathered so far as a JSON string ready to be sent
 to the client.
 
 Returns the actions gathered so far as a JSON string ready to be sent
 to the client.
 
+=item C<render [$controller]>
+
+Renders C<$self> via the controller. Useful for chaining. Equivalent
+to the following:
+
+  $controller->render(\$self->to_json, { type => 'json' });
+
+The controller instance to use can be set during object creation (see
+synopsis) or as an argument to C<render>.
+
+=item C<dialog>
+
+Tells C<$self> that the next action is to be called on a jQuery UI
+dialog instance, e.g. one opened by C<kivi.popup_dialog()>. For
+example:
+
+  $js->dialog->close('#jqueryui_popup_dialog');
+
+=item C<jstree>
+
+Tells C<$self> that the next action is to be called on a jstree
+instance. For example:
+
+  $js->jstree->rename_node('tb-' . $text_block->id, $text_block->title);
+
 =back
 
 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
 
 =back
 
 =head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
 
+=head2 GENERIC FUNCTION
+
+All of the following functions can be invoked in two ways: either by
+calling the function name directly on C<$self> or by calling
+L</action> with the function name as the first parameter. Therefore
+the following two calls are identical:
+
+  $js->insertAfter($html, '#some-id');
+  $js->action('insertAfter', $html, '#some-id');
+
+The second form, calling L</action>, is more to type but can be useful
+in situations in which you have to call one of two functions depending
+on context. For example, when you want to insert new code in a
+list. If the list is empty you might have to use C<appendTo>, if it
+isn't you might have to use C<insertAfter>. Example:
+
+  my $html = $self->render(...);
+  $js->action($list_is_empty ? 'appendTo' : 'insertAfter', $html, '#text-block-' . ($list_is_empty ? 'list' : $self->text_block->id));
+
+Instead of:
+
+  my $html = $self->render(...);
+  if ($list_is_empty) {
+    $js->appendTo($html, '#text-block-list');
+  } else {
+    $js->insertAfter($html, '#text-block-' . $self->text_block->id);
+  }
+
+The first variation is obviously better suited for chaining.
+
+=over 4
+
+=item C<action $method, @args>
+
+Call the function with the name C<$method> on C<$self> with arguments
+C<@args>. Returns the return value of the actual function
+called. Useful for chaining (see above).
+
+=item C<action_if $condition, $method, @args>
+
+Call the function with the name C<$method> on C<$self> with arguments
+C<@args> if C<$condition> is trueish. Does nothing otherwise.
+
+Returns the return value of the actual function called if
+C<$condition> is trueish and C<$self> otherwise. Useful for chaining
+(see above).
+
+This function is equivalent to the following:
+
+  if ($condition) {
+    $obj->$method(@args);
+  }
+
+But it is easier to integrate into a method call chain, e.g.:
+
+  $js->html('#content', $html)
+     ->action_if($item->is_flagged, 'toggleClass', '#marker', 'flagged')
+     ->render($self);
+
+=back
+
+=head2 ADDITIONAL FUNCTIONS
+
+=over 4
+
+=item C<flash $type, $message>
+
+Display a C<$message> in the flash of type C<$type>. Multiple calls of
+C<flash> on the same C<$self> will be merged by type.
+
+On the client side the flashes of all types will be cleared after each
+successful ClientJS call that did not end with C<$js-E<gt>error(...)>.
+This clearing can be switched of by the function C<no_flash_clear>
+
+=item C<flash_detail $type, $message>
+
+Display a detailed message C<$message> in the flash of type C<$type>. Multiple calls of
+C<flash_detail> on the same C<$self> will be merged by type.
+So the flash message can be hold short and the visibility of details can toggled by the user.
+
+=item C<no_flash_clear>
+
+No automatic clearing of flash after successful ClientJS call
+
+=item C<error $message>
+
+Causes L<to_json> (and therefore L<render>) to output a JSON object
+that only contains an C<error> field set to this C<$message>. The
+client will then show the message in the 'error' flash.
+
+The messages of multiple calls of C<error> on the same C<$self> will
+be merged.
+
+=item C<redirect_to $url>
+
+Redirects the browser window to the new URL by setting the JavaScript
+property C<window.location.href>. Note that
+L<SL::Controller::Base/redirect_to> is AJAX aware and uses this
+function if the current request is an AJAX request as determined by
+L<SL::Request/is_ajax>.
+
+=back
+
+=head2 KIVITENDO FUNCTIONS
+
+The following functions from the C<kivi> namespace are supported:
+
+=over 4
+
+=item Displaying stuff
+
+C<flash> (don't call directly, use L</flash> instead)
+
+=item Running functions
+
+C<run>, C<run_once_for>
+
+=item Widgets
+
+C<reinit_widgets>
+
+=back
+
 =head2 JQUERY FUNCTIONS
 
 The following jQuery functions are supported:
 =head2 JQUERY FUNCTIONS
 
 The following jQuery functions are supported:
@@ -236,33 +577,114 @@ C<replaceAll>, C<replaceWith>
 
 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
 
 
 C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>
 
+=item Class attributes
+
+C<addClass>, C<removeClass>, C<toggleClass>
+
 =item Data storage
 
 C<data>, C<removeData>
 
 =item Data storage
 
 C<data>, C<removeData>
 
+=item Form Events
+
+C<focus>
+
+=item Generic Event Handlers
+
+C<on>, C<off>, C<one>
+
+These attach/detach event listeners to specific selectors. The first
+argument is the selector, the second the name of the events and the
+third argument is the name of the handler function. That function must
+already exist when the handler is added.
+
+=back
+
+=head2 JQUERY POPUP DIALOG PLUGIN
+
+Supported functions of the C<popup dialog> plugin to jQuery. They are
+invoked by first calling C<dialog> in the ClientJS instance and then
+the function itself:
+
+  $js->dialog->close(...);
+
+=over 4
+
+=item Closing and removing the popup
+
+C<close>
+
+=back
+
+=head2 AJAXFORM JQUERY PLUGIN
+
+The following functions of the C<ajaxForm> plugin to jQuery are
+supported:
+
+=over 4
+
+=item All functions by the generic accessor function:
+
+C<ajaxForm>
+
+=back
+
+=head2 JSTREE JQUERY PLUGIN
+
+Supported functions of the C<jstree> plugin to jQuery. They are
+invoked by first calling C<jstree> in the ClientJS instance and then
+the function itself:
+
+  $js->jstree->open_node(...);
+
+=over 4
+
+=item Operations on the whole tree
+
+C<lock>, C<unlock>
+
+=item Opening and closing nodes
+
+C<open_node>, C<close_node>, C<toggle_node>, C<open_all>,
+C<close_all>, C<save_opened>, C<reopen>
+
+=item Modifying nodes
+
+C<rename_node>, C<delete_node>, C<move_node>
+
+=item Selecting nodes (from the 'ui' jstree plugin)
+
+C<select_node>, C<deselect_node>, C<deselect_all>
+
 =back
 
 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
 
 =back
 
 =head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS
 
-In order not having to maintain two files (this one and
+In order to not have to maintain two files (this one and
 C<js/client_js.js>) there's a script that can parse this file's
 C<js/client_js.js>) there's a script that can parse this file's
-C<%supported_methods> definition and convert it into the appropriate
-code ready for manual insertion into C<js/client_js.js>. The steps
-are:
+C<%supported_methods> definition and generate the file
+C<js/client_js.js> accordingly. The steps are:
 
 =over 2
 
 =item 1. Add lines in this file to the C<%supported_methods> hash. The
 key is the function name and the value is the number of expected
 
 =over 2
 
 =item 1. Add lines in this file to the C<%supported_methods> hash. The
 key is the function name and the value is the number of expected
-parameters.
+parameters. The value can be negative to indicate that the function
+takes at least the absolute of this value as parameters and optionally
+more. In such a case the C<E<lt>ARGSE<gt>> format expands to an actual
+array (and the individual elements if the value is positive>.
 
 
-=item 2. Run C<scripts/generate_client_js_actions.pl>
+=item 2. Run C<scripts/generate_client_js_actions.pl>. It will
+generate C<js/client_js.js> automatically.
 
 
-=item 3. Edit C<js/client_js.js> and replace the type casing code with
-the output generated in step 2.
+=item 3. Reload the files in your browser (cleaning its cache can also
+help).
 
 =back
 
 
 =back
 
+The template file used for generated C<js/client_js.js> is
+C<scripts/generate_client_js_actions.tpl>.
+
 =head1 BUGS
 
 Nothing here yet.
 =head1 BUGS
 
 Nothing here yet.