d28ecb44f43d9d0ffb723583ba345af88df240b5
[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 List::MoreUtils qw(apply);
6 use List::Util qw(max);
7
8 use strict;
9
10 { # This will give you an id for identifying html tags and such.
11   # It's guaranteed to be unique unless you exceed 10 mio calls per request.
12   # Do not use these id's to store information across requests.
13 my $_id_sequence = int rand 1e7;
14 sub _tag_id {
15   return $_id_sequence = ($_id_sequence + 1) % 1e7;
16 }
17 }
18
19 sub _H {
20   my $string = shift;
21   return $::locale->quote_special_chars('HTML', $string);
22 }
23
24 sub _J {
25   my $string =  "" . shift;
26   $string    =~ s/\"/\\\"/g;
27   return $string;
28 }
29
30 sub _hashify {
31   return (@_ && (ref($_[0]) eq 'HASH')) ? %{ $_[0] } : @_;
32 }
33
34 sub new {
35   my ($class, $context, @args) = @_;
36
37   return bless {
38     CONTEXT => $context,
39   }, $class;
40 }
41
42 sub _context {
43   die 'not an accessor' if @_ > 1;
44   return $_[0]->{CONTEXT};
45 }
46
47 sub name_to_id {
48   my $self =  shift;
49   my $name =  shift;
50
51   $name    =~ s/[^\w_]/_/g;
52   $name    =~ s/_+/_/g;
53
54   return $name;
55 }
56
57 sub attributes {
58   my ($self, @slurp)    = @_;
59   my %options = _hashify(@slurp);
60
61   my @result = ();
62   while (my ($name, $value) = each %options) {
63     next unless $name;
64     next if $name eq 'disabled' && !$value;
65     $value = '' if !defined($value);
66     push @result, _H($name) . '="' . _H($value) . '"';
67   }
68
69   return @result ? ' ' . join(' ', @result) : '';
70 }
71
72 sub html_tag {
73   my ($self, $tag, $content, @slurp) = @_;
74   my $attributes = $self->attributes(@slurp);
75
76   return "<${tag}${attributes}/>" unless defined($content);
77   return "<${tag}${attributes}>${content}</${tag}>";
78 }
79
80 sub select_tag {
81   my $self            = shift;
82   my $name            = shift;
83   my $options_str     = shift;
84   my %attributes      = _hashify(@_);
85
86   $attributes{id}   ||= $self->name_to_id($name);
87   $options_str        = $self->options_for_select($options_str) if ref $options_str;
88
89   return $self->html_tag('select', $options_str, %attributes, name => $name);
90 }
91
92 sub textarea_tag {
93   my ($self, $name, $content, @slurp) = @_;
94   my %attributes      = _hashify(@slurp);
95
96   $attributes{id}   ||= $self->name_to_id($name);
97   $content            = $content ? _H($content) : '';
98
99   return $self->html_tag('textarea', $content, %attributes, name => $name);
100 }
101
102 sub checkbox_tag {
103   my ($self, $name, @slurp) = @_;
104   my %attributes       = _hashify(@slurp);
105
106   $attributes{id}    ||= $self->name_to_id($name);
107   $attributes{value}   = 1 unless defined $attributes{value};
108   my $label            = delete $attributes{label};
109
110   if ($attributes{checked}) {
111     $attributes{checked} = 'checked';
112   } else {
113     delete $attributes{checked};
114   }
115
116   my $code  = $self->html_tag('input', undef,  %attributes, name => $name, type => 'checkbox');
117   $code    .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
118
119   return $code;
120 }
121
122 sub radio_button_tag {
123   my $self             = shift;
124   my $name             = shift;
125   my %attributes       = _hashify(@_);
126
127   $attributes{value}   = 1 unless defined $attributes{value};
128   $attributes{id}    ||= $self->name_to_id($name . "_" . $attributes{value});
129   my $label            = delete $attributes{label};
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 input_tag {
144   my ($self, $name, $value, @slurp) = @_;
145   my %attributes      = _hashify(@slurp);
146
147   $attributes{id}   ||= $self->name_to_id($name);
148   $attributes{type} ||= 'text';
149
150   return $self->html_tag('input', undef, %attributes, name => $name, value => $value);
151 }
152
153 sub hidden_tag {
154   return shift->input_tag(@_, type => 'hidden');
155 }
156
157 sub div_tag {
158   my ($self, $content, @slurp) = @_;
159   return $self->html_tag('div', $content, @slurp);
160 }
161
162 sub ul_tag {
163   my ($self, $content, @slurp) = @_;
164   return $self->html_tag('ul', $content, @slurp);
165 }
166
167 sub li_tag {
168   my ($self, $content, @slurp) = @_;
169   return $self->html_tag('li', $content, @slurp);
170 }
171
172 sub link {
173   my ($self, $href, $content, @slurp) = @_;
174   my %params = _hashify(@slurp);
175
176   $href ||= '#';
177
178   return $self->html_tag('a', $content, %params, href => $href);
179 }
180
181 sub submit_tag {
182   my ($self, $name, $value, @slurp) = @_;
183   my %attributes = _hashify(@slurp);
184
185   $attributes{onclick} = "if (confirm('" . delete($attributes{confirm}) . "')) return true; else return false;" if $attributes{confirm};
186
187   return $self->input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
188 }
189
190 sub button_tag {
191   my ($self, $onclick, $value, @slurp) = @_;
192   my %attributes = _hashify(@slurp);
193
194   return $self->input_tag(undef, $value, %attributes, type => 'button', onclick => $onclick);
195 }
196
197 sub options_for_select {
198   my $self            = shift;
199   my $collection      = shift;
200   my %options         = _hashify(@_);
201
202   my $value_key       = $options{value} || 'id';
203   my $title_key       = $options{title} || $value_key;
204
205   my $value_sub       = $options{value_sub};
206   my $title_sub       = $options{title_sub};
207
208   my $value_title_sub = $options{value_title_sub};
209
210   my %selected        = map { ( $_ => 1 ) } @{ ref($options{default}) eq 'ARRAY' ? $options{default} : defined($options{default}) ? [ $options{default} ] : [] };
211
212   my $access = sub {
213     my ($element, $index, $key, $sub) = @_;
214     my $ref = ref $element;
215     return  $sub            ? $sub->($element)
216          : !$ref            ? $element
217          :  $ref eq 'ARRAY' ? $element->[$index]
218          :  $ref eq 'HASH'  ? $element->{$key}
219          :                    $element->$key;
220   };
221
222   my @elements = ();
223   push @elements, [ undef, $options{empty_title} || '' ] if $options{with_empty};
224   push @elements, map [
225     $value_title_sub ? $value_title_sub->($_) : (
226       $access->($_, 0, $value_key, $value_sub),
227       $access->($_, 1, $title_key, $title_sub),
228     )
229   ], @{ $collection } if $collection && ref $collection eq 'ARRAY';
230
231   my $code = '';
232   foreach my $result (@elements) {
233     my %attributes = ( value => $result->[0] );
234     $attributes{selected} = 'selected' if $selected{ defined($result->[0]) ? $result->[0] : '' };
235
236     $code .= $self->html_tag('option', _H($result->[1]), %attributes);
237   }
238
239   return $code;
240 }
241
242 sub javascript {
243   my ($self, $data) = @_;
244   return $self->html_tag('script', $data, type => 'text/javascript');
245 }
246
247 sub stylesheet_tag {
248   my $self = shift;
249   my $code = '';
250
251   foreach my $file (@_) {
252     $file .= '.css'        unless $file =~ m/\.css$/;
253     $file  = "css/${file}" unless $file =~ m|/|;
254
255     $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
256   }
257
258   return $code;
259 }
260
261 sub date_tag {
262   my ($self, $name, $value, @slurp) = @_;
263   my %params   = _hashify(@slurp);
264   my $name_e   = _H($name);
265   my $seq      = _tag_id();
266   my $datefmt  = apply {
267     s/d+/\%d/gi;
268     s/m+/\%m/gi;
269     s/y+/\%Y/gi;
270   } $::myconfig{"dateformat"};
271
272   $params{cal_align} ||= 'BR';
273
274   $self->input_tag($name, $value,
275     id     => $name_e,
276     size   => 11,
277     title  => _H($::myconfig{dateformat}),
278     onBlur => 'check_right_date_format(this)',
279     %params,
280   ) . ((!$params{no_cal}) ?
281   $self->html_tag('img', undef,
282     src    => 'image/calendar.png',
283     id     => "trigger$seq",
284     title  => _H($::myconfig{dateformat}),
285     %params,
286   ) .
287   $self->javascript(
288     "Calendar.setup({ inputField: '$name_e', ifFormat: '$datefmt', align: '$params{cal_align}', button: 'trigger$seq' });"
289   ) : '');
290 }
291
292 sub javascript_tag {
293   my $self = shift;
294   my $code = '';
295
296   foreach my $file (@_) {
297     $file .= '.js'        unless $file =~ m/\.js$/;
298     $file  = "js/${file}" unless $file =~ m|/|;
299
300     $code .= qq|<script type="text/javascript" src="${file}"></script>|;
301   }
302
303   return $code;
304 }
305
306 sub tabbed {
307   my ($self, $tabs, @slurp) = @_;
308   my %params   = _hashify(@slurp);
309   my $id       = $params{id} || 'tab_' . _tag_id();
310
311   $params{selected} *= 1;
312
313   die 'L.tabbed needs an arrayred of tabs for first argument'
314     unless ref $tabs eq 'ARRAY';
315
316   my (@header, @blocks);
317   for my $i (0..$#$tabs) {
318     my $tab = $tabs->[$i];
319
320     next if $tab eq '';
321
322     my $selected = $params{selected} == $i;
323     my $tab_id   = "__tab_id_$i";
324     push @header, $self->li_tag(
325       $self->link('', $tab->{name}, rel => $tab_id),
326         ($selected ? (class => 'selected') : ())
327     );
328     push @blocks, $self->div_tag($tab->{data},
329       id => $tab_id, class => 'tabcontent');
330   }
331
332   return '' unless @header;
333   return $self->ul_tag(
334     join('', @header), id => $id, class => 'shadetabs'
335   ) .
336   $self->div_tag(
337     join('', @blocks), class => 'tabcontentstyle'
338   ) .
339   $self->javascript(
340     qq|var $id = new ddtabcontent("$id");$id.setpersist(true);| .
341     qq|$id.setselectedClassTarget("link");$id.init();|
342   );
343 }
344
345 sub tab {
346   my ($self, $name, $src, @slurp) = @_;
347   my %params = _hashify(@slurp);
348
349   $params{method} ||= 'process';
350
351   return () if defined $params{if} && !$params{if};
352
353   my $data;
354   if ($params{method} eq 'raw') {
355     $data = $src;
356   } elsif ($params{method} eq 'process') {
357     $data = $self->_context->process($src, %{ $params{args} || {} });
358   } else {
359     die "unknown tag method '$params{method}'";
360   }
361
362   return () unless $data;
363
364   return +{ name => $name, data => $data };
365 }
366
367 sub areainput_tag {
368   my ($self, $name, $value, @slurp) = @_;
369   my %attributes      = _hashify(@slurp);
370
371   my $rows = delete $attributes{rows}     || 1;
372   my $min  = delete $attributes{min_rows} || 1;
373
374   return $rows > 1
375     ? $self->textarea_tag($name, $value, %attributes, rows => max $rows, $min)
376     : $self->input_tag($name, $value, %attributes);
377 }
378
379 sub multiselect2side {
380   my ($self, $id, @slurp) = @_;
381   my %params              = _hashify(@slurp);
382
383   $params{labelsx}        = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
384   $params{labeldx}        = "\"" . _J($params{labeldx} || $::locale->text('Selected'))  . "\"";
385   $params{moveOptions}    = 'false';
386
387   my $vars                = join(', ', map { "${_}: " . $params{$_} } keys %params);
388   my $code                = <<EOCODE;
389 <script type="text/javascript">
390   \$().ready(function() {
391     \$('#${id}').multiselect2side({ ${vars} });
392   });
393 </script>
394 EOCODE
395
396   return $code;
397 }
398
399 sub sortable_element {
400   my ($self, $selector, @slurp) = @_;
401   my %params                    = _hashify(@slurp);
402
403   my %attributes = ( helper => <<JAVASCRIPT );
404     function(event, ui) {
405       ui.children().each(function() {
406         \$(this).width(\$(this).width());
407       });
408       return ui;
409     }
410 JAVASCRIPT
411
412   if ($params{url} && $params{with}) {
413     my $as      = $params{as} || $params{with};
414     my $filter  = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
415     $filter    .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
416
417     $attributes{stop} = <<JAVASCRIPT;
418       function(event, ui) {
419         \$.post('$params{url}', { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() });
420         return ui;
421       }
422 JAVASCRIPT
423   }
424
425   my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
426
427   my $code = <<JAVASCRIPT;
428 <script type="text/javascript">
429   \$(function() {
430     \$( "${selector}" ).sortable({ ${attr_str} }).disableSelection();
431   });
432 </script>
433 JAVASCRIPT
434
435   return $code;
436 }
437
438 sub online_help_tag {
439   my ($self, $tag, @slurp) = @_;
440   my %params               = _hashify(@slurp);
441   my $cc                   = $::myconfig{countrycode};
442   my $file                 = "doc/online/$cc/$tag.html";
443   my $text                 = $params{text} || $::locale->text('Help');
444
445   die 'malformed help tag' unless $tag =~ /^[a-zA-Z0-9_]+$/;
446   return unless -f $file;
447   return $self->html_tag('a', $text, href => $file, target => '_blank');
448 }
449
450 sub dump {
451   my $self = shift;
452   require Data::Dumper;
453   return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
454 }
455
456 1;
457
458 __END__
459
460 =head1 NAME
461
462 SL::Templates::Plugin::L -- Layouting / tag generation
463
464 =head1 SYNOPSIS
465
466 Usage from a template:
467
468   [% USE L %]
469
470   [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right' ] ]) %]
471
472   [% L.select_tag('direction', L.options_for_select([ { direction => 'left',  display => 'To the left'  },
473                                                       { direction => 'right', display => 'To the right' } ],
474                                                     value => 'direction', title => 'display', default => 'right')) %]
475
476 =head1 DESCRIPTION
477
478 A module modeled a bit after Rails' ActionView helpers. Several small
479 functions that create HTML tags from various kinds of data sources.
480
481 =head1 FUNCTIONS
482
483 =head2 LOW-LEVEL FUNCTIONS
484
485 =over 4
486
487 =item C<name_to_id $name>
488
489 Converts a name to a HTML id by replacing various characters.
490
491 =item C<attributes %items>
492
493 Creates a string from all elements in C<%items> suitable for usage as
494 HTML tag attributes. Keys and values are HTML escaped even though keys
495 must not contain non-ASCII characters for browsers to accept them.
496
497 =item C<html_tag $tag_name, $content_string, %attributes>
498
499 Creates an opening and closing HTML tag for C<$tag_name> and puts
500 C<$content_string> between the two. If C<$content_string> is undefined
501 or empty then only a E<lt>tag/E<gt> tag will be created. Attributes
502 are key/value pairs added to the opening tag.
503
504 C<$content_string> is not HTML escaped.
505
506 =back
507
508 =head2 HIGH-LEVEL FUNCTIONS
509
510 =over 4
511
512 =item C<select_tag $name, $options_string, %attributes>
513
514 Creates a HTML 'select' tag named C<$name> with the contents
515 C<$options_string> and with arbitrary HTML attributes from
516 C<%attributes>. The tag's C<id> defaults to C<name_to_id($name)>.
517
518 The C<$options_string> is usually created by the
519 L</options_for_select> function. If C<$options_string> is an array
520 reference then it will be passed to L</options_for_select>
521 automatically.
522
523 =item C<input_tag $name, $value, %attributes>
524
525 Creates a HTML 'input type=text' tag named C<$name> with the value
526 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
527 tag's C<id> defaults to C<name_to_id($name)>.
528
529 =item C<hidden_tag $name, $value, %attributes>
530
531 Creates a HTML 'input type=hidden' tag named C<$name> with the value
532 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
533 tag's C<id> defaults to C<name_to_id($name)>.
534
535 =item C<submit_tag $name, $value, %attributes>
536
537 Creates a HTML 'input type=submit class=submit' tag named C<$name> with the
538 value C<$value> and with arbitrary HTML attributes from C<%attributes>. The
539 tag's C<id> defaults to C<name_to_id($name)>.
540
541 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
542 be added via the C<onclick> handler asking the question given with
543 C<$attributes{confirm}>. If request is only submitted if the user
544 clicks the dialog's ok/yes button.
545
546 =item C<textarea_tag $name, $value, %attributes>
547
548 Creates a HTML 'textarea' tag named C<$name> with the content
549 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
550 tag's C<id> defaults to C<name_to_id($name)>.
551
552 =item C<checkbox_tag $name, %attributes>
553
554 Creates a HTML 'input type=checkbox' tag named C<$name> with arbitrary
555 HTML attributes from C<%attributes>. The tag's C<id> defaults to
556 C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
557
558 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
559 created with said C<label>. No attribute named C<label> is created in
560 that case.
561
562 =item C<date_tag $name, $value, cal_align =E<gt> $align_code, %attributes>
563
564 Creates a date input field, with an attached javascript that will open a
565 calendar on click. The javascript ist by default anchoered at the bottom right
566 sight. This can be overridden with C<cal_align>, see Calendar documentation for
567 the details, usually you'll want a two letter abbreviation of the alignment.
568 Right + Bottom becomes C<BL>.
569
570 =item C<radio_button_tag $name, %attributes>
571
572 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
573 HTML attributes from C<%attributes>. The tag's C<value> defaults to
574 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
575
576 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
577 created with said C<label>. No attribute named C<label> is created in
578 that case.
579
580 =item C<javascript_tag $file1, $file2, $file3...>
581
582 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
583 tag for each file name parameter passed. Each file name will be
584 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
585 doesn't contain a slash.
586
587 =item C<stylesheet_tag $file1, $file2, $file3...>
588
589 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
590 for each file name parameter passed. Each file name will be postfixed
591 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
592 contain a slash.
593
594 =item C<date_tag $name, $value, cal_align =E<gt> $align_code, %attributes>
595
596 Creates a date input field, with an attached javascript that will open a
597 calendar on click. The javascript ist by default anchoered at the bottom right
598 sight. This can be overridden with C<cal_align>, see Calendar documentation for
599 the details, usually you'll want a two letter abbreviation of the alignment.
600 Right + Bottom becomes C<BL>.
601
602 =item C<tabbed \@tab, %attributes>
603
604 Will create a tabbed area. The tabs should be created with the helper function
605 C<tab>. Example:
606
607   [% L.tabbed([
608     L.tab(LxERP.t8('Basic Data'),       'part/_main_tab.html'),
609     L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
610   ]) %]
611
612 An optional attribute is C<selected>, which accepts the ordinal of a tab which
613 should be selected by default.
614
615 =item C<areainput_tag $name, $content, %PARAMS>
616
617 Creates a generic input tag or textarea tag, depending on content size. The
618 mount of desired rows must be given with C<rows> parameter, Accpeted parameters
619 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
620
621 You can force input by setting rows to 1, and you can force textarea by setting
622 rows to anything >1.
623
624 =item C<multiselect2side $id, %params>
625
626 Creates a JavaScript snippet calling the jQuery function
627 C<multiselect2side> on the select control with the ID C<$id>. The
628 select itself is not created. C<%params> can contain the following
629 entries:
630
631 =over 2
632
633 =item C<labelsx>
634
635 The label of the list of available options. Defaults to the
636 translation of 'Available'.
637
638 =item C<labeldx>
639
640 The label of the list of selected options. Defaults to the
641 translation of 'Selected'.
642
643 =back
644
645 =item C<sortable_element $selector, %params>
646
647 Makes the children of the DOM element C<$selector> (a jQuery selector)
648 sortable with the I<jQuery UI Selectable> library. The children can be
649 dragged & dropped around. After dropping an element an URL can be
650 postet to with the element IDs of the sorted children.
651
652 C<%params> can contain the following entries:
653
654 =over 2
655
656 =item C<url>
657
658 The URL to POST an AJAX request to after a dragged element has been
659 dropped. The AJAX request's return value is ignored. If given then
660 C<$params{with}> must be given as well.
661
662 =item C<with>
663
664 A string that is interpreted as the prefix of the children's ID. Upon
665 POSTing the result each child whose ID starts with C<$params{with}> is
666 considered. The prefix and the following "_" is removed from the
667 ID. The remaining parts of the IDs of those children are posted as a
668 single array parameter. The array parameter's name is either
669 C<$params{as}> or, missing that, C<$params{with}>.
670
671 =item C<as>
672
673 Sets the POST parameter name for AJAX request after dropping an
674 element (see C<$params{with}>).
675
676 =back
677
678 Example:
679
680   <table id="thing_list">
681     <thead>
682       <tr><td>This</td><td>That</td></tr>
683     </thead>
684     <tbody>
685       <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
686       <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
687       <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
688     </tbody>
689   <table>
690
691   [% L.sortable_element('#thing_list tbody',
692                         'url'  => 'controller.pl?action=SystemThings/reorder',
693                         'with' => 'thingy',
694                         'as'   => 'thing_ids') %]
695
696 After dropping e.g. the third element at the top of the list a POST
697 request would be made to the C<reorder> action of the C<SystemThings>
698 controller with a single parameter called C<thing_ids> -- an array
699 containing the values C<[ 6, 2, 15 ]>.
700
701 =item C<dump REF>
702
703 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
704
705 =back
706
707 =head2 CONVERSION FUNCTIONS
708
709 =over 4
710
711 =item C<options_for_select \@collection, %options>
712
713 Creates a string suitable for a HTML 'select' tag consisting of one
714 'E<lt>optionE<gt>' tag for each element in C<\@collection>. The value
715 to use and the title to display are extracted from the elements in
716 C<\@collection>. Each element can be one of four things:
717
718 =over 12
719
720 =item 1. An array reference with at least two elements. The first element is
721 the value, the second element is its title.
722
723 =item 2. A scalar. The scalar is both the value and the title.
724
725 =item 3. A hash reference. In this case C<%options> must contain
726 I<value> and I<title> keys that name the keys in the element to use
727 for the value and title respectively.
728
729 =item 4. A blessed reference. In this case C<%options> must contain
730 I<value> and I<title> keys that name functions called on the blessed
731 reference whose return values are used as the value and title
732 respectively.
733
734 =back
735
736 For cases 3 and 4 C<$options{value}> defaults to C<id> and
737 C<$options{title}> defaults to C<$options{value}>.
738
739 In addition to pure keys/method you can also provide coderefs as I<value_sub>
740 and/or I<title_sub>. If present, these take precedence over keys or methods,
741 and are called with the element as first argument. It must return the value or
742 title.
743
744 Lastly a joint coderef I<value_title_sub> may be provided, which in turn takes
745 precedence over each individual sub. It will only be called once for each
746 element and must return a list of value and title.
747
748 If the option C<with_empty> is set then an empty element (value
749 C<undef>) will be used as the first element. The title to display for
750 this element can be set with the option C<empty_title> and defaults to
751 an empty string.
752
753 The option C<default> can be either a scalar or an array reference
754 containing the values of the options which should be set to be
755 selected.
756
757 =item C<tab, description, target, %PARAMS>
758
759 Creates a tab for C<tabbed>. The description will be used as displayed name.
760 The target should be a block or template that can be processed. C<tab> supports
761 a C<method> parameter, which can override the process method to apply target.
762 C<method => 'raw'> will just include the given text as is. I was too lazy to
763 implement C<include> properly.
764
765 Also an C<if> attribute is supported, so that tabs can be suppressed based on
766 some occasion. In this case the supplied block won't even get processed, and
767 the resulting tab will get ignored by C<tabbed>:
768
769   L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
770
771 =back
772
773 =head1 MODULE AUTHORS
774
775 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
776
777 L<http://linet-services.de>