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