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