1 package SL::Template::Plugin::L;
3 use base qw( Template::Plugin );
5 use List::MoreUtils qw(apply);
6 use List::Util qw(max);
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;
15 return $_id_sequence = ($_id_sequence + 1) % 1e7;
21 return $::locale->quote_special_chars('HTML', $string);
25 my $string = "" . shift;
26 $string =~ s/\"/\\\"/g;
31 return (@_ && (ref($_[0]) eq 'HASH')) ? %{ $_[0] } : @_;
35 my ($class, $context, @args) = @_;
43 die 'not an accessor' if @_ > 1;
44 return $_[0]->{CONTEXT};
51 $name =~ s/[^\w_]/_/g;
58 my ($self, @slurp) = @_;
59 my %options = _hashify(@slurp);
62 while (my ($name, $value) = each %options) {
64 next if $name eq 'disabled' && !$value;
65 $value = '' if !defined($value);
66 push @result, _H($name) . '="' . _H($value) . '"';
69 return @result ? ' ' . join(' ', @result) : '';
73 my ($self, $tag, $content, @slurp) = @_;
74 my $attributes = $self->attributes(@slurp);
76 return "<${tag}${attributes}/>" unless defined($content);
77 return "<${tag}${attributes}>${content}</${tag}>";
83 my $options_str = shift;
84 my %attributes = _hashify(@_);
86 $attributes{id} ||= $self->name_to_id($name);
87 $options_str = $self->options_for_select($options_str) if ref $options_str;
89 return $self->html_tag('select', $options_str, %attributes, name => $name);
93 my ($self, $name, $content, @slurp) = @_;
94 my %attributes = _hashify(@slurp);
96 $attributes{id} ||= $self->name_to_id($name);
97 $content = $content ? _H($content) : '';
99 return $self->html_tag('textarea', $content, %attributes, name => $name);
103 my ($self, $name, @slurp) = @_;
104 my %attributes = _hashify(@slurp);
106 $attributes{id} ||= $self->name_to_id($name);
107 $attributes{value} = 1 unless defined $attributes{value};
108 my $label = delete $attributes{label};
109 my $checkall = delete $attributes{checkall};
111 if ($attributes{checked}) {
112 $attributes{checked} = 'checked';
114 delete $attributes{checked};
117 my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'checkbox');
118 $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
119 $code .= $self->javascript(qq|\$('#$attributes{id}').checkall('$checkall');|) if $checkall;
124 sub radio_button_tag {
127 my %attributes = _hashify(@_);
129 $attributes{value} = 1 unless defined $attributes{value};
130 $attributes{id} ||= $self->name_to_id($name . "_" . $attributes{value});
131 my $label = delete $attributes{label};
133 if ($attributes{checked}) {
134 $attributes{checked} = 'checked';
136 delete $attributes{checked};
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;
146 my ($self, $name, $value, @slurp) = @_;
147 my %attributes = _hashify(@slurp);
149 $attributes{id} ||= $self->name_to_id($name);
150 $attributes{type} ||= 'text';
152 return $self->html_tag('input', undef, %attributes, name => $name, value => $value);
156 return shift->input_tag(@_, type => 'hidden');
160 my ($self, $content, @slurp) = @_;
161 return $self->html_tag('div', $content, @slurp);
165 my ($self, $content, @slurp) = @_;
166 return $self->html_tag('ul', $content, @slurp);
170 my ($self, $content, @slurp) = @_;
171 return $self->html_tag('li', $content, @slurp);
175 my ($self, $href, $content, @slurp) = @_;
176 my %params = _hashify(@slurp);
180 return $self->html_tag('a', $content, %params, href => $href);
184 my ($self, $name, $value, @slurp) = @_;
185 my %attributes = _hashify(@slurp);
187 $attributes{onclick} = "if (confirm('" . delete($attributes{confirm}) . "')) return true; else return false;" if $attributes{confirm};
189 return $self->input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
193 my ($self, $onclick, $value, @slurp) = @_;
194 my %attributes = _hashify(@slurp);
196 return $self->input_tag(undef, $value, %attributes, type => 'button', onclick => $onclick);
199 sub options_for_select {
201 my $collection = shift;
202 my %options = _hashify(@_);
204 my $value_key = $options{value} || 'id';
205 my $title_key = $options{title} || $value_key;
207 my $value_sub = $options{value_sub};
208 my $title_sub = $options{title_sub};
210 my $value_title_sub = $options{value_title_sub};
212 my %selected = map { ( $_ => 1 ) } @{ ref($options{default}) eq 'ARRAY' ? $options{default} : defined($options{default}) ? [ $options{default} ] : [] };
215 my ($element, $index, $key, $sub) = @_;
216 my $ref = ref $element;
217 return $sub ? $sub->($element)
219 : $ref eq 'ARRAY' ? $element->[$index]
220 : $ref eq 'HASH' ? $element->{$key}
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),
231 ], @{ $collection } if $collection && ref $collection eq 'ARRAY';
234 foreach my $result (@elements) {
235 my %attributes = ( value => $result->[0] );
236 $attributes{selected} = 'selected' if $selected{ defined($result->[0]) ? $result->[0] : '' };
238 $code .= $self->html_tag('option', _H($result->[1]), %attributes);
245 my ($self, $data) = @_;
246 return $self->html_tag('script', $data, type => 'text/javascript');
253 foreach my $file (@_) {
254 $file .= '.css' unless $file =~ m/\.css$/;
255 $file = "css/${file}" unless $file =~ m|/|;
257 $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
264 my ($self, $name, $value, @slurp) = @_;
265 my %params = _hashify(@slurp);
266 my $name_e = _H($name);
268 my $datefmt = apply {
272 } $::myconfig{"dateformat"};
274 $params{cal_align} ||= 'BR';
276 $self->input_tag($name, $value,
279 title => _H($::myconfig{dateformat}),
280 onBlur => 'check_right_date_format(this)',
282 ) . ((!$params{no_cal}) ?
283 $self->html_tag('img', undef,
284 src => 'image/calendar.png',
286 title => _H($::myconfig{dateformat}),
290 "Calendar.setup({ inputField: '$name_e', ifFormat: '$datefmt', align: '$params{cal_align}', button: 'trigger$seq' });"
294 sub customer_picker {
295 my ($self, $name, $value, %params) = @_;
296 my $name_e = _H($name);
298 $self->hidden_tag($name, (ref $value && $value->can('id')) ? $value->id : '') .
299 $self->input_tag("$name_e\_name", (ref $value && $value->can('name')) ? $value->name : '', %params) .
300 $self->javascript(<<JS);
301 function autocomplete_customer (selector, column) {
302 \$(function(){ \$(selector).autocomplete({
303 source: function(req, rsp) {
305 url: 'controller.pl?action=Customer/ajax_autocomplete',
310 current: function() { \$('#$name_e').val() },
313 success: function (data){ rsp(data) }
318 select: function(event, ui) {
319 \$('#$name_e').val(ui.item.id);
320 \$('#$name_e\_name').val(ui.item.name);
324 autocomplete_customer('#$name_e\_name');
332 foreach my $file (@_) {
333 $file .= '.js' unless $file =~ m/\.js$/;
334 $file = "js/${file}" unless $file =~ m|/|;
336 $code .= qq|<script type="text/javascript" src="${file}"></script>|;
343 my ($self, $tabs, @slurp) = @_;
344 my %params = _hashify(@slurp);
345 my $id = $params{id} || 'tab_' . _tag_id();
347 $params{selected} *= 1;
349 die 'L.tabbed needs an arrayred of tabs for first argument'
350 unless ref $tabs eq 'ARRAY';
352 my (@header, @blocks);
353 for my $i (0..$#$tabs) {
354 my $tab = $tabs->[$i];
358 my $selected = $params{selected} == $i;
359 my $tab_id = "__tab_id_$i";
360 push @header, $self->li_tag(
361 $self->link('', $tab->{name}, rel => $tab_id),
362 ($selected ? (class => 'selected') : ())
364 push @blocks, $self->div_tag($tab->{data},
365 id => $tab_id, class => 'tabcontent');
368 return '' unless @header;
369 return $self->ul_tag(
370 join('', @header), id => $id, class => 'shadetabs'
373 join('', @blocks), class => 'tabcontentstyle'
376 qq|var $id = new ddtabcontent("$id");$id.setpersist(true);| .
377 qq|$id.setselectedClassTarget("link");$id.init();|
382 my ($self, $name, $src, @slurp) = @_;
383 my %params = _hashify(@slurp);
385 $params{method} ||= 'process';
387 return () if defined $params{if} && !$params{if};
390 if ($params{method} eq 'raw') {
392 } elsif ($params{method} eq 'process') {
393 $data = $self->_context->process($src, %{ $params{args} || {} });
395 die "unknown tag method '$params{method}'";
398 return () unless $data;
400 return +{ name => $name, data => $data };
404 my ($self, $name, $value, @slurp) = @_;
405 my %attributes = _hashify(@slurp);
407 my $rows = delete $attributes{rows} || 1;
408 my $min = delete $attributes{min_rows} || 1;
411 ? $self->textarea_tag($name, $value, %attributes, rows => max $rows, $min)
412 : $self->input_tag($name, $value, %attributes);
415 sub multiselect2side {
416 my ($self, $id, @slurp) = @_;
417 my %params = _hashify(@slurp);
419 $params{labelsx} = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
420 $params{labeldx} = "\"" . _J($params{labeldx} || $::locale->text('Selected')) . "\"";
421 $params{moveOptions} = 'false';
423 my $vars = join(', ', map { "${_}: " . $params{$_} } keys %params);
425 <script type="text/javascript">
426 \$().ready(function() {
427 \$('#${id}').multiselect2side({ ${vars} });
435 sub sortable_element {
436 my ($self, $selector, @slurp) = @_;
437 my %params = _hashify(@slurp);
439 my %attributes = ( distance => 5,
440 helper => <<'JAVASCRIPT' );
441 function(event, ui) {
442 ui.children().each(function() {
443 $(this).width($(this).width());
451 if ($params{url} && $params{with}) {
452 my $as = $params{as} || $params{with};
453 my $filter = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
454 $filter .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
456 $stop_event = <<JAVASCRIPT;
457 \$.post('$params{url}', { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() });
461 if (!$params{dont_recolor}) {
462 $stop_event .= <<JAVASCRIPT;
463 \$('${selector}>*:odd').removeClass('listrow1').removeClass('listrow0').addClass('listrow0');
464 \$('${selector}>*:even').removeClass('listrow1').removeClass('listrow0').addClass('listrow1');
469 $attributes{stop} = <<JAVASCRIPT;
470 function(event, ui) {
477 $params{handle} = '.dragdrop' unless exists $params{handle};
478 $attributes{handle} = "'$params{handle}'" if $params{handle};
480 my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
482 my $code = <<JAVASCRIPT;
483 <script type="text/javascript">
485 \$( "${selector}" ).sortable({ ${attr_str} })
493 sub online_help_tag {
494 my ($self, $tag, @slurp) = @_;
495 my %params = _hashify(@slurp);
496 my $cc = $::myconfig{countrycode};
497 my $file = "doc/online/$cc/$tag.html";
498 my $text = $params{text} || $::locale->text('Help');
500 die 'malformed help tag' unless $tag =~ /^[a-zA-Z0-9_]+$/;
501 return unless -f $file;
502 return $self->html_tag('a', $text, href => $file, target => '_blank');
507 require Data::Dumper;
508 return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
517 SL::Templates::Plugin::L -- Layouting / tag generation
521 Usage from a template:
525 [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right' ] ]) %]
527 [% L.select_tag('direction', L.options_for_select([ { direction => 'left', display => 'To the left' },
528 { direction => 'right', display => 'To the right' } ],
529 value => 'direction', title => 'display', default => 'right')) %]
533 A module modeled a bit after Rails' ActionView helpers. Several small
534 functions that create HTML tags from various kinds of data sources.
538 =head2 LOW-LEVEL FUNCTIONS
542 =item C<name_to_id $name>
544 Converts a name to a HTML id by replacing various characters.
546 =item C<attributes %items>
548 Creates a string from all elements in C<%items> suitable for usage as
549 HTML tag attributes. Keys and values are HTML escaped even though keys
550 must not contain non-ASCII characters for browsers to accept them.
552 =item C<html_tag $tag_name, $content_string, %attributes>
554 Creates an opening and closing HTML tag for C<$tag_name> and puts
555 C<$content_string> between the two. If C<$content_string> is undefined
556 or empty then only a E<lt>tag/E<gt> tag will be created. Attributes
557 are key/value pairs added to the opening tag.
559 C<$content_string> is not HTML escaped.
563 =head2 HIGH-LEVEL FUNCTIONS
567 =item C<select_tag $name, $options_string, %attributes>
569 Creates a HTML 'select' tag named C<$name> with the contents
570 C<$options_string> and with arbitrary HTML attributes from
571 C<%attributes>. The tag's C<id> defaults to C<name_to_id($name)>.
573 The C<$options_string> is usually created by the
574 L</options_for_select> function. If C<$options_string> is an array
575 reference then it will be passed to L</options_for_select>
578 =item C<input_tag $name, $value, %attributes>
580 Creates a HTML 'input type=text' tag named C<$name> with the value
581 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
582 tag's C<id> defaults to C<name_to_id($name)>.
584 =item C<hidden_tag $name, $value, %attributes>
586 Creates a HTML 'input type=hidden' tag named C<$name> with the value
587 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
588 tag's C<id> defaults to C<name_to_id($name)>.
590 =item C<submit_tag $name, $value, %attributes>
592 Creates a HTML 'input type=submit class=submit' tag named C<$name> with the
593 value C<$value> and with arbitrary HTML attributes from C<%attributes>. The
594 tag's C<id> defaults to C<name_to_id($name)>.
596 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
597 be added via the C<onclick> handler asking the question given with
598 C<$attributes{confirm}>. If request is only submitted if the user
599 clicks the dialog's ok/yes button.
601 =item C<textarea_tag $name, $value, %attributes>
603 Creates a HTML 'textarea' tag named C<$name> with the content
604 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
605 tag's C<id> defaults to C<name_to_id($name)>.
607 =item C<checkbox_tag $name, %attributes>
609 Creates a HTML 'input type=checkbox' tag named C<$name> with arbitrary
610 HTML attributes from C<%attributes>. The tag's C<id> defaults to
611 C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
613 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
614 created with said C<label>. No attribute named C<label> is created in
617 If C<%attributes> contains a key C<checkall> then the value is taken as a
618 JQuery selector and clicking this checkbox will also toggle all checkboxes
619 matching the selector.
621 =item C<date_tag $name, $value, cal_align =E<gt> $align_code, %attributes>
623 Creates a date input field, with an attached javascript that will open a
624 calendar on click. The javascript ist by default anchoered at the bottom right
625 sight. This can be overridden with C<cal_align>, see Calendar documentation for
626 the details, usually you'll want a two letter abbreviation of the alignment.
627 Right + Bottom becomes C<BL>.
629 =item C<radio_button_tag $name, %attributes>
631 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
632 HTML attributes from C<%attributes>. The tag's C<value> defaults to
633 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
635 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
636 created with said C<label>. No attribute named C<label> is created in
639 =item C<javascript_tag $file1, $file2, $file3...>
641 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
642 tag for each file name parameter passed. Each file name will be
643 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
644 doesn't contain a slash.
646 =item C<stylesheet_tag $file1, $file2, $file3...>
648 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
649 for each file name parameter passed. Each file name will be postfixed
650 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
653 =item C<date_tag $name, $value, cal_align =E<gt> $align_code, %attributes>
655 Creates a date input field, with an attached javascript that will open a
656 calendar on click. The javascript ist by default anchoered at the bottom right
657 sight. This can be overridden with C<cal_align>, see Calendar documentation for
658 the details, usually you'll want a two letter abbreviation of the alignment.
659 Right + Bottom becomes C<BL>.
661 =item C<tabbed \@tab, %attributes>
663 Will create a tabbed area. The tabs should be created with the helper function
667 L.tab(LxERP.t8('Basic Data'), 'part/_main_tab.html'),
668 L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
671 An optional attribute is C<selected>, which accepts the ordinal of a tab which
672 should be selected by default.
674 =item C<areainput_tag $name, $content, %PARAMS>
676 Creates a generic input tag or textarea tag, depending on content size. The
677 mount of desired rows must be given with C<rows> parameter, Accpeted parameters
678 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
680 You can force input by setting rows to 1, and you can force textarea by setting
683 =item C<multiselect2side $id, %params>
685 Creates a JavaScript snippet calling the jQuery function
686 C<multiselect2side> on the select control with the ID C<$id>. The
687 select itself is not created. C<%params> can contain the following
694 The label of the list of available options. Defaults to the
695 translation of 'Available'.
699 The label of the list of selected options. Defaults to the
700 translation of 'Selected'.
704 =item C<sortable_element $selector, %params>
706 Makes the children of the DOM element C<$selector> (a jQuery selector)
707 sortable with the I<jQuery UI Selectable> library. The children can be
708 dragged & dropped around. After dropping an element an URL can be
709 postet to with the element IDs of the sorted children.
711 If this is used then the JavaScript file C<js/jquery-ui.js> must be
712 included manually as well as it isn't loaded via C<$::form-gt;header>.
714 C<%params> can contain the following entries:
720 The URL to POST an AJAX request to after a dragged element has been
721 dropped. The AJAX request's return value is ignored. If given then
722 C<$params{with}> must be given as well.
726 A string that is interpreted as the prefix of the children's ID. Upon
727 POSTing the result each child whose ID starts with C<$params{with}> is
728 considered. The prefix and the following "_" is removed from the
729 ID. The remaining parts of the IDs of those children are posted as a
730 single array parameter. The array parameter's name is either
731 C<$params{as}> or, missing that, C<$params{with}>.
735 Sets the POST parameter name for AJAX request after dropping an
736 element (see C<$params{with}>).
740 An optional jQuery selector specifying which part of the child element
741 is dragable. If the parameter is not given then it defaults to
742 C<.dragdrop> matching DOM elements with the class C<dragdrop>. If the
743 parameter is set and empty then the whole child element is dragable,
744 and clicks through to underlying elements like inputs or links might
747 =item C<dont_recolor>
749 If trueish then the children will not be recolored. The default is to
750 recolor the children by setting the class C<listrow0> on odd and
751 C<listrow1> on even entries.
757 <script type="text/javascript" src="js/jquery-ui.js"></script>
759 <table id="thing_list">
761 <tr><td>This</td><td>That</td></tr>
764 <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
765 <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
766 <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
770 [% L.sortable_element('#thing_list tbody',
771 url => 'controller.pl?action=SystemThings/reorder',
774 recolor_rows => 1) %]
776 After dropping e.g. the third element at the top of the list a POST
777 request would be made to the C<reorder> action of the C<SystemThings>
778 controller with a single parameter called C<thing_ids> -- an array
779 containing the values C<[ 6, 2, 15 ]>.
783 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
787 =head2 CONVERSION FUNCTIONS
791 =item C<options_for_select \@collection, %options>
793 Creates a string suitable for a HTML 'select' tag consisting of one
794 'E<lt>optionE<gt>' tag for each element in C<\@collection>. The value
795 to use and the title to display are extracted from the elements in
796 C<\@collection>. Each element can be one of four things:
800 =item 1. An array reference with at least two elements. The first element is
801 the value, the second element is its title.
803 =item 2. A scalar. The scalar is both the value and the title.
805 =item 3. A hash reference. In this case C<%options> must contain
806 I<value> and I<title> keys that name the keys in the element to use
807 for the value and title respectively.
809 =item 4. A blessed reference. In this case C<%options> must contain
810 I<value> and I<title> keys that name functions called on the blessed
811 reference whose return values are used as the value and title
816 For cases 3 and 4 C<$options{value}> defaults to C<id> and
817 C<$options{title}> defaults to C<$options{value}>.
819 In addition to pure keys/method you can also provide coderefs as I<value_sub>
820 and/or I<title_sub>. If present, these take precedence over keys or methods,
821 and are called with the element as first argument. It must return the value or
824 Lastly a joint coderef I<value_title_sub> may be provided, which in turn takes
825 precedence over each individual sub. It will only be called once for each
826 element and must return a list of value and title.
828 If the option C<with_empty> is set then an empty element (value
829 C<undef>) will be used as the first element. The title to display for
830 this element can be set with the option C<empty_title> and defaults to
833 The option C<default> can be either a scalar or an array reference
834 containing the values of the options which should be set to be
837 =item C<tab, description, target, %PARAMS>
839 Creates a tab for C<tabbed>. The description will be used as displayed name.
840 The target should be a block or template that can be processed. C<tab> supports
841 a C<method> parameter, which can override the process method to apply target.
842 C<method => 'raw'> will just include the given text as is. I was too lazy to
843 implement C<include> properly.
845 Also an C<if> attribute is supported, so that tabs can be suppressed based on
846 some occasion. In this case the supplied block won't even get processed, and
847 the resulting tab will get ignored by C<tabbed>:
849 L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
853 =head1 MODULE AUTHORS
855 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
857 L<http://linet-services.de>