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