62a4e8620cad110f8d2925cfa3e6aa0a5e5fde26
[kivitendo-erp.git] / SL / Template / Plugin / L.pm
1 package SL::Template::Plugin::L;
2
3 use base qw( Template::Plugin );
4 use Template::Plugin;
5 use Data::Dumper;
6 use List::MoreUtils qw(apply);
7 use List::Util qw(max);
8 use Scalar::Util qw(blessed);
9
10 use SL::Presenter;
11 use SL::Util qw(_hashify);
12
13 use strict;
14
15 { # This will give you an id for identifying html tags and such.
16   # It's guaranteed to be unique unless you exceed 10 mio calls per request.
17   # Do not use these id's to store information across requests.
18 my $_id_sequence = int rand 1e7;
19 sub _tag_id {
20   return "id_" . ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
21 }
22 }
23
24 sub _H {
25   my $string = shift;
26   return $::locale->quote_special_chars('HTML', $string);
27 }
28
29 sub _J {
30   my $string = shift;
31   $string    =~ s/(\"|\'|\\)/\\$1/g;
32   return $string;
33 }
34
35 sub new {
36   my ($class, $context, @args) = @_;
37
38   return bless {
39     CONTEXT => $context,
40   }, $class;
41 }
42
43 sub _context {
44   die 'not an accessor' if @_ > 1;
45   return $_[0]->{CONTEXT};
46 }
47
48 sub _call_presenter {
49   my ($method, $self, @args) = @_;
50
51   my $presenter              = $::request->presenter;
52
53   if (!$presenter->can($method)) {
54     $::lxdebug->message(LXDebug::WARN(), "SL::Presenter has no method named '$method'!");
55     return '';
56   }
57
58   splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
59
60   $presenter->$method(@args);
61 }
62
63 sub name_to_id    { return _call_presenter('name_to_id',    @_); }
64 sub html_tag      { return _call_presenter('html_tag',      @_); }
65 sub select_tag    { return _call_presenter('select_tag',    @_); }
66 sub input_tag     { return _call_presenter('input_tag',     @_); }
67 sub truncate      { return _call_presenter('truncate',      @_); }
68 sub simple_format { return _call_presenter('simple_format', @_); }
69 sub part_picker   { return _call_presenter('part_picker',   @_); }
70 sub chart_picker  { return _call_presenter('chart_picker',  @_); }
71 sub customer_vendor_picker   { return _call_presenter('customer_vendor_picker',   @_); }
72
73 sub _set_id_attribute {
74   my ($attributes, $name, $unique) = @_;
75   SL::Presenter::Tag::_set_id_attribute($attributes, $name, $unique);
76 }
77
78 sub img_tag {
79   my ($self, %options) = _hashify(1, @_);
80
81   $options{alt} ||= '';
82
83   return $self->html_tag('img', undef, %options);
84 }
85
86 sub textarea_tag {
87   my ($self, $name, $content, %attributes) = _hashify(3, @_);
88
89   _set_id_attribute(\%attributes, $name);
90   $attributes{rows}  *= 1; # required by standard
91   $attributes{cols}  *= 1; # required by standard
92   $content            = $content ? _H($content) : '';
93
94   return $self->html_tag('textarea', $content, %attributes, name => $name);
95 }
96
97 sub checkbox_tag {
98   my ($self, $name, %attributes) = _hashify(2, @_);
99
100   _set_id_attribute(\%attributes, $name);
101   $attributes{value}   = 1 unless defined $attributes{value};
102   my $label            = delete $attributes{label};
103   my $checkall         = delete $attributes{checkall};
104   my $for_submit       = delete $attributes{for_submit};
105
106   if ($attributes{checked}) {
107     $attributes{checked} = 'checked';
108   } else {
109     delete $attributes{checked};
110   }
111
112   my $code  = '';
113   $code    .= $self->hidden_tag($name, 0, %attributes, id => $attributes{id} . '_hidden') if $for_submit;
114   $code    .= $self->html_tag('input', undef,  %attributes, name => $name, type => 'checkbox');
115   $code    .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
116   $code    .= $self->javascript(qq|\$('#$attributes{id}').checkall('$checkall');|) if $checkall;
117
118   return $code;
119 }
120
121 sub radio_button_tag {
122   my ($self, $name, %attributes) = _hashify(2, @_);
123
124   $attributes{value}   = 1 unless exists $attributes{value};
125
126   _set_id_attribute(\%attributes, $name, 1);
127   my $label            = delete $attributes{label};
128
129   _set_id_attribute(\%attributes, $name . '_' . $attributes{value});
130
131   if ($attributes{checked}) {
132     $attributes{checked} = 'checked';
133   } else {
134     delete $attributes{checked};
135   }
136
137   my $code  = $self->html_tag('input', undef,  %attributes, name => $name, type => 'radio');
138   $code    .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
139
140   return $code;
141 }
142
143 sub hidden_tag {
144   my ($self, $name, $value, %attributes) = _hashify(3, @_);
145   return $self->input_tag($name, $value, %attributes, type => 'hidden');
146 }
147
148 sub div_tag {
149   my ($self, $content, @slurp) = @_;
150   return $self->html_tag('div', $content, @slurp);
151 }
152
153 sub ul_tag {
154   my ($self, $content, @slurp) = @_;
155   return $self->html_tag('ul', $content, @slurp);
156 }
157
158 sub li_tag {
159   my ($self, $content, @slurp) = @_;
160   return $self->html_tag('li', $content, @slurp);
161 }
162
163 sub link {
164   my ($self, $href, $content, %params) = _hashify(3, @_);
165
166   $href ||= '#';
167
168   return $self->html_tag('a', $content, %params, href => $href);
169 }
170
171 sub submit_tag {
172   my ($self, $name, $value, %attributes) = _hashify(3, @_);
173
174   if ( $attributes{confirm} ) {
175     $attributes{onclick} = 'return confirm("'. _J(delete($attributes{confirm})) .'");';
176   }
177
178   return $self->input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
179 }
180
181 sub button_tag {
182   my ($self, $onclick, $value, %attributes) = _hashify(3, @_);
183
184   _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
185   $attributes{type} ||= 'button';
186
187   $onclick = 'if (!confirm("'. _J(delete($attributes{confirm})) .'")) return false; ' . $onclick if $attributes{confirm};
188
189   return $self->html_tag('input', undef, %attributes, value => $value, onclick => $onclick);
190 }
191
192 sub ajax_submit_tag {
193   my ($self, $url, $form_selector, $text, @slurp) = @_;
194
195   $url           = _J($url);
196   $form_selector = _J($form_selector);
197   my $onclick    = qq|kivi.submit_ajax_form('${url}', '${form_selector}')|;
198
199   return $self->button_tag($onclick, $text, @slurp);
200 }
201
202 sub yes_no_tag {
203   my ($self, $name, $value, %attributes) = _hashify(3, @_);
204
205   return $self->select_tag($name, [ [ 1 => $::locale->text('Yes') ], [ 0 => $::locale->text('No') ] ], default => $value ? 1 : 0, %attributes);
206 }
207
208 sub javascript {
209   my ($self, $data) = @_;
210   return $self->html_tag('script', $data, type => 'text/javascript');
211 }
212
213 sub stylesheet_tag {
214   my $self = shift;
215   my $code = '';
216
217   foreach my $file (@_) {
218     $file .= '.css'        unless $file =~ m/\.css$/;
219     $file  = "css/${file}" unless $file =~ m|/|;
220
221     $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
222   }
223
224   return $code;
225 }
226
227 my $date_tag_id_idx = 0;
228 sub date_tag {
229   my ($self, $name, $value, %params) = _hashify(3, @_);
230
231   _set_id_attribute(\%params, $name);
232   my @onchange = $params{onchange} ? (onChange => delete $params{onchange}) : ();
233   my @classes  = $params{no_cal} || $params{readonly} ? () : ('datepicker');
234   push @classes, delete($params{class}) if $params{class};
235   my %class    = @classes ? (class => join(' ', @classes)) : ();
236
237   $::request->presenter->need_reinit_widgets($params{id});
238
239   return $self->input_tag(
240     $name, blessed($value) ? $value->to_lxoffice : $value,
241     size   => 11,
242     onblur => "check_right_date_format(this);",
243     %params,
244     %class, @onchange,
245   );
246 }
247
248 # simple version with select_tag
249 sub vendor_selector {
250   my ($self, $name, $value, %params) = _hashify(3, @_);
251
252   my $actual_vendor_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"}) ? $::form->{"$name"}->id : $::form->{"$name"}) :
253                          (ref $value && $value->can('id')) ? $value->id : '';
254
255   return $self->select_tag($name, SL::DB::Manager::Vendor->get_all(),
256                                   default      => $actual_vendor_id,
257                                   title_sub    => sub { $_[0]->vendornumber . " : " . $_[0]->name },
258                                   'with_empty' => 1,
259                                   %params);
260 }
261
262
263 # simple version with select_tag
264 sub part_selector {
265   my ($self, $name, $value, %params) = _hashify(3, @_);
266
267   my $actual_part_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"})? $::form->{"$name"}->id : $::form->{"$name"}) :
268                        (ref $value && $value->can('id')) ? $value->id : '';
269
270   return $self->select_tag($name, SL::DB::Manager::Part->get_all(),
271                            default      => $actual_part_id,
272                            title_sub    => sub { $_[0]->partnumber . " : " . $_[0]->description },
273                            with_empty   => 1,
274                            %params);
275 }
276
277
278 sub javascript_tag {
279   my $self = shift;
280   my $code = '';
281
282   foreach my $file (@_) {
283     $file .= '.js'        unless $file =~ m/\.js$/;
284     $file  = "js/${file}" unless $file =~ m|/|;
285
286     $code .= qq|<script type="text/javascript" src="${file}"></script>|;
287   }
288
289   return $code;
290 }
291
292 sub tabbed {
293   my ($self, $tabs, %params) = _hashify(2, @_);
294   my $id       = $params{id} || 'tab_' . _tag_id();
295
296   $params{selected} *= 1;
297
298   die 'L.tabbed needs an arrayred of tabs for first argument'
299     unless ref $tabs eq 'ARRAY';
300
301   my (@header, @blocks);
302   for my $i (0..$#$tabs) {
303     my $tab = $tabs->[$i];
304
305     next if $tab eq '';
306
307     my $tab_id = "__tab_id_$i";
308     push @header, $self->li_tag($self->link('#' . $tab_id, $tab->{name}));
309     push @blocks, $self->div_tag($tab->{data}, id => $tab_id);
310   }
311
312   return '' unless @header;
313
314   my $ul = $self->ul_tag(join('', @header), id => $id);
315   return $self->div_tag(join('', $ul, @blocks), class => 'tabwidget');
316 }
317
318 sub tab {
319   my ($self, $name, $src, %params) = _hashify(3, @_);
320
321   $params{method} ||= 'process';
322
323   return () if defined $params{if} && !$params{if};
324
325   my $data;
326   if ($params{method} eq 'raw') {
327     $data = $src;
328   } elsif ($params{method} eq 'process') {
329     $data = $self->_context->process($src, %{ $params{args} || {} });
330   } else {
331     die "unknown tag method '$params{method}'";
332   }
333
334   return () unless $data;
335
336   return +{ name => $name, data => $data };
337 }
338
339 sub areainput_tag {
340   my ($self, $name, $value, %attributes) = _hashify(3, @_);
341
342   my $cols    = delete $attributes{cols} || delete $attributes{size};
343   my $minrows = delete $attributes{min_rows} || 1;
344   my $maxrows = delete $attributes{max_rows};
345   my $rows    = $::form->numtextrows($value, $cols, $maxrows, $minrows);
346
347   return $rows > 1
348     ? $self->textarea_tag($name, $value, %attributes, rows => $rows, cols => $cols)
349     : $self->input_tag($name, $value, %attributes, size => $cols);
350 }
351
352 sub multiselect2side {
353   my ($self, $id, %params) = _hashify(2, @_);
354
355   $params{labelsx}        = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
356   $params{labeldx}        = "\"" . _J($params{labeldx} || $::locale->text('Selected'))  . "\"";
357   $params{moveOptions}    = 'false';
358
359   my $vars                = join(', ', map { "${_}: " . $params{$_} } keys %params);
360   my $code                = <<EOCODE;
361 <script type="text/javascript">
362   \$().ready(function() {
363     \$('#${id}').multiselect2side({ ${vars} });
364   });
365 </script>
366 EOCODE
367
368   return $code;
369 }
370
371 sub sortable_element {
372   my ($self, $selector, %params) = _hashify(2, @_);
373
374   my %attributes = ( distance => 5,
375                      helper   => <<'JAVASCRIPT' );
376     function(event, ui) {
377       ui.children().each(function() {
378         $(this).width($(this).width());
379       });
380       return ui;
381     }
382 JAVASCRIPT
383
384   my $stop_event = '';
385
386   if ($params{url} && $params{with}) {
387     my $as      = $params{as} || $params{with};
388     my $filter  = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
389     $filter    .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
390
391     my $params_js = $params{params} ? qq| + ($params{params})| : '';
392
393     $stop_event = <<JAVASCRIPT;
394         \$.post('$params{url}'${params_js}, { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() });
395 JAVASCRIPT
396   }
397
398   if (!$params{dont_recolor}) {
399     $stop_event .= <<JAVASCRIPT;
400         \$('${selector}>*:odd').removeClass('listrow1').removeClass('listrow0').addClass('listrow0');
401         \$('${selector}>*:even').removeClass('listrow1').removeClass('listrow0').addClass('listrow1');
402 JAVASCRIPT
403   }
404
405   if ($stop_event) {
406     $attributes{stop} = <<JAVASCRIPT;
407       function(event, ui) {
408         ${stop_event}
409         return ui;
410       }
411 JAVASCRIPT
412   }
413
414   $params{handle}     = '.dragdrop' unless exists $params{handle};
415   $attributes{handle} = "'$params{handle}'" if $params{handle};
416
417   my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
418
419   my $code = <<JAVASCRIPT;
420 <script type="text/javascript">
421   \$(function() {
422     \$( "${selector}" ).sortable({ ${attr_str} })
423   });
424 </script>
425 JAVASCRIPT
426
427   return $code;
428 }
429
430 sub dump {
431   my $self = shift;
432   return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
433 }
434
435 sub sortable_table_header {
436   my ($self, $by, %params) = _hashify(2, @_);
437
438   my $controller          = $self->{CONTEXT}->stash->get('SELF');
439   my $models              = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
440   my $sort_spec           = $models->get_sort_spec;
441   my $by_spec             = $sort_spec->{$by};
442   my %current_sort_params = $models->get_current_sort_params;
443   my ($image, $new_dir)   = ('', $current_sort_params{dir});
444   my $title               = delete($params{title}) || $::locale->text($by_spec->{title});
445
446   if ($current_sort_params{sort_by} eq $by) {
447     my $current_dir = $current_sort_params{sort_dir} ? 'up' : 'down';
448     $image          = '<img border="0" src="image/' . $current_dir . '.png">';
449     $new_dir        = 1 - ($current_sort_params{sort_dir} || 0);
450   }
451
452   $params{ $models->sorted->form_params->[0] } = $by;
453   $params{ $models->sorted->form_params->[1] } = ($new_dir ? '1' : '0');
454
455   return '<a href="' . $models->get_callback(%params) . '">' . _H($title) . $image . '</a>';
456 }
457
458 sub paginate_controls {
459   my ($self, %params) = _hashify(1, @_);
460
461   my $controller      = $self->{CONTEXT}->stash->get('SELF');
462   my $models          = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
463   my $pager           = $models->paginated;
464 #  my $paginate_spec   = $controller->get_paginate_spec;
465
466   my %paginate_params = $models->get_paginate_args;
467
468   my %template_params = (
469     pages             => \%paginate_params,
470     url_maker         => sub {
471       my %url_params                                    = _hashify(0, @_);
472       $url_params{ $pager->form_params->[0] } = delete $url_params{page};
473       $url_params{ $pager->form_params->[1] } = delete $url_params{per_page} if exists $url_params{per_page};
474
475       return $models->get_callback(%url_params);
476     },
477     %params,
478   );
479
480   return SL::Presenter->get->render('common/paginate', %template_params);
481 }
482
483 1;
484
485 __END__
486
487 =head1 NAME
488
489 SL::Templates::Plugin::L -- Layouting / tag generation
490
491 =head1 SYNOPSIS
492
493 Usage from a template:
494
495   [% USE L %]
496
497   [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right', 1 ] ]) %]
498
499   [% L.select_tag('direction', [ { direction => 'left',  display => 'To the left'  },
500                                  { direction => 'right', display => 'To the right' } ],
501                                value_key => 'direction', title_key => 'display', default => 'right')) %]
502
503   [% L.select_tag('direction', [ { direction => 'left',  display => 'To the left'  },
504                                  { direction => 'right', display => 'To the right', selected => 1 } ],
505                                value_key => 'direction', title_key => 'display')) %]
506
507 =head1 DESCRIPTION
508
509 A module modeled a bit after Rails' ActionView helpers. Several small
510 functions that create HTML tags from various kinds of data sources.
511
512 The C<id> attribute is usually calculated automatically. This can be
513 overridden by either specifying an C<id> attribute or by setting
514 C<no_id> to trueish.
515
516 =head1 FUNCTIONS
517
518 =head2 LOW-LEVEL FUNCTIONS
519
520 The following items are just forwarded to L<SL::Presenter::Tag>:
521
522 =over 2
523
524 =item * C<name_to_id $name>
525
526 =item * C<stringify_attributes %items>
527
528 =item * C<html_tag $tag_name, $content_string, %attributes>
529
530 =back
531
532 =head2 HIGH-LEVEL FUNCTIONS
533
534 The following functions are just forwarded to L<SL::Presenter::Tag>:
535
536 =over 2
537
538 =item * C<input_tag $name, $value, %attributes>
539
540 =item * C<select_tag $name, \@collection, %attributes>
541
542 =back
543
544 Available high-level functions implemented in this module:
545
546 =over 4
547
548 =item C<yes_no_tag $name, $value, %attributes>
549
550 Creates a HTML 'select' tag with the two entries C<yes> and C<no> by
551 calling L<select_tag>. C<$value> determines
552 which entry is selected. The C<%attributes> are passed through to
553 L<select_tag>.
554
555 =item C<hidden_tag $name, $value, %attributes>
556
557 Creates a HTML 'input type=hidden' tag named C<$name> with the value
558 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
559 tag's C<id> defaults to C<name_to_id($name)>.
560
561 =item C<submit_tag $name, $value, %attributes>
562
563 Creates a HTML 'input type=submit class=submit' tag named C<$name> with the
564 value C<$value> and with arbitrary HTML attributes from C<%attributes>. The
565 tag's C<id> defaults to C<name_to_id($name)>.
566
567 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
568 be added via the C<onclick> handler asking the question given with
569 C<$attributes{confirm}>. The request is only submitted if the user
570 clicks the dialog's ok/yes button.
571
572 =item C<ajax_submit_tag $url, $form_selector, $text, %attributes>
573
574 Creates a HTML 'input type="button"' tag with a very specific onclick
575 handler that submits the form given by the jQuery selector
576 C<$form_selector> to the URL C<$url> (the actual JavaScript function
577 called for that is C<kivi.submit_ajax_form()> in
578 C<js/client_js.js>). The button's label will be C<$text>.
579
580 =item C<button_tag $onclick, $text, %attributes>
581
582 Creates a HTML 'input type="button"' tag with an onclick handler
583 C<$onclick> and a value of C<$text>. The button does not have a name
584 nor an ID by default.
585
586 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
587 be prepended to the C<$onclick> handler asking the question given with
588 C<$attributes{confirm}>. The request is only submitted if the user
589 clicks the dialog's "ok/yes" button.
590
591 =item C<textarea_tag $name, $value, %attributes>
592
593 Creates a HTML 'textarea' tag named C<$name> with the content
594 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
595 tag's C<id> defaults to C<name_to_id($name)>.
596
597 =item C<checkbox_tag $name, %attributes>
598
599 Creates a HTML 'input type=checkbox' tag named C<$name> with arbitrary
600 HTML attributes from C<%attributes>. The tag's C<id> defaults to
601 C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
602
603 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
604 created with said C<label>. No attribute named C<label> is created in
605 that case.
606
607 If C<%attributes> contains a key C<checkall> then the value is taken as a
608 JQuery selector and clicking this checkbox will also toggle all checkboxes
609 matching the selector.
610
611 =item C<date_tag $name, $value, %attributes>
612
613 Creates a date input field, with an attached javascript that will open a
614 calendar on click.
615
616 =item C<radio_button_tag $name, %attributes>
617
618 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
619 HTML attributes from C<%attributes>. The tag's C<value> defaults to
620 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
621
622 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
623 created with said C<label>. No attribute named C<label> is created in
624 that case.
625
626 =item C<javascript_tag $file1, $file2, $file3...>
627
628 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
629 tag for each file name parameter passed. Each file name will be
630 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
631 doesn't contain a slash.
632
633 =item C<stylesheet_tag $file1, $file2, $file3...>
634
635 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
636 for each file name parameter passed. Each file name will be postfixed
637 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
638 contain a slash.
639
640 =item C<tabbed \@tab, %attributes>
641
642 Will create a tabbed area. The tabs should be created with the helper function
643 C<tab>. Example:
644
645   [% L.tabbed([
646     L.tab(LxERP.t8('Basic Data'),       'part/_main_tab.html'),
647     L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
648   ]) %]
649
650 =item C<areainput_tag $name, $content, %PARAMS>
651
652 Creates a generic input tag or textarea tag, depending on content size. The
653 amount of desired rows must be either given with the C<rows> parameter or can
654 be computed from the value and the C<cols> paramter, Accepted parameters
655 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
656
657 You can force input by setting rows to 1, and you can force textarea by setting
658 rows to anything >1.
659
660 =item C<multiselect2side $id, %params>
661
662 Creates a JavaScript snippet calling the jQuery function
663 C<multiselect2side> on the select control with the ID C<$id>. The
664 select itself is not created. C<%params> can contain the following
665 entries:
666
667 =over 2
668
669 =item C<labelsx>
670
671 The label of the list of available options. Defaults to the
672 translation of 'Available'.
673
674 =item C<labeldx>
675
676 The label of the list of selected options. Defaults to the
677 translation of 'Selected'.
678
679 =back
680
681 =item C<sortable_element $selector, %params>
682
683 Makes the children of the DOM element C<$selector> (a jQuery selector)
684 sortable with the I<jQuery UI Selectable> library. The children can be
685 dragged & dropped around. After dropping an element an URL can be
686 postet to with the element IDs of the sorted children.
687
688 If this is used then the JavaScript file C<js/jquery-ui.js> must be
689 included manually as well as it isn't loaded via C<$::form-gt;header>.
690
691 C<%params> can contain the following entries:
692
693 =over 2
694
695 =item C<url>
696
697 The URL to POST an AJAX request to after a dragged element has been
698 dropped. The AJAX request's return value is ignored. If given then
699 C<$params{with}> must be given as well.
700
701 =item C<with>
702
703 A string that is interpreted as the prefix of the children's ID. Upon
704 POSTing the result each child whose ID starts with C<$params{with}> is
705 considered. The prefix and the following "_" is removed from the
706 ID. The remaining parts of the IDs of those children are posted as a
707 single array parameter. The array parameter's name is either
708 C<$params{as}> or, missing that, C<$params{with}>.
709
710 =item C<as>
711
712 Sets the POST parameter name for AJAX request after dropping an
713 element (see C<$params{with}>).
714
715 =item C<handle>
716
717 An optional jQuery selector specifying which part of the child element
718 is dragable. If the parameter is not given then it defaults to
719 C<.dragdrop> matching DOM elements with the class C<dragdrop>.  If the
720 parameter is set and empty then the whole child element is dragable,
721 and clicks through to underlying elements like inputs or links might
722 not work.
723
724 =item C<dont_recolor>
725
726 If trueish then the children will not be recolored. The default is to
727 recolor the children by setting the class C<listrow0> on odd and
728 C<listrow1> on even entries.
729
730 =item C<params>
731
732 An optional JavaScript string that is evaluated before sending the
733 POST request. The result must be a string that is appended to the URL.
734
735 =back
736
737 Example:
738
739   <script type="text/javascript" src="js/jquery-ui.js"></script>
740
741   <table id="thing_list">
742     <thead>
743       <tr><td>This</td><td>That</td></tr>
744     </thead>
745     <tbody>
746       <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
747       <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
748       <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
749     </tbody>
750   <table>
751
752   [% L.sortable_element('#thing_list tbody',
753                         url          => 'controller.pl?action=SystemThings/reorder',
754                         with         => 'thingy',
755                         as           => 'thing_ids',
756                         recolor_rows => 1) %]
757
758 After dropping e.g. the third element at the top of the list a POST
759 request would be made to the C<reorder> action of the C<SystemThings>
760 controller with a single parameter called C<thing_ids> -- an array
761 containing the values C<[ 6, 2, 15 ]>.
762
763 =item C<dump REF>
764
765 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
766
767 =item C<sortable_table_header $by, %params>
768
769 Create a link and image suitable for placement in a table
770 header. C<$by> must be an index set up by the controller with
771 L<SL::Controller::Helper::make_sorted>.
772
773 The optional parameter C<$params{title}> can override the column title
774 displayed to the user. Otherwise the column title from the
775 controller's sort spec is used.
776
777 The other parameters in C<%params> are passed unmodified to the
778 underlying call to L<SL::Controller::Base::url_for>.
779
780 See the documentation of L<SL::Controller::Helper::Sorted> for an
781 overview and further usage instructions.
782
783 =item C<paginate_controls>
784
785 Create a set of links used to paginate a list view.
786
787 See the documentation of L<SL::Controller::Helper::Paginated> for an
788 overview and further usage instructions.
789
790 =back
791
792 =head2 CONVERSION FUNCTIONS
793
794 =over 4
795
796 =item C<tab, description, target, %PARAMS>
797
798 Creates a tab for C<tabbed>. The description will be used as displayed name.
799 The target should be a block or template that can be processed. C<tab> supports
800 a C<method> parameter, which can override the process method to apply target.
801 C<method => 'raw'> will just include the given text as is. I was too lazy to
802 implement C<include> properly.
803
804 Also an C<if> attribute is supported, so that tabs can be suppressed based on
805 some occasion. In this case the supplied block won't even get processed, and
806 the resulting tab will get ignored by C<tabbed>:
807
808   L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
809
810 =item C<truncate $text, [%params]>
811
812 See L<SL::Presenter::Text/truncate>.
813
814 =item C<simple_format $text>
815
816 See L<SL::Presenter::Text/simple_format>.
817
818 =back
819
820 =head1 MODULE AUTHORS
821
822 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
823
824 L<http://linet-services.de>