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