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