--- /dev/null
+package SL::ClientJS;
+
+use strict;
+
+use parent qw(Rose::Object);
+
+use Carp;
+use SL::JSON ();
+
+use Rose::Object::MakeMethods::Generic
+(
+ 'scalar --get_set_init' => [ qw(_actions) ],
+);
+
+my %supported_methods = (
+ # 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,
+
+ # Data storage
+ data => 3,
+ removeData => 2,
+);
+
+sub AUTOLOAD {
+ our $AUTOLOAD;
+
+ my ($self, @args) = @_;
+
+ my $method = $AUTOLOAD;
+ $method =~ s/.*:://;
+ return if $method eq 'DESTROY';
+
+ 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;
+
+ if ($num_args) {
+ # Force flattening from SL::Presenter::EscapedText: "" . $...
+ $args[0] = "" . $args[0];
+ $args[0] =~ s/^\s+//;
+ }
+
+ push @{ $self->_actions }, [ $method, @args ];
+
+ return $self;
+}
+
+sub init__actions {
+ return [];
+}
+
+sub to_json {
+ my ($self) = @_;
+ return SL::JSON::to_json({ eval_actions => $self->_actions });
+}
+
+sub to_array {
+ my ($self) = @_;
+ return $self->_actions;
+}
+
+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
+ // calls "eval_json_response(data)":
+ var data = {
+ action: "SomeController/the_action",
+ id: $('#some_input_field').val()
+ };
+ $.post("controller.pl", data, eval_json_response);
+
+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!
+ my $js = SL::ClientJS->new;
+
+ # 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);
+
+ # Finally render the JSON response:
+ $self->render($js);
+ }
+
+=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.
+
+=item 2. The client code needs to call C<eval_json_response()> with the result returned from the server.
+
+=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.
+
+=back
+
+=head1 FUNCTIONS EVALUATED ON THE CLIENT SIDE
+
+=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>
+
+=item Data storage
+
+C<data>, C<removeData>
+
+=back
+
+=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
+C<%supported_methods> definition and convert it into the appropriate
+code ready for manual insertion into C<js/client_js.js>. 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
+parameters.
+
+=item 2. Run C<scripts/generate_client_js_actions.pl>
+
+=item 3. Edit C<js/client_js.js> and replace the type casing code with
+the output generated in step 2.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
--- /dev/null
+// NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE NOTE:
+
+// Generate the dispatching lines in this script by running
+// "scripts/generate_client_js_actions.pl". See the documentation for
+// SL/ClientJS.pm for instructions.
+
+function eval_json_result(data) {
+ if (!data)
+ return;
+
+ if ((data.js || '') != '')
+ eval(data.js);
+
+ if (data.eval_actions)
+ $(data.eval_actions).each(function(idx, action) {
+ // console.log("ACTION " + action[0] + " ON " + action[1]);
+
+ // Basic effects
+ if (action[0] == 'hide') $(action[1]).hide();
+ else if (action[0] == 'show') $(action[1]).show();
+ else if (action[0] == 'toggle') $(action[1]).toggle();
+
+ // DOM insertion, around
+ else if (action[0] == 'unwrap') $(action[1]).unwrap();
+ else if (action[0] == 'wrap') $(action[1]).wrap(action[2]);
+ else if (action[0] == 'wrapAll') $(action[1]).wrapAll(action[2]);
+ else if (action[0] == 'wrapInner') $(action[1]).wrapInner(action[2]);
+
+ // DOM insertion, inside
+ else if (action[0] == 'append') $(action[1]).append(action[2]);
+ else if (action[0] == 'appendTo') $(action[1]).appendTo(action[2]);
+ else if (action[0] == 'html') $(action[1]).html(action[2]);
+ else if (action[0] == 'prepend') $(action[1]).prepend(action[2]);
+ else if (action[0] == 'prependTo') $(action[1]).prependTo(action[2]);
+ else if (action[0] == 'text') $(action[1]).text(action[2]);
+
+ // DOM insertion, outside
+ else if (action[0] == 'after') $(action[1]).after(action[2]);
+ else if (action[0] == 'before') $(action[1]).before(action[2]);
+ else if (action[0] == 'insertAfter') $(action[1]).insertAfter(action[2]);
+ else if (action[0] == 'insertBefore') $(action[1]).insertBefore(action[2]);
+
+ // DOM removal
+ else if (action[0] == 'empty') $(action[1]).empty();
+ else if (action[0] == 'remove') $(action[1]).remove();
+
+ // DOM replacement
+ else if (action[0] == 'replaceAll') $(action[1]).replaceAll(action[2]);
+ else if (action[0] == 'replaceWith') $(action[1]).replaceWith(action[2]);
+
+ // General attributes
+ else if (action[0] == 'attr') $(action[1]).attr(action[2], action[3]);
+ else if (action[0] == 'prop') $(action[1]).prop(action[2], action[3]);
+ else if (action[0] == 'removeAttr') $(action[1]).removeAttr(action[2]);
+ else if (action[0] == 'removeProp') $(action[1]).removeProp(action[2]);
+ else if (action[0] == 'val') $(action[1]).val(action[2]);
+
+ // Data storage
+ else if (action[0] == 'data') $(action[1]).data(action[2], action[3]);
+ else if (action[0] == 'removeData') $(action[1]).removeData(action[2]);
+
+ else console.log("Unknown action: " + action[0]);
+ });
+
+ console.log("current_content_type " + $('#current_content_type').val() + ' ID ' + $('#current_content_id').val());
+}
--- /dev/null
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use File::Slurp;
+use List::Util qw(first max);
+
+my $file_name = (first { -f } qw(SL/ClientJS.pm ../SL/ClientJS.pm)) || die "ClientJS.pm not found";
+my @actions;
+
+foreach (read_file($file_name)) {
+ chomp;
+
+ next unless (m/^my \%supported_methods/ .. m/^\);/);
+
+ push @actions, [ 'action', $1, $2 ] if m/^\s+([a-zA-Z]+)\s*=>\s*(\d+),$/;
+ push @actions, [ 'comment', $1 ] if m/^\s+#\s+(.+)/;
+}
+
+my $longest = max map { length($_->[1]) } grep { $_->[0] eq 'action' } @actions;
+my $first = 1;
+my $output;
+
+# else if (action[0] == 'hide') $(action[1]).hide();
+foreach my $action (@actions) {
+ if ($action->[0] eq 'comment') {
+ print "\n" unless $first;
+ print " // ", $action->[1], "\n";
+
+ } else {
+ my $args = $action->[2] == 1 ? '' : join(', ', map { "action[$_]" } (2..$action->[2]));
+
+ printf(' %s if (action[0] == \'%s\')%s $(action[1]).%s(%s);' . "\n",
+ $first ? ' ' : 'else',
+ $action->[1],
+ ' ' x ($longest - length($action->[1])),
+ $action->[1],
+ $args);
+ $first = 0;
+ }
+}
+
+printf "\n else\%sconsole.log('Unknown action: ' + action[0]);\n", ' ' x (4 + 2 + 6 + 3 + 4 + 2 + $longest + 1);