Projekt

Allgemein

Profil

Herunterladen (15,4 KB) Statistiken
| Zweig: | Markierung: | Revision:
7af2b128 Moritz Bunkus
package SL::ClientJS;

use strict;

use parent qw(Rose::Object);

use Carp;
use SL::JSON ();

use Rose::Object::MakeMethods::Generic
(
1e6fdf48 Moritz Bunkus
scalar => [ qw(controller) ],
b9740e8a Moritz Bunkus
'scalar --get_set_init' => [ qw(_actions _flash _error) ],
7af2b128 Moritz Bunkus
);

my %supported_methods = (
6ca21978 Moritz Bunkus
# ## jQuery basics ##

7af2b128 Moritz Bunkus
# Basic effects
hide => 1,
show => 1,
toggle => 1,

# DOM insertion, around
unwrap => 1,
wrap => 2,
wrapAll => 2,
wrapInner => 2,

# DOM insertion, inside
append => 2,
appendTo => 2,
html => 2,
prepend => 2,
prependTo => 2,
text => 2,

# DOM insertion, outside
after => 2,
before => 2,
insertAfter => 2,
insertBefore => 2,

# DOM removal
empty => 1,
remove => 1,

# DOM replacement
replaceAll => 2,
replaceWith => 2,

# General attributes
attr => 3,
prop => 3,
removeAttr => 2,
removeProp => 2,
val => 2,

5551a36b Moritz Bunkus
# Class attribute
addClass => 2,
removeClass => 2,
toggleClass => 2,

7af2b128 Moritz Bunkus
# Data storage
data => 3,
removeData => 2,
6ca21978 Moritz Bunkus
128f3100 Moritz Bunkus
# Form Events
551d4b78 Moritz Bunkus
focus => 1, # kivi.set_focus(<TARGET>)
128f3100 Moritz Bunkus
824f9ddf Moritz Bunkus
# Generic Event Handling ## pattern: $(<TARGET>).<FUNCTION>(<ARG1>, kivi.get_function_by_name(<ARG2>))
on => 3,
off => 3,
one => 3,

6dbc83af Moritz Bunkus
# ## jQuery UI dialog plugin ## pattern: $(<TARGET>).dialog('<FUNCTION>')
39bbd478 Moritz Bunkus
# Closing and removing the popup
6dbc83af Moritz Bunkus
'dialog:close' => 1,
39bbd478 Moritz Bunkus
c00f1e29 Moritz Bunkus
# ## jQuery Form plugin ##
'ajaxForm' => 1, # pattern: $(<TARGET>).ajaxForm({ success: eval_json_result })

6ca21978 Moritz Bunkus
# ## 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
001c83b1 Moritz Bunkus
'jstree:create_node' => 4,
6ca21978 Moritz Bunkus
'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,
2738c03e Moritz Bunkus
551d4b78 Moritz Bunkus
# ## ckeditor stuff ##
'focus_ckeditor' => 1, # kivi.focus_ckeditor_when_ready(<TARGET>)

2738c03e Moritz Bunkus
# ## other stuff ##
redirect_to => 1, # window.location.href = <TARGET>
3ac83c61 Moritz Bunkus
a82f3bef Moritz Bunkus
flash => 2, # kivi.display_flash(<TARGET>, <ARGS>)
3ac83c61 Moritz Bunkus
reinit_widgets => 0, # kivi.reinit_widgets()
a82f3bef Moritz Bunkus
run => -1, # kivi.run(<TARGET>, <ARGS>)
run_once_for => 3, # kivi.run_once_for(<TARGET>, <ARGS>)
7af2b128 Moritz Bunkus
);

sub AUTOLOAD {
our $AUTOLOAD;

my ($self, @args) = @_;

my $method = $AUTOLOAD;
$method =~ s/.*:://;
return if $method eq 'DESTROY';
128f3100 Moritz Bunkus
return $self->action($method, @args);
}

sub action {
my ($self, $method, @args) = @_;
7af2b128 Moritz Bunkus
6ca21978 Moritz Bunkus
$method = (delete($self->{_prefix}) || '') . $method;
7af2b128 Moritz Bunkus
my $num_args = $supported_methods{$method};

croak "Unsupported jQuery action: $method" unless defined $num_args;
a82f3bef Moritz Bunkus
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;
}
7af2b128 Moritz Bunkus
133e3be2 Moritz Bunkus
foreach my $idx (0..$num_args - 1) {
# Force flattening from SL::Presenter::EscapedText and trim leading whitespace for scalars
$args[$idx] = "" . $args[$idx] if ref($args[$idx]) eq 'SL::Presenter::EscapedText';
$args[$idx] =~ s/^\s+// if !ref($args[$idx]);
7af2b128 Moritz Bunkus
}

push @{ $self->_actions }, [ $method, @args ];

return $self;
}

5551a36b Moritz Bunkus
sub action_if {
my ($self, $condition, @args) = @_;

return $condition ? $self->action(@args) : $self;
}

7af2b128 Moritz Bunkus
sub init__actions {
return [];
}

b9740e8a Moritz Bunkus
sub init__flash {
return {};
}

sub init__error {
return '';
}

7af2b128 Moritz Bunkus
sub to_json {
my ($self) = @_;
b9740e8a Moritz Bunkus
return SL::JSON::to_json({ error => $self->_error }) if $self->_error;
7af2b128 Moritz Bunkus
return SL::JSON::to_json({ eval_actions => $self->_actions });
}

sub to_array {
my ($self) = @_;
return $self->_actions;
}

4d9d7d51 Moritz Bunkus
sub render {
my ($self, $controller) = @_;
1e6fdf48 Moritz Bunkus
$controller ||= $self->controller;
3ac83c61 Moritz Bunkus
$self->reinit_widgets if $::request->presenter->need_reinit_widgets;
4d9d7d51 Moritz Bunkus
return $controller->render(\$self->to_json, { type => 'json' });
}

6ca21978 Moritz Bunkus
sub jstree {
my ($self) = @_;
$self->{_prefix} = 'jstree:';
return $self;
}

6dbc83af Moritz Bunkus
sub dialog {
my ($self) = @_;
$self->{_prefix} = 'dialog:';
return $self;
}

551d4b78 Moritz Bunkus
sub ckeditor {
my ($self) = @_;
$self->{_prefix} = 'ckeditor:';
return $self;
}

b9740e8a Moritz Bunkus
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 error {
my ($self, @messages) = @_;

$self->_error(join ' ', grep { $_ } ($self->_error, @messages));

return $self;
}

7af2b128 Moritz Bunkus
1;
__END__

=pod

=encoding utf8

=head1 NAME

SL::ClientJS - Easy programmatic client-side JavaScript generation
with jQuery

=head1 SYNOPSIS

First some JavaScript code:

// In the client generate an AJAX request whose 'success' handler
17f39e02 Moritz Bunkus
// calls "eval_json_result(data)":
7af2b128 Moritz Bunkus
var data = {
action: "SomeController/the_action",
id: $('#some_input_field').val()
};
17f39e02 Moritz Bunkus
$.post("controller.pl", data, eval_json_result);
7af2b128 Moritz Bunkus
Now some Perl code:

# 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 ($self) = @_;

# Create a new client-side JS object and do stuff with it!
1e6fdf48 Moritz Bunkus
my $js = SL::ClientJS->new(controller => $self);
7af2b128 Moritz Bunkus
# Show some element on the page:
$js->show('#usually_hidden');

# Set to hidden inputs. Yes, calls can be chained!
$js->val('#hidden_id', $self->new_id)
->val('#other_type', 'Unicorn');

# Replace some HTML code:
my $html = $self->render('SomeController/the_action', { output => 0 });
$js->html('#id_with_new_content', $html);

6ca21978 Moritz Bunkus
# 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);

6dbc83af Moritz Bunkus
# Close a popup opened by kivi.popup_dialog():
$js->dialog->close('#jqueryui_popup_dialog');

7af2b128 Moritz Bunkus
# Finally render the JSON response:
$self->render($js);
6ca21978 Moritz Bunkus
# Rendering can also be chained, e.g.
$js->html('#selector', $html)
1e6fdf48 Moritz Bunkus
->render;
7af2b128 Moritz Bunkus
}

=head1 OVERVIEW

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
server creates some actions and sends them back, and the client then
implements each of these actions.

There are three things that need to be done for this to work:

=over 2

=item 1. The "client_js.js" has to be loaded before the AJAX request is started.

17f39e02 Moritz Bunkus
=item 2. The client code needs to call C<kivi.eval_json_result()> with the result returned from the server.
7af2b128 Moritz Bunkus
=item 3. The server must use this module.

=back

The functions called on the client side are mostly jQuery
functions. Other functionality may be added later.

Note that L<SL::Controller/render> is aware of this module which saves
you some boilerplate. The following two calls are equivalent:

$controller->render($client_js);
$controller->render(\$client_js->to_json, { type => 'json' });

=head1 FUNCTIONS NOT PASSED TO THE CLIENT SIDE

=over 4

=item C<to_array>

Returns the actions gathered so far as an array reference. Each
element is an array reference containing at least two items: the
function's name and what it is called on. Additional array elements
are the function parameters.

=item C<to_json>

Returns the actions gathered so far as a JSON string ready to be sent
to the client.

1e6fdf48 Moritz Bunkus
=item C<render [$controller]>
4d9d7d51 Moritz Bunkus
Renders C<$self> via the controller. Useful for chaining. Equivalent
to the following:

$controller->render(\$self->to_json, { type => 'json' });

1e6fdf48 Moritz Bunkus
The controller instance to use can be set during object creation (see
synopsis) or as an argument to C<render>.

6dbc83af Moritz Bunkus
=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');

6ca21978 Moritz Bunkus
=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);

7af2b128 Moritz Bunkus
=back

=head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE

128f3100 Moritz Bunkus
=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.

5551a36b Moritz Bunkus
=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
b9740e8a Moritz Bunkus
=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.

ab92bc46 Moritz Bunkus
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(...)>.
b9740e8a Moritz Bunkus
=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.

2738c03e Moritz Bunkus
=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>.

b9740e8a Moritz Bunkus
=back

a82f3bef Moritz Bunkus
=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

7af2b128 Moritz Bunkus
=head2 JQUERY FUNCTIONS

The following jQuery functions are supported:

=over 4

=item Basic effects

C<hide>, C<show>, C<toggle>

=item DOM insertion, around

C<unwrap>, C<wrap>, C<wrapAll>, C<wrapInner>

=item DOM insertion, inside

C<append>, C<appendTo>, C<html>, C<prepend>, C<prependTo>, C<text>

=item DOM insertion, outside

C<after>, C<before>, C<insertAfter>, C<insertBefore>

=item DOM removal

C<empty>, C<remove>

=item DOM replacement

C<replaceAll>, C<replaceWith>

=item General attributes

C<attr>, C<prop>, C<removeAttr>, C<removeProp>, C<val>

a82f3bef Moritz Bunkus
=item Class attributes

C<addClass>, C<removeClass>, C<toggleClass>

7af2b128 Moritz Bunkus
=item Data storage

C<data>, C<removeData>

128f3100 Moritz Bunkus
=item Form Events

C<focus>

824f9ddf Moritz Bunkus
=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.

7af2b128 Moritz Bunkus
=back

a82f3bef Moritz Bunkus
=head2 JQUERY POPUP DIALOG PLUGIN
6ca21978 Moritz Bunkus
a82f3bef Moritz Bunkus
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
6ca21978 Moritz Bunkus
supported:

=over 4

a82f3bef Moritz Bunkus
=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

6ca21978 Moritz Bunkus
=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

7af2b128 Moritz Bunkus
=head1 ADDING SUPPORT FOR ADDITIONAL FUNCTIONS

In order not having to maintain two files (this one and
C<js/client_js.js>) there's a script that can parse this file's
6ca21978 Moritz Bunkus
C<%supported_methods> definition and generate the file
C<js/client_js.js> accordingly. The steps are:
7af2b128 Moritz Bunkus
=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
a82f3bef Moritz Bunkus
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>.
7af2b128 Moritz Bunkus
6ca21978 Moritz Bunkus
=item 2. Run C<scripts/generate_client_js_actions.pl>. It will
generate C<js/client_js.js> automatically.
7af2b128 Moritz Bunkus
6ca21978 Moritz Bunkus
=item 3. Reload the files in your browser (cleaning its cache can also
help).
7af2b128 Moritz Bunkus
=back

6ca21978 Moritz Bunkus
The template file used for generated C<js/client_js.js> is
C<scripts/generate_client_js_actions.tpl>.

7af2b128 Moritz Bunkus
=head1 BUGS

Nothing here yet.

=head1 AUTHOR

Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>

=cut