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