"alle" E-Mail-Adressen per Anhaken als Empfänger hinzufügen können
[kivitendo-erp.git] / SL / ClientJS.pm
index 1218e4c..49bde80 100644 (file)
@@ -9,13 +9,11 @@ use SL::JSON ();
 
 use Rose::Object::MakeMethods::Generic
 (
-  'scalar --get_set_init' => [ qw(_actions _flash _error) ],
+  scalar                  => [ qw() ],
+  'scalar --get_set_init' => [ qw(controller _actions _flash _flash_detail _no_flash_clear _error) ],
 );
 
 my %supported_methods = (
-  # ## Non-jQuery methods ##
-  flash        => 2,            # display_flash(<TARGET>, <ARGS>)
-
   # ## jQuery basics ##
 
   # Basic effects
@@ -68,7 +66,21 @@ my %supported_methods = (
   removeData   => 2,
 
   # Form Events
-  focus        => 1,
+  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>)
 
@@ -95,8 +107,28 @@ my %supported_methods = (
   '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;
 
@@ -114,15 +146,26 @@ sub action {
   $method      =  (delete($self->{_prefix}) || '') . $method;
   my $num_args =  $supported_methods{$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 > 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;
+  }
 
-  if ($num_args) {
-    # Force flattening from SL::Presenter::EscapedText: "" . $...
-    $args[0] =  "" . $args[0];
-    $args[0] =~ s/^\s+//;
+  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;
@@ -142,15 +185,23 @@ sub init__flash {
   return {};
 }
 
+sub init__flash_detail {
+  return {};
+}
+
 sub init__error {
   return '';
 }
 
+sub init__no_flash_clear {
+  return '';
+}
+
 sub to_json {
   my ($self) = @_;
 
-  return SL::JSON::to_json({ error        => $self->_error   }) if $self->_error;
-  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 {
@@ -160,6 +211,8 @@ sub to_array {
 
 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' });
 }
 
@@ -169,6 +222,18 @@ sub 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) = @_;
 
@@ -184,6 +249,27 @@ sub flash {
   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) = @_;
 
@@ -192,6 +278,12 @@ sub error {
   return $self;
 }
 
+sub init_controller {
+  # fallback
+  require SL::Controller::Base;
+  SL::Controller::Base->new;
+}
+
 1;
 __END__
 
@@ -209,26 +301,21 @@ with jQuery
 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 = {
-    action: "SomeController/the_action",
+    action: "SomeController/my_personal_action",
     id:     $('#some_input_field').val()
   };
-  $.post("controller.pl", data, eval_json_response);
-
-Now some Perl code:
+  $.post("controller.pl", data, eval_json_result);
 
-  # 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 some Controller (perl) code for my personal action:
 
-  # 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 $js = SL::ClientJS->new;
+    my $js = SL::ClientJS->new(controller => $self);
 
     # Show some element on the page:
     $js->show('#usually_hidden');
@@ -246,12 +333,15 @@ Now some Perl code:
     $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);
 
     # Rendering can also be chained, e.g.
     $js->html('#selector', $html)
-       ->render($self);
+       ->render;
   }
 
 =head1 OVERVIEW
@@ -260,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 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.
 
@@ -270,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 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.
 
@@ -301,13 +391,24 @@ are the function parameters.
 Returns the actions gathered so far as a JSON string ready to be sent
 to the client.
 
-=item C<render $controller>
+=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
@@ -389,8 +490,19 @@ But it is easier to integrate into a method call chain, e.g.:
 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 flash of this type will be cleared before the
-message is shown.
+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>
 
@@ -401,6 +513,34 @@ 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
@@ -437,6 +577,10 @@ C<replaceAll>, C<replaceWith>
 
 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>
@@ -445,15 +589,56 @@ C<data>, C<removeData>
 
 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 JSTREE JQUERY PLUGIN
+=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
 
-The following functions of the C<jstree> plugin to jQuery are
+=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>
@@ -475,7 +660,7 @@ C<select_node>, C<deselect_node>, C<deselect_all>
 
 =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<%supported_methods> definition and generate the file
 C<js/client_js.js> accordingly. The steps are:
@@ -484,7 +669,10 @@ C<js/client_js.js> accordingly. The steps are:
 
 =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>. It will
 generate C<js/client_js.js> automatically.