test action
[kivitendo-erp.git] / SL / Presenter / Tag.pm
1 package SL::Presenter::Tag;
2
3 use strict;
4
5 use SL::HTML::Restrict;
6 use SL::Presenter::EscapedText qw(escape);
7 use Scalar::Util qw(blessed);
8
9 use Exporter qw(import);
10 our @EXPORT_OK = qw(
11   html_tag input_tag hidden_tag javascript man_days_tag name_to_id select_tag
12   checkbox_tag button_tag submit_tag ajax_submit_tag input_number_tag
13   stringify_attributes restricted_html textarea_tag link_tag date_tag
14   div_tag radio_button_tag img_tag);
15 our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
16
17 use Carp;
18
19 my %_valueless_attributes = map { $_ => 1 } qw(
20   checked compact declare defer disabled ismap multiple noresize noshade nowrap
21   readonly selected hidden
22 );
23
24 my %_singleton_tags = map { $_ => 1 } qw(
25   area base br col command embed hr img input keygen link meta param source
26   track wbr
27 );
28
29 sub _call_on {
30   my ($object, $method, @params) = @_;
31   return $object->$method(@params);
32 }
33
34 { # This will give you an id for identifying html tags and such.
35   # It's guaranteed to be unique unless you exceed 10 mio calls per request.
36   # Do not use these id's to store information across requests.
37 my $_id_sequence = int rand 1e7;
38 sub _id {
39   return ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
40 }
41 }
42
43 sub _J {
44   my $string = shift;
45   $string    =~ s/(\"|\'|\\)/\\$1/g;
46   return $string;
47 }
48
49 sub join_values {
50   my ($name, $value) = @_;
51   my $spacer = $name eq 'class' ? ' ' : ''; # join classes with spaces, everything else as is
52
53   ref $value && 'ARRAY' eq ref $value
54   ? join $spacer, map { join_values($name, $_) } @$value
55   : $value
56 }
57
58 sub stringify_attributes {
59   my (%params) = @_;
60
61   my @result = ();
62   while (my ($name, $value) = each %params) {
63     next unless $name;
64     next if $_valueless_attributes{$name} && !$value;
65     $value = '' if !defined($value);
66     $value = join_values($name, $value) if ref $value && 'ARRAY' eq ref $value;
67     push @result, $_valueless_attributes{$name} ? escape($name) : escape($name) . '="' . escape($value) . '"';
68   }
69
70   return @result ? ' ' . join(' ', @result) : '';
71 }
72
73 sub html_tag {
74   my ($tag, $content, %params) = @_;
75   my $attributes = stringify_attributes(%params);
76
77   return "<${tag}${attributes}>" if !defined($content) && $_singleton_tags{$tag};
78   return "<${tag}${attributes}>${content}</${tag}>";
79 }
80
81 sub input_tag {
82   my ($name, $value, %attributes) = @_;
83
84   _set_id_attribute(\%attributes, $name);
85   $attributes{type} ||= 'text';
86
87   html_tag('input', undef, %attributes, name => $name, value => $value);
88 }
89
90 sub hidden_tag {
91   my ($name, $value, %attributes) = @_;
92   input_tag($name, $value, %attributes, type => 'hidden');
93 }
94
95 sub man_days_tag {
96   my ($name, $object, %attributes) = @_;
97
98   my $size           =  delete($attributes{size})   || 5;
99   my $method         =  $name;
100   $method            =~ s/^.*\.//;
101
102   my $time_selection = input_tag("${name}_as_man_days_string", _call_on($object, "${method}_as_man_days_string"), %attributes, size => $size);
103   my $unit_selection = select_tag("${name}_as_man_days_unit",   [[ 'h', $::locale->text('h') ], [ 'man_day', $::locale->text('MD') ]],
104                                           %attributes, default => _call_on($object, "${method}_as_man_days_unit"));
105
106   return $time_selection . $unit_selection;
107 }
108
109 sub name_to_id {
110   my ($name) = @_;
111
112   $name =~ s/\[\+?\]/ _id() /ge; # give constructs with [] or [+] unique ids
113   $name =~ s/[^\w_]/_/g;
114   $name =~ s/_+/_/g;
115
116   return $name;
117 }
118
119 sub select_tag {
120   my ($name, $collection, %attributes) = @_;
121
122   _set_id_attribute(\%attributes, $name);
123
124   $collection         = [] if defined($collection) && !ref($collection) && ($collection eq '');
125
126   my $with_filter     = delete($attributes{with_filter});
127   my $fil_placeholder = delete($attributes{filter_placeholder});
128   my $value_key       = delete($attributes{value_key})   || 'id';
129   my $title_key       = delete($attributes{title_key})   || $value_key;
130   my $default_key     = delete($attributes{default_key}) || 'selected';
131   my $default_val_key = delete($attributes{default_value_key});
132   my $default_coll    = delete($attributes{default});
133
134   my $value_title_sub = delete($attributes{value_title_sub});
135
136   my $value_sub       = delete($attributes{value_sub});
137   my $title_sub       = delete($attributes{title_sub});
138   my $default_sub     = delete($attributes{default_sub});
139
140   my $with_empty      = delete($attributes{with_empty});
141   my $empty_title     = delete($attributes{empty_title});
142
143   my $with_optgroups  = delete($attributes{with_optgroups});
144
145   undef $default_key if $default_sub || $default_val_key;
146
147   my $normalize_entry = sub {
148     my ($type, $entry, $sub, $key) = @_;
149
150     return $sub->($entry) if $sub;
151
152     my $ref = ref($entry);
153
154     if ( !$ref ) {
155       return $entry if $type eq 'value' || $type eq 'title';
156       return 0;
157     }
158
159     if ( $ref eq 'ARRAY' ) {
160       return $entry->[ $type eq 'value' ? 0 : $type eq 'title' ? 1 : 2 ];
161     }
162
163     return $entry->{$key} if $ref  eq 'HASH';
164     return $entry->$key   if $type ne 'default' || $entry->can($key);
165     return undef;
166   };
167
168   my %selected;
169   if (defined($default_coll) && !ref $default_coll) {
170     %selected = ($default_coll => 1);
171
172   } elsif (ref($default_coll) eq 'HASH') {
173     %selected = %{ $default_coll };
174
175   } elsif ($default_coll) {
176     $default_coll = [ $default_coll ] unless 'ARRAY' eq ref $default_coll;
177
178     %selected = $default_val_key ? map({ ($normalize_entry->('value', $_, undef, $default_val_key) => 1) } @{ $default_coll })
179               :                    map({ ($_                                                       => 1) } @{ $default_coll });
180   }
181
182   my $list_to_code = sub {
183     my ($sub_collection) = @_;
184
185     if ('ARRAY' ne ref $sub_collection) {
186       $sub_collection = [ $sub_collection ];
187     }
188
189     my @options;
190     foreach my $entry ( @{ $sub_collection } ) {
191       my $value;
192       my $title;
193
194       if ( $value_title_sub ) {
195         ($value, $title) = @{ $value_title_sub->($entry) };
196       } else {
197
198         $value = $normalize_entry->('value', $entry, $value_sub, $value_key);
199         $title = $normalize_entry->('title', $entry, $title_sub, $title_key);
200       }
201
202       my $default = $default_key ? $normalize_entry->('default', $entry, $default_sub, $default_key) : 0;
203
204       push(@options, [$value, $title, $selected{$value} || $default]);
205     }
206
207     return join '', map { html_tag('option', escape($_->[1]), value => $_->[0], selected => $_->[2]) } @options;
208   };
209
210   my $code  = '';
211   $code    .= html_tag('option', escape($empty_title || ''), value => '') if $with_empty;
212
213   if (!$with_optgroups) {
214     $code .= $list_to_code->($collection);
215
216   } else {
217     $code .= join '', map {
218       my ($optgroup_title, $sub_collection) = @{ $_ };
219       html_tag('optgroup', $list_to_code->($sub_collection), label => $optgroup_title)
220     } @{ $collection };
221   }
222
223   my $select_html = html_tag('select', $code, %attributes, name => $name);
224
225   if ($with_filter) {
226     my $input_style;
227
228     if (($attributes{style} // '') =~ m{width: *(\d+) *px}i) {
229       $input_style = "width: " . ($1 - 22) . "px";
230     }
231
232     my $input_html = html_tag(
233       'input', undef,
234       autocomplete     => 'off',
235       type             => 'text',
236       id               => $attributes{id} . '_filter',
237       'data-select-id' => $attributes{id},
238       (placeholder     => $fil_placeholder) x !!$fil_placeholder,
239       (style           => $input_style)     x !!$input_style,
240     );
241     $select_html = html_tag('div', $input_html . $select_html, class => "filtered_select");
242   }
243
244   return $select_html;
245 }
246
247 sub checkbox_tag {
248   my ($name, %attributes) = @_;
249
250   _set_id_attribute(\%attributes, $name);
251
252   $attributes{value}   = 1 unless defined $attributes{value};
253   my $label            = delete $attributes{label};
254   my $checkall         = delete $attributes{checkall};
255   my $for_submit       = delete $attributes{for_submit};
256
257   if ($attributes{checked}) {
258     $attributes{checked} = 'checked';
259   } else {
260     delete $attributes{checked};
261   }
262
263   my $code  = '';
264   $code    .= hidden_tag($name, 0, %attributes, id => $attributes{id} . '_hidden') if $for_submit;
265   $code    .= html_tag('input', undef,  %attributes, name => $name, type => 'checkbox');
266   $code    .= html_tag('label', $label, for => $attributes{id}) if $label;
267   $code    .= javascript(qq|\$('#$attributes{id}').checkall('$checkall');|) if $checkall;
268
269   return $code;
270 }
271
272 sub radio_button_tag {
273   my ($name, %attributes) = @_;
274
275   $attributes{value}   = 1 unless exists $attributes{value};
276
277   _set_id_attribute(\%attributes, $name, 1);
278   my $label            = delete $attributes{label};
279
280   _set_id_attribute(\%attributes, $name . '_' . $attributes{value});
281
282   if ($attributes{checked}) {
283     $attributes{checked} = 'checked';
284   } else {
285     delete $attributes{checked};
286   }
287
288   my $code  = html_tag('input', undef,  %attributes, name => $name, type => 'radio');
289   $code    .= html_tag('label', $label, for => $attributes{id}) if $label;
290
291   return $code;
292 }
293
294 sub button_tag {
295   my ($onclick, $value, %attributes) = @_;
296
297   _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
298   $attributes{type} ||= 'button';
299
300   $onclick = 'if (!confirm("'. _J(delete($attributes{confirm})) .'")) return false; ' . $onclick if $attributes{confirm};
301
302   html_tag('input', undef, %attributes, value => $value, (onclick => $onclick)x!!$onclick);
303 }
304
305 sub submit_tag {
306   my ($name, $value, %attributes) = @_;
307
308   _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
309
310   if ( $attributes{confirm} ) {
311     $attributes{onclick} = 'return confirm("'. _J(delete($attributes{confirm})) .'");';
312   }
313
314   input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
315 }
316
317 sub ajax_submit_tag {
318   my ($url, $form_selector, $text, %attributes) = @_;
319
320   $url           = _J($url);
321   $form_selector = _J($form_selector);
322   my $onclick    = qq|kivi.submit_ajax_form('${url}', '${form_selector}')|;
323
324   button_tag($onclick, $text, %attributes);
325 }
326
327 sub input_number_tag {
328   my ($name, $value, %params) = @_;
329
330   _set_id_attribute(\%params, $name);
331   my @onchange = $params{onchange} ? (onChange => delete $params{onchange}) : ();
332   my @classes  = ('numeric');
333   push @classes, delete($params{class}) if $params{class};
334   my %class    = @classes ? (class => join(' ', @classes)) : ();
335
336   $::request->layout->add_javascripts('kivi.Validator.js');
337   $::request->presenter->need_reinit_widgets($params{id});
338
339   input_tag(
340     $name, $::form->format_amount(\%::myconfig, $value, $params{precision}),
341     "data-validate" => "number",
342     %params,
343     %class, @onchange,
344   );
345 }
346
347
348 sub javascript {
349   my ($data) = @_;
350   html_tag('script', $data, type => 'text/javascript');
351 }
352
353 sub _set_id_attribute {
354   my ($attributes, $name, $unique) = @_;
355
356   if (!delete($attributes->{no_id}) && !$attributes->{id}) {
357     $attributes->{id}  = name_to_id($name);
358     $attributes->{id} .= '_' . $attributes->{value} if $unique;
359   }
360
361   %{ $attributes };
362 }
363
364 my $html_restricter;
365
366 sub restricted_html {
367   my ($value) = @_;
368
369   $html_restricter ||= SL::HTML::Restrict->create;
370   return $html_restricter->process($value);
371 }
372
373 sub textarea_tag {
374   my ($name, $content, %attributes) = @_;
375
376   _set_id_attribute(\%attributes, $name);
377   $attributes{rows}  *= 1; # required by standard
378   $attributes{cols}  *= 1; # required by standard
379
380   html_tag('textarea', $content, %attributes, name => $name);
381 }
382
383 sub link_tag {
384   my ($href, $content, %params) = @_;
385
386   $href ||= '#';
387
388   html_tag('a', $content, %params, href => $href);
389 }
390 # alias for compatibility
391 sub link { goto &link_tag }
392
393 sub date_tag {
394   my ($name, $value, %params) = @_;
395
396   _set_id_attribute(\%params, $name);
397   my @onchange = $params{onchange} ? (onChange => delete $params{onchange}) : ();
398   my @classes  = $params{no_cal} || $params{readonly} ? () : ('datepicker');
399   push @classes, delete($params{class}) if $params{class};
400   my %class    = @classes ? (class => join(' ', @classes)) : ();
401
402   $::request->layout->add_javascripts('kivi.Validator.js');
403   $::request->presenter->need_reinit_widgets($params{id});
404
405   $params{'data-validate'} = join(' ', "date", grep { $_ } (delete $params{'data-validate'}));
406
407   input_tag(
408     $name, blessed($value) ? $value->to_lxoffice : $value,
409     size   => 11,
410     %params,
411     %class, @onchange,
412   );
413 }
414
415 sub div_tag {
416   my ($content, %params) = @_;
417   return html_tag('div', $content, %params);
418 }
419
420 sub img_tag {
421   my (%params) = @_;
422
423   $params{alt} ||= '';
424
425   return html_tag('img', undef, %params);
426 }
427
428 1;
429 __END__
430
431 =pod
432
433 =encoding utf8
434
435 =head1 NAME
436
437 SL::Presenter::Tag - Layouting / tag generation
438
439 =head1 SYNOPSIS
440
441 Usage in a template:
442
443   [% USE P %]
444
445   [% P.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right', 1 ] ]) %]
446
447   [% P.select_tag('direction', [ { direction => 'left',  display => 'To the left'  },
448                                  { direction => 'right', display => 'To the right' } ],
449                                value_key => 'direction', title_key => 'display', default => 'right') %]
450
451   [% P.select_tag('direction', [ { direction => 'left',  display => 'To the left'  },
452                                  { direction => 'right', display => 'To the right', selected => 1 } ],
453                                value_key => 'direction', title_key => 'display') %]
454
455   # Use an RDBO object and its n:m relationship as the default
456   # values. For example, a user can be a member of many groups. "All
457   # groups" is therefore the full collection and "$user->groups" is a
458   # list of RDBO AuthGroup objects whose IDs must match the ones in
459   # "All groups". This could look like the following:
460   [% P.select_tag('user.groups[]', SELF.all_groups, multiple=1,
461                   default=SELF.user.groups, default_value_key='id' ) %]
462
463 =head1 DESCRIPTION
464
465 A module modeled a bit after Rails' ActionView helpers. Several small
466 functions that create HTML tags from various kinds of data sources.
467
468 The C<id> attribute is usually calculated automatically. This can be
469 overridden by either specifying an C<id> attribute or by setting
470 C<no_id> to trueish.
471
472 =head1 FUNCTIONS
473
474 =head2 LOW-LEVEL FUNCTIONS
475
476 =over 4
477
478 =item C<html_tag $tag_name, $content_string, %attributes>
479
480 Creates an opening and closing HTML tag for C<$tag_name> and puts
481 C<$content_string> between the two. If C<$content_string> is undefined
482 or empty then only a E<lt>tag/E<gt> tag will be created. Attributes
483 are key/value pairs added to the opening tag.
484
485 C<$content_string> is not HTML escaped.
486
487 =item C<name_to_id $name>
488
489 Converts a name to a HTML id by replacing various characters.
490
491 =item C<stringify_attributes %items>
492
493 Creates a string from all elements in C<%items> suitable for usage as
494 HTML tag attributes. Keys and values are HTML escaped even though keys
495 must not contain non-ASCII characters for browsers to accept them.
496
497 =item C<restricted_html $html>
498
499 Returns HTML stripped of unknown tags. See L<SL::HTML::Restrict>.
500
501 =back
502
503 =head2 HIGH-LEVEL FUNCTIONS
504
505 =over 4
506
507 =item C<input_tag $name, $value, %attributes>
508
509 Creates a HTML 'input type=text' tag named C<$name> with the value
510 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
511 tag's C<id> defaults to C<name_to_id($name)>.
512
513 =item C<submit_tag $name, $value, %attributes>
514
515 Creates a HTML 'input type=submit class=submit' tag named C<$name> with the
516 value C<$value> and with arbitrary HTML attributes from C<%attributes>. The
517 tag's C<id> defaults to C<name_to_id($name)>.
518
519 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
520 be added via the C<onclick> handler asking the question given with
521 C<$attributes{confirm}>. The request is only submitted if the user
522 clicks the dialog's ok/yes button.
523
524 =item C<ajax_submit_tag $url, $form_selector, $text, %attributes>
525
526 Creates a HTML 'input type="button"' tag with a very specific onclick
527 handler that submits the form given by the jQuery selector
528 C<$form_selector> to the URL C<$url> (the actual JavaScript function
529 called for that is C<kivi.submit_ajax_form()> in
530 C<js/client_js.js>). The button's label will be C<$text>.
531
532 =item C<button_tag $onclick, $text, %attributes>
533
534 Creates a HTML 'input type="button"' tag with an onclick handler
535 C<$onclick> and a value of C<$text>. The button does not have a name
536 nor an ID by default.
537
538 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
539 be prepended to the C<$onclick> handler asking the question given with
540 C<$attributes{confirm}>. The request is only submitted if the user
541 clicks the dialog's "ok/yes" button.
542
543 =item C<man_days_tag $name, $object, %attributes>
544
545 Creates two HTML inputs: a text input for entering a number and a drop
546 down box for chosing the unit (either 'man days' or 'hours').
547
548 C<$object> must be a L<Rose::DB::Object> instance using the
549 L<SL::DB::Helper::AttrDuration> helper.
550
551 C<$name> is supposed to be the name of the underlying column,
552 e.g. C<time_estimation> for an instance of
553 C<SL::DB::RequirementSpecItem>. If C<$name> has the form
554 C<prefix.method> then the full C<$name> is used for the input's base
555 names while the methods called on C<$object> are only the suffix. This
556 makes it possible to write statements like e.g.
557
558   [% P.man_days_tag("requirement_spec_item.time_estimation", SELF.item) %]
559
560 The attribute C<size> can be used to set the text input's size. It
561 defaults to 5.
562
563 =item C<hidden_tag $name, $value, %attributes>
564
565 Creates a HTML 'input type=hidden' tag named C<$name> with the value
566 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
567 tag's C<id> defaults to C<name_to_id($name)>.
568
569 =item C<checkbox_tag $name, %attributes>
570
571 Creates a HTML 'input type=checkbox' tag named C<$name> with arbitrary
572 HTML attributes from C<%attributes>. The tag's C<id> defaults to
573 C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
574
575 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
576 created with said C<label>. No attribute named C<label> is created in
577 that case.
578
579 If C<%attributes> contains a key C<checkall> then the value is taken as a
580 JQuery selector and clicking this checkbox will also toggle all checkboxes
581 matching the selector.
582
583 =item C<radio_button_tag $name, %attributes>
584
585 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
586 HTML attributes from C<%attributes>. The tag's C<value> defaults to
587 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
588
589 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
590 created with said C<label>. No attribute named C<label> is created in
591 that case.
592
593 =item C<select_tag $name, \@collection, %attributes>
594
595 Creates an HTML 'select' tag named C<$name> with the contents of one
596 'E<lt>optionE<gt>' tag for each element in C<\@collection> and with arbitrary
597 HTML attributes from C<%attributes>. The value
598 to use and the title to display are extracted from the elements in
599 C<\@collection>. Each element can be one of four things:
600
601 =over 12
602
603 =item 1. An array reference with at least two elements. The first element is
604 the value, the second element is its title. The third element is optional and and should contain a boolean.
605 If it is true, than the element will be used as default.
606
607 =item 2. A scalar. The scalar is both the value and the title.
608
609 =item 3. A hash reference. In this case C<%attributes> must contain
610 I<value_key>, I<title_key> and may contain I<default_key> keys that name the keys in the element to use
611 for the value, title and default respectively.
612
613 =item 4. A blessed reference. In this case C<%attributes> must contain
614 I<value_key>, I<title_key> and may contain I<default_key> keys that name functions called on the blessed
615 reference whose return values are used as the value, title and default
616 respectively.
617
618 =back
619
620 For cases 3 and 4 C<$attributes{value_key}> defaults to C<id>,
621 C<$attributes{title_key}> defaults to C<$attributes{value_key}> and
622 C<$attributes{default_key}> defaults to C<selected>. Note that
623 C<$attributes{default_key}> is set to C<undef> if
624 C<$attributes{default_value_key}> is used as well (see below).
625
626 In addition to pure keys/method you can also provide coderefs as I<value_sub>
627 and/or I<title_sub> and/or I<default_sub>. If present, these take precedence over keys or methods,
628 and are called with the element as first argument. It must return the value, title or default.
629
630 Lastly a joint coderef I<value_title_sub> may be provided, which in turn takes
631 precedence over the C<value_sub> and C<title_sub> subs. It will only be called once for each
632 element and must return a list of value and title.
633
634 If the option C<with_empty> is set then an empty element (value
635 C<undef>) will be used as the first element. The title to display for
636 this element can be set with the option C<empty_title> and defaults to
637 an empty string.
638
639 The tag's C<id> defaults to C<name_to_id($name)>.
640
641 The option C<default> can be quite a lot of things:
642
643 =over 4
644
645 =item 1. A scalar value. This is the value of the entry that's
646 selected by default.
647
648 =item 2. A hash reference for C<multiple=1>. Whether or not an entry
649 is selected by default is looked up in this hash.
650
651 =item 3. An array reference containing scalar values. Same as 1., just
652 for the case of C<multiple=1>.
653
654 =item 4. If C<default_value_key> is given: an array reference of hash
655 references. For each hash reference the value belonging to the key
656 C<default_value_key> is treated as one value to select by
657 default. Constructs a hash that's treated like 3.
658
659 =item 5. If C<default_value_key> is given: an array reference of
660 blessed objects. For each object the value returne from calling the
661 function named C<default_value_key> on the object is treated as one
662 value to select by default. Constructs a hash that's treated like 3.
663
664 =back
665
666 5. also applies to single RDBO instances (due to 'wantarray'
667 shenanigans assigning RDBO's relationships to a hash key will result
668 in a single RDBO object being assigned instead of an array reference
669 containing that single RDBO object).
670
671 If the option C<with_optgroups> is set then this function expects
672 C<\@collection> to be one level deeper. The upper-most level is
673 translated into an HTML C<optgroup> tag. So the structure becomes:
674
675 =over 4
676
677 =item 1. Array of array references. Each element in the
678 C<\@collection> is converted into an optgroup.
679
680 =item 2. The optgroup's C<label> attribute will be set to the
681 first element in the array element. The second array element is then
682 converted to a list of C<option> tags as described above.
683
684 =back
685
686 Example for use of optgroups:
687
688   # First in a controller:
689   my @collection = (
690     [ t8("First optgroup with three items"),
691       [ { id => 42, name => "item one" },
692         { id => 54, name => "second item" },
693         { id => 23, name => "and the third one" },
694       ] ],
695     [ t8("Another optgroup, with a lot of items from Rose"),
696       SL::DB::Manager::Customer->get_all_sorted ],
697   );
698
699   # Later in the template:
700   [% L.select_tag('the_selection', COLLECTION, with_optgroups=1, title_key='name') %]
701
702 =back
703
704 =head1 BUGS
705
706 Nothing here yet.
707
708 =head1 AUTHOR
709
710 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
711 Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
712
713 =cut