X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FClientJS.pm;h=49bde8092cbfbeb1114c5969ad9f93786a51a04f;hb=bfa674c9d4edc551afa6e27aac6f684a7b8cb656;hp=6f9ce5adbcd3311126bcd4153fed865dd2469f9c;hpb=2473bd32292484336637336bc6968a0f329d9ad4;p=kivitendo-erp.git diff --git a/SL/ClientJS.pm b/SL/ClientJS.pm index 6f9ce5adb..49bde8092 100644 --- a/SL/ClientJS.pm +++ b/SL/ClientJS.pm @@ -9,7 +9,8 @@ use SL::JSON (); 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 = ( @@ -55,10 +56,32 @@ my %supported_methods = ( removeProp => 2, val => 2, + # Class attribute + addClass => 2, + removeClass => 2, + toggleClass => 2, + # Data storage data => 3, removeData => 2, + # Form Events + focus => 1, # kivi.set_focus() + + # Generic Event Handling ## pattern: $().(, kivi.get_function_by_name()) + on => 3, + off => 3, + one => 3, + + # ## jQuery UI dialog plugin ## pattern: $().dialog('') + + # Opening and closing a popup + 'dialog:open' => 1, # kivi.popup_dialog() + 'dialog:close' => 1, + + # ## jQuery Form plugin ## + 'ajaxForm' => 1, # $().ajaxForm({ success: eval_json_result }) + # ## jstree plugin ## pattern: $.jstree._reference($()).() # Operations on the whole tree @@ -75,6 +98,7 @@ my %supported_methods = ( 'jstree:reopen' => 1, # Modifying nodes + 'jstree:create_node' => 4, 'jstree:rename_node' => 3, 'jstree:delete_node' => 2, 'jstree:move_node' => 5, @@ -83,8 +107,28 @@ my %supported_methods = ( 'jstree:select_node' => 2, # $.jstree._reference($()).(, true) 'jstree:deselect_node' => 2, 'jstree:deselect_all' => 1, + + # ## ckeditor stuff ## + 'focus_ckeditor' => 1, # kivi.focus_ckeditor_when_ready() + + # ## other stuff ## + redirect_to => 1, # window.location.href = + save_file => 4, # kivi.save_file(, ) + + flash => 2, # kivi.display_flash(, ) + flash_detail => 2, # kivi.display_flash_detail(, ) + clear_flash => 2, # kivi.clear_flash(, ) + reinit_widgets => 0, # kivi.reinit_widgets() + run => -1, # kivi.run(, ) + run_once_for => 3, # kivi.run_once_for(, ) + + scroll_into_view => 1, # $()[0].scrollIntoView() + + set_cursor_position => 2, # kivi.set_cursor_position(, ) ); +my %trim_target_for = map { ($_ => 1) } qw(insertAfter insertBefore appendTo prependTo); + sub AUTOLOAD { our $AUTOLOAD; @@ -93,31 +137,71 @@ sub AUTOLOAD { 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}; - 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: $("

test

").appendTo('#some-id'). jQuery croaks + # on leading whitespaces, e.g. on $("

test

"). + $args[0] =~ s{^\s+}{} if $trim_target_for{$method}; + 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__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({ 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 { @@ -127,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' }); } @@ -136,6 +222,68 @@ 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) = @_; + + 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 '
', 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__ @@ -153,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'); @@ -190,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 @@ -204,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. @@ -214,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 with the result returned from the server. +=item 2. The client code needs to call C with the result returned from the server. =item 3. The server must use this module. @@ -245,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 +=item C 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. + +=item C + +Tells C<$self> that the next action is to be called on a jQuery UI +dialog instance, e.g. one opened by C. For +example: + + $js->dialog->close('#jqueryui_popup_dialog'); + =item C Tells C<$self> that the next action is to be called on a jstree @@ -263,6 +420,129 @@ instance. For example: =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 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, 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, if it +isn't you might have to use C. 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 + +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 + +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 + +Display a C<$message> in the flash of type C<$type>. Multiple calls of +C 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-Eerror(...)>. +This clearing can be switched of by the function C + +=item C + +Display a detailed message C<$message> in the flash of type C<$type>. Multiple calls of +C 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 automatic clearing of flash after successful ClientJS call + +=item C + +Causes L (and therefore L) to output a JSON object +that only contains an C field set to this C<$message>. The +client will then show the message in the 'error' flash. + +The messages of multiple calls of C on the same C<$self> will +be merged. + +=item C + +Redirects the browser window to the new URL by setting the JavaScript +property C. Note that +L is AJAX aware and uses this +function if the current request is an AJAX request as determined by +L. + +=back + +=head2 KIVITENDO FUNCTIONS + +The following functions from the C namespace are supported: + +=over 4 + +=item Displaying stuff + +C (don't call directly, use L instead) + +=item Running functions + +C, C + +=item Widgets + +C + +=back + =head2 JQUERY FUNCTIONS The following jQuery functions are supported: @@ -297,19 +577,68 @@ C, C C, C, C, C, C +=item Class attributes + +C, C, C + =item Data storage C, C +=item Form Events + +C + +=item Generic Event Handlers + +C, C, C + +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 plugin to jQuery. They are +invoked by first calling C in the ClientJS instance and then +the function itself: -The following functions of the C plugin to jQuery are + $js->dialog->close(...); + +=over 4 + +=item Closing and removing the popup + +C + +=back + +=head2 AJAXFORM JQUERY PLUGIN + +The following functions of the C plugin to jQuery are supported: =over 4 +=item All functions by the generic accessor function: + +C + +=back + +=head2 JSTREE JQUERY PLUGIN + +Supported functions of the C plugin to jQuery. They are +invoked by first calling C in the ClientJS instance and then +the function itself: + + $js->jstree->open_node(...); + +=over 4 + =item Operations on the whole tree C, C @@ -331,7 +660,7 @@ C, C, C =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) there's a script that can parse this file's C<%supported_methods> definition and generate the file C accordingly. The steps are: @@ -340,7 +669,10 @@ C 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 CARGSE> format expands to an actual +array (and the individual elements if the value is positive>. =item 2. Run C. It will generate C automatically.