844c88dd296b914a637cda8fa4224070ecd832e8
[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    = shift;
59   my %options = _hashify(@_);
60
61   my @result = ();
62   while (my ($name, $value) = each %options) {
63     next unless $name;
64     $value = '' if !defined($value);
65     push @result, _H($name) . '="' . _H($value) . '"';
66   }
67
68   return @result ? ' ' . join(' ', @result) : '';
69 }
70
71 sub html_tag {
72   my $self       = shift;
73   my $tag        = shift;
74   my $content    = shift;
75   my $attributes = $self->attributes(@_);
76
77   return "<${tag}${attributes}/>" unless defined($content);
78   return "<${tag}${attributes}>${content}</${tag}>";
79 }
80
81 sub select_tag {
82   my $self            = shift;
83   my $name            = shift;
84   my $options_str     = shift;
85   my %attributes      = _hashify(@_);
86
87   $attributes{id}   ||= $self->name_to_id($name);
88   $options_str        = $self->options_for_select($options_str) if ref $options_str;
89
90   return $self->html_tag('select', $options_str, %attributes, name => $name);
91 }
92
93 sub textarea_tag {
94   my ($self, $name, $content, @slurp) = @_;
95   my %attributes      = _hashify(@slurp);
96
97   $attributes{id}   ||= $self->name_to_id($name);
98   $content            = $content ? _H($content) : '';
99
100   return $self->html_tag('textarea', $content, %attributes, name => $name);
101 }
102
103 sub checkbox_tag {
104   my $self             = shift;
105   my $name             = shift;
106   my %attributes       = _hashify(@_);
107
108   $attributes{id}    ||= $self->name_to_id($name);
109   $attributes{value}   = 1 unless defined $attributes{value};
110   my $label            = delete $attributes{label};
111
112   if ($attributes{checked}) {
113     $attributes{checked} = 'checked';
114   } else {
115     delete $attributes{checked};
116   }
117
118   my $code  = $self->html_tag('input', undef,  %attributes, name => $name, type => 'checkbox');
119   $code    .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
120
121   return $code;
122 }
123
124 sub radio_button_tag {
125   my $self             = shift;
126   my $name             = shift;
127   my %attributes       = _hashify(@_);
128
129   $attributes{value}   = 1 unless defined $attributes{value};
130   $attributes{id}    ||= $self->name_to_id($name . "_" . $attributes{value});
131   my $label            = delete $attributes{label};
132
133   if ($attributes{checked}) {
134     $attributes{checked} = 'checked';
135   } else {
136     delete $attributes{checked};
137   }
138
139   my $code  = $self->html_tag('input', undef,  %attributes, name => $name, type => 'radio');
140   $code    .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
141
142   return $code;
143 }
144
145 sub input_tag {
146   my ($self, $name, $value, @slurp) = @_;
147   my %attributes      = _hashify(@slurp);
148
149   $attributes{id}   ||= $self->name_to_id($name);
150   $attributes{type} ||= 'text';
151
152   return $self->html_tag('input', undef, %attributes, name => $name, value => $value);
153 }
154
155 sub hidden_tag {
156   return shift->input_tag(@_, type => 'hidden');
157 }
158
159 sub div_tag {
160   my ($self, $content, @slurp) = @_;
161   return $self->html_tag('div', $content, @slurp);
162 }
163
164 sub ul_tag {
165   my ($self, $content, @slurp) = @_;
166   return $self->html_tag('ul', $content, @slurp);
167 }
168
169 sub li_tag {
170   my ($self, $content, @slurp) = @_;
171   return $self->html_tag('li', $content, @slurp);
172 }
173
174 sub link {
175   my ($self, $href, $content, @slurp) = @_;
176   my %params = _hashify(@slurp);
177
178   $href ||= '#';
179
180   return $self->html_tag('a', $content, %params, href => $href);
181 }
182
183 sub submit_tag {
184   my ($self, $name, $value, @slurp) = @_;
185   my %attributes = _hashify(@slurp);
186
187   $attributes{onclick} = "if (confirm('" . delete($attributes{confirm}) . "')) return true; else return false;" if $attributes{confirm};
188
189   return $self->input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
190 }
191
192 sub button_tag {
193   my ($self, $onclick, $value, @slurp) = @_;
194   my %attributes = _hashify(@slurp);
195
196   return $self->input_tag(undef, $value, %attributes, type => 'button', onclick => $onclick);
197 }
198
199 sub options_for_select {
200   my $self            = shift;
201   my $collection      = shift;
202   my %options         = _hashify(@_);
203
204   my $value_key       = $options{value} || 'id';
205   my $title_key       = $options{title} || $value_key;
206
207   my $value_sub       = $options{value_sub};
208   my $title_sub       = $options{title_sub};
209
210   my $value_title_sub = $options{value_title_sub};
211
212   my %selected        = map { ( $_ => 1 ) } @{ ref($options{default}) eq 'ARRAY' ? $options{default} : $options{default} ? [ $options{default} ] : [] };
213
214   my $access = sub {
215     my ($element, $index, $key, $sub) = @_;
216     my $ref = ref $element;
217     return  $sub            ? $sub->($element)
218          : !$ref            ? $element
219          :  $ref eq 'ARRAY' ? $element->[$index]
220          :  $ref eq 'HASH'  ? $element->{$key}
221          :                    $element->$key;
222   };
223
224   my @elements = ();
225   push @elements, [ undef, $options{empty_title} || '' ] if $options{with_empty};
226   push @elements, map [
227     $value_title_sub ? $value_title_sub->($_) : (
228       $access->($_, 0, $value_key, $value_sub),
229       $access->($_, 1, $title_key, $title_sub),
230     )
231   ], @{ $collection } if $collection && ref $collection eq 'ARRAY';
232
233   my $code = '';
234   foreach my $result (@elements) {
235     my %attributes = ( value => $result->[0] );
236     $attributes{selected} = 'selected' if $selected{ $result->[0] || '' };
237
238     $code .= $self->html_tag('option', _H($result->[1]), %attributes);
239   }
240
241   return $code;
242 }
243
244 sub javascript {
245   my ($self, $data) = @_;
246   return $self->html_tag('script', $data, type => 'text/javascript');
247 }
248
249 sub stylesheet_tag {
250   my $self = shift;
251   my $code = '';
252
253   foreach my $file (@_) {
254     $file .= '.css'        unless $file =~ m/\.css$/;
255     $file  = "css/${file}" unless $file =~ m|/|;
256
257     $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
258   }
259
260   return $code;
261 }
262
263 sub date_tag {
264   my ($self, $name, $value, @slurp) = @_;
265   my %params   = _hashify(@slurp);
266   my $name_e   = _H($name);
267   my $seq      = _tag_id();
268   my $datefmt  = apply {
269     s/d+/\%d/gi;
270     s/m+/\%m/gi;
271     s/y+/\%Y/gi;
272   } $::myconfig{"dateformat"};
273
274   $params{cal_align} ||= 'BR';
275
276   $self->input_tag($name, $value,
277     id     => $name_e,
278     size   => 11,
279     title  => _H($::myconfig{dateformat}),
280     onBlur => 'check_right_date_format(this)',
281     %params,
282   ) . ((!$params{no_cal}) ?
283   $self->html_tag('img', undef,
284     src    => 'image/calendar.png',
285     id     => "trigger$seq",
286     title  => _H($::myconfig{dateformat}),
287     %params,
288   ) .
289   $self->javascript(
290     "Calendar.setup({ inputField: '$name_e', ifFormat: '$datefmt', align: '$params{cal_align}', button: 'trigger$seq' });"
291   ) : '');
292
293 sub javascript_tag {
294   my $self = shift;
295   my $code = '';
296
297   foreach my $file (@_) {
298     $file .= '.js'        unless $file =~ m/\.js$/;
299     $file  = "js/${file}" unless $file =~ m|/|;
300
301     $code .= qq|<script type="text/javascript" src="${file}"></script>|;
302   }
303
304   return $code;
305 }
306
307 sub tabbed {
308   my ($self, $tabs, @slurp) = @_;
309   my %params   = _hashify(@slurp);
310   my $id       = 'tab_' . _tag_id();
311
312   $params{selected} *= 1;
313
314   die 'L.tabbed needs an arrayred of tabs for first argument'
315     unless ref $tabs eq 'ARRAY';
316
317   my (@header, @blocks);
318   for my $i (0..$#$tabs) {
319     my $tab = $tabs->[$i];
320
321     next if $tab eq '';
322
323     my $selected = $params{selected} == $i;
324     my $tab_id = _tag_id();
325     push @header, $self->li_tag(
326       $self->link('', $tab->{name}, rel => $tab_id),
327         ($selected ? (class => 'selected') : ())
328     );
329     push @blocks, $self->div_tag($tab->{data},
330       id => $tab_id, class => 'tabcontent');
331   }
332
333   return '' unless @header;
334   return $self->ul_tag(
335     join('', @header), id => $id, class => 'shadetabs'
336   ) .
337   $self->div_tag(
338     join('', @blocks), class => 'tabcontentstyle'
339   ) .
340   $self->javascript(
341     qq|var $id = new ddtabcontent("$id");$id.setpersist(true);| .
342     qq|$id.setselectedClassTarget("link");$id.init();|
343   );
344 }
345
346 sub tab {
347   my ($self, $name, $src, @slurp) = @_;
348   my %params = _hashify(@slurp);
349
350   $params{method} ||= 'process';
351
352   return () if defined $params{if} && !$params{if};
353
354   my $data;
355   if ($params{method} eq 'raw') {
356     $data = $src;
357   } elsif ($params{method} eq 'process') {
358     $data = $self->_context->process($src, %{ $params{args} || {} });
359   } else {
360     die "unknown tag method '$params{method}'";
361   }
362
363   return () unless $data;
364
365   return +{ name => $name, data => $data };
366 }
367
368 sub areainput_tag {
369   my ($self, $name, $value, @slurp) = @_;
370   my %attributes      = _hashify(@slurp);
371
372   my $rows = delete $attributes{rows}     || 1;
373   my $min  = delete $attributes{min_rows} || 1;
374
375   return $rows > 1
376     ? $self->textarea_tag($name, $value, %attributes, rows => max $rows, $min)
377     : $self->input_tag($name, $value, %attributes);
378 }
379
380 1;
381
382 __END__
383
384 =head1 NAME
385
386 SL::Templates::Plugin::L -- Layouting / tag generation
387
388 =head1 SYNOPSIS
389
390 Usage from a template:
391
392   [% USE L %]
393
394   [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right' ] ]) %]
395
396   [% L.select_tag('direction', L.options_for_select([ { direction => 'left',  display => 'To the left'  },
397                                                       { direction => 'right', display => 'To the right' } ],
398                                                     value => 'direction', title => 'display', default => 'right')) %]
399
400 =head1 DESCRIPTION
401
402 A module modeled a bit after Rails' ActionView helpers. Several small
403 functions that create HTML tags from various kinds of data sources.
404
405 =head1 FUNCTIONS
406
407 =head2 LOW-LEVEL FUNCTIONS
408
409 =over 4
410
411 =item C<name_to_id $name>
412
413 Converts a name to a HTML id by replacing various characters.
414
415 =item C<attributes %items>
416
417 Creates a string from all elements in C<%items> suitable for usage as
418 HTML tag attributes. Keys and values are HTML escaped even though keys
419 must not contain non-ASCII characters for browsers to accept them.
420
421 =item C<html_tag $tag_name, $content_string, %attributes>
422
423 Creates an opening and closing HTML tag for C<$tag_name> and puts
424 C<$content_string> between the two. If C<$content_string> is undefined
425 or empty then only a E<lt>tag/E<gt> tag will be created. Attributes
426 are key/value pairs added to the opening tag.
427
428 C<$content_string> is not HTML escaped.
429
430 =back
431
432 =head2 HIGH-LEVEL FUNCTIONS
433
434 =over 4
435
436 =item C<select_tag $name, $options_string, %attributes>
437
438 Creates a HTML 'select' tag named C<$name> with the contents
439 C<$options_string> and with arbitrary HTML attributes from
440 C<%attributes>. The tag's C<id> defaults to C<name_to_id($name)>.
441
442 The C<$options_string> is usually created by the
443 L</options_for_select> function. If C<$options_string> is an array
444 reference then it will be passed to L</options_for_select>
445 automatically.
446
447 =item C<input_tag $name, $value, %attributes>
448
449 Creates a HTML 'input type=text' tag named C<$name> with the value
450 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
451 tag's C<id> defaults to C<name_to_id($name)>.
452
453 =item C<hidden_tag $name, $value, %attributes>
454
455 Creates a HTML 'input type=hidden' tag named C<$name> with the value
456 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
457 tag's C<id> defaults to C<name_to_id($name)>.
458
459 =item C<submit_tag $name, $value, %attributes>
460
461 Creates a HTML 'input type=submit class=submit' tag named C<$name> with the
462 value C<$value> and with arbitrary HTML attributes from C<%attributes>. The
463 tag's C<id> defaults to C<name_to_id($name)>.
464
465 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
466 be added via the C<onclick> handler asking the question given with
467 C<$attributes{confirm}>. If request is only submitted if the user
468 clicks the dialog's ok/yes button.
469
470 =item C<textarea_tag $name, $value, %attributes>
471
472 Creates a HTML 'textarea' tag named C<$name> with the content
473 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
474 tag's C<id> defaults to C<name_to_id($name)>.
475
476 =item C<checkbox_tag $name, %attributes>
477
478 Creates a HTML 'input type=checkbox' tag named C<$name> with arbitrary
479 HTML attributes from C<%attributes>. The tag's C<id> defaults to
480 C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
481
482 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
483 created with said C<label>. No attribute named C<label> is created in
484 that case.
485
486 =item C<date_tag $name, $value, cal_align =E<gt> $align_code, %attributes>
487
488 Creates a date input field, with an attached javascript that will open a
489 calendar on click. The javascript ist by default anchoered at the bottom right
490 sight. This can be overridden with C<cal_align>, see Calendar documentation for
491 the details, usually you'll want a two letter abbreviation of the alignment.
492 Right + Bottom becomes C<BL>.
493
494 =item C<radio_button_tag $name, %attributes>
495
496 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
497 HTML attributes from C<%attributes>. The tag's C<value> defaults to
498 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
499
500 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
501 created with said C<label>. No attribute named C<label> is created in
502 that case.
503
504 =item C<javascript_tag $file1, $file2, $file3...>
505
506 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
507 tag for each file name parameter passed. Each file name will be
508 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
509 doesn't contain a slash.
510
511 =item C<stylesheet_tag $file1, $file2, $file3...>
512
513 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
514 for each file name parameter passed. Each file name will be postfixed
515 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
516 contain a slash.
517
518 =item C<date_tag $name, $value, cal_align =E<gt> $align_code, %attributes>
519
520 Creates a date input field, with an attached javascript that will open a
521 calendar on click. The javascript ist by default anchoered at the bottom right
522 sight. This can be overridden with C<cal_align>, see Calendar documentation for
523 the details, usually you'll want a two letter abbreviation of the alignment.
524 Right + Bottom becomes C<BL>.
525
526 =item C<tabbed \@tab, %attributes>
527
528 Will create a tabbed area. The tabs should be created with the helper function
529 C<tab>. Example:
530
531   [% L.tabbed([
532     L.tab(LxERP.t8('Basic Data'),       'part/_main_tab.html'),
533     L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
534   ]) %]
535
536 An optional attribute is C<selected>, which accepts the ordinal of a tab which
537 should be selected by default.
538
539 =item C<areainput_tag $name, $content, %PARAMS>
540
541 Creates a generic input tag or textarea tag, depending on content size. The
542 mount of desired rows must be given with C<rows> parameter, Accpeted parameters
543 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
544
545 You can force input by setting rows to 1, and you can force textarea by setting
546 rows to anything >1.
547
548 =back
549
550 =head2 CONVERSION FUNCTIONS
551
552 =over 4
553
554 =item C<options_for_select \@collection, %options>
555
556 Creates a string suitable for a HTML 'select' tag consisting of one
557 'E<lt>optionE<gt>' tag for each element in C<\@collection>. The value
558 to use and the title to display are extracted from the elements in
559 C<\@collection>. Each element can be one of four things:
560
561 =over 12
562
563 =item 1. An array reference with at least two elements. The first element is
564 the value, the second element is its title.
565
566 =item 2. A scalar. The scalar is both the value and the title.
567
568 =item 3. A hash reference. In this case C<%options> must contain
569 I<value> and I<title> keys that name the keys in the element to use
570 for the value and title respectively.
571
572 =item 4. A blessed reference. In this case C<%options> must contain
573 I<value> and I<title> keys that name functions called on the blessed
574 reference whose return values are used as the value and title
575 respectively.
576
577 =back
578
579 For cases 3 and 4 C<$options{value}> defaults to C<id> and
580 C<$options{title}> defaults to C<$options{value}>.
581
582 In addition to pure keys/method you can also provide coderefs as I<value_sub>
583 and/or I<title_sub>. If present, these take precedence over keys or methods,
584 and are called with the element as first argument. It must return the value or
585 title.
586
587 Lastly a joint coderef I<value_title_sub> may be provided, which in turn takes
588 precedence over each individual sub. It will only be called once for each
589 element and must return a list of value and title.
590
591 If the option C<with_empty> is set then an empty element (value
592 C<undef>) will be used as the first element. The title to display for
593 this element can be set with the option C<empty_title> and defaults to
594 an empty string.
595
596 The option C<default> can be either a scalar or an array reference
597 containing the values of the options which should be set to be
598 selected.
599
600 =item C<tab, description, target, %PARAMS>
601
602 Creates a tab for C<tabbed>. The description will be used as displayed name.
603 The target should be a block or template that can be processed. C<tab> supports
604 a C<method> parameter, which can override the process method to apply target.
605 C<method => 'raw'> will just include the given text as is. I was too lazy to
606 implement C<include> properly.
607
608 Also an C<if> attribute is supported, so that tabs can be suppressed based on
609 some occasion. In this case the supplied block won't even get processed, and
610 the resulting tab will get ignored by C<tabbed>:
611
612   L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
613
614 =back
615
616 =head1 MODULE AUTHORS
617
618 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
619
620 L<http://linet-services.de>