1 package SL::Template::Plugin::L;
3 use base qw( Template::Plugin );
5 use List::MoreUtils qw(apply);
6 use List::Util qw(max);
7 use Scalar::Util qw(blessed);
11 { # This will give you an id for identifying html tags and such.
12 # It's guaranteed to be unique unless you exceed 10 mio calls per request.
13 # Do not use these id's to store information across requests.
14 my $_id_sequence = int rand 1e7;
16 return "id_" . ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
20 my %_valueless_attributes = map { $_ => 1 } qw(
21 checked compact declare defer disabled ismap multiple noresize noshade nowrap
27 return $::locale->quote_special_chars('HTML', $string);
31 my $string = "" . shift;
32 $string =~ s/\"/\\\"/g;
37 return (@_ && (ref($_[0]) eq 'HASH')) ? %{ $_[0] } : @_;
41 my ($class, $context, @args) = @_;
49 die 'not an accessor' if @_ > 1;
50 return $_[0]->{CONTEXT};
57 $name =~ s/[^\w_]/_/g;
64 my ($self, @slurp) = @_;
65 my %options = _hashify(@slurp);
68 while (my ($name, $value) = each %options) {
70 next if $_valueless_attributes{$name} && !$value;
71 $value = '' if !defined($value);
72 push @result, $_valueless_attributes{$name} ? _H($name) : _H($name) . '="' . _H($value) . '"';
75 return @result ? ' ' . join(' ', @result) : '';
79 my ($self, $tag, $content, @slurp) = @_;
80 my $attributes = $self->attributes(@slurp);
82 return "<${tag}${attributes}>" unless defined($content);
83 return "<${tag}${attributes}>${content}</${tag}>";
89 my $options_str = shift;
90 my %attributes = _hashify(@_);
92 $attributes{id} ||= $self->name_to_id($name);
93 $options_str = $self->options_for_select($options_str) if ref $options_str;
95 return $self->html_tag('select', $options_str, %attributes, name => $name);
99 my ($self, $name, $content, @slurp) = @_;
100 my %attributes = _hashify(@slurp);
102 $attributes{id} ||= $self->name_to_id($name);
103 $attributes{rows} *= 1; # required by standard
104 $attributes{cols} *= 1; # required by standard
105 $content = $content ? _H($content) : '';
107 return $self->html_tag('textarea', $content, %attributes, name => $name);
111 my ($self, $name, @slurp) = @_;
112 my %attributes = _hashify(@slurp);
114 $attributes{id} ||= $self->name_to_id($name);
115 $attributes{value} = 1 unless defined $attributes{value};
116 my $label = delete $attributes{label};
117 my $checkall = delete $attributes{checkall};
119 if ($attributes{checked}) {
120 $attributes{checked} = 'checked';
122 delete $attributes{checked};
125 my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'checkbox');
126 $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
127 $code .= $self->javascript(qq|\$('#$attributes{id}').checkall('$checkall');|) if $checkall;
132 sub radio_button_tag {
135 my %attributes = _hashify(@_);
137 $attributes{value} = 1 unless defined $attributes{value};
138 $attributes{id} ||= $self->name_to_id($name . "_" . $attributes{value});
139 my $label = delete $attributes{label};
141 if ($attributes{checked}) {
142 $attributes{checked} = 'checked';
144 delete $attributes{checked};
147 my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'radio');
148 $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
154 my ($self, $name, $value, @slurp) = @_;
155 my %attributes = _hashify(@slurp);
157 $attributes{id} ||= $self->name_to_id($name);
158 $attributes{type} ||= 'text';
160 return $self->html_tag('input', undef, %attributes, name => $name, value => $value);
164 return shift->input_tag(@_, type => 'hidden');
168 my ($self, $content, @slurp) = @_;
169 return $self->html_tag('div', $content, @slurp);
173 my ($self, $content, @slurp) = @_;
174 return $self->html_tag('ul', $content, @slurp);
178 my ($self, $content, @slurp) = @_;
179 return $self->html_tag('li', $content, @slurp);
183 my ($self, $href, $content, @slurp) = @_;
184 my %params = _hashify(@slurp);
188 return $self->html_tag('a', $content, %params, href => $href);
192 my ($self, $name, $value, @slurp) = @_;
193 my %attributes = _hashify(@slurp);
195 $attributes{onclick} = "if (confirm('" . delete($attributes{confirm}) . "')) return true; else return false;" if $attributes{confirm};
197 return $self->input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
201 my ($self, $onclick, $value, @slurp) = @_;
202 my %attributes = _hashify(@slurp);
204 $attributes{id} ||= $self->name_to_id($attributes{name}) if $attributes{name};
205 $attributes{type} ||= 'button';
207 return $self->html_tag('input', undef, %attributes, value => $value, onclick => $onclick);
210 sub options_for_select {
212 my $collection = shift;
213 my %options = _hashify(@_);
215 my $value_key = $options{value} || 'id';
216 my $title_key = $options{title} || $value_key;
218 my $value_sub = $options{value_sub};
219 my $title_sub = $options{title_sub};
221 my $value_title_sub = $options{value_title_sub};
223 my %selected = map { ( $_ => 1 ) } @{ ref($options{default}) eq 'ARRAY' ? $options{default} : defined($options{default}) ? [ $options{default} ] : [] };
226 my ($element, $index, $key, $sub) = @_;
227 my $ref = ref $element;
228 return $sub ? $sub->($element)
230 : $ref eq 'ARRAY' ? $element->[$index]
231 : $ref eq 'HASH' ? $element->{$key}
236 push @elements, [ undef, $options{empty_title} || '' ] if $options{with_empty};
237 push @elements, map [
238 $value_title_sub ? @{ $value_title_sub->($_) } : (
239 $access->($_, 0, $value_key, $value_sub),
240 $access->($_, 1, $title_key, $title_sub),
242 ], @{ $collection } if $collection && ref $collection eq 'ARRAY';
245 foreach my $result (@elements) {
246 my %attributes = ( value => $result->[0] );
247 $attributes{selected} = 'selected' if $selected{ defined($result->[0]) ? $result->[0] : '' };
249 $code .= $self->html_tag('option', _H($result->[1]), %attributes);
256 my ($self, $data) = @_;
257 return $self->html_tag('script', $data, type => 'text/javascript');
264 foreach my $file (@_) {
265 $file .= '.css' unless $file =~ m/\.css$/;
266 $file = "css/${file}" unless $file =~ m|/|;
268 $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
275 my ($self, $name, $value, @slurp) = @_;
276 my %params = _hashify(@slurp);
277 my $name_e = _H($name);
279 my $datefmt = apply {
283 } $::myconfig{"dateformat"};
285 my $cal_align = delete $params{cal_align} || 'BR';
286 my $str_value = blessed $value ? $value->to_lxoffice : $value;
288 $self->input_tag($name, $str_value,
291 title => _H($::myconfig{dateformat}),
292 onBlur => 'check_right_date_format(this)',
294 ) . ((!$params{no_cal}) ?
295 $self->html_tag('img', undef,
296 src => 'image/calendar.png',
297 alt => $::locale->text('Calendar'),
299 title => _H($::myconfig{dateformat}),
303 "Calendar.setup({ inputField: '$name_e', ifFormat: '$datefmt', align: '$cal_align', button: 'trigger$seq' });"
307 sub customer_picker {
308 my ($self, $name, $value, %params) = @_;
309 my $name_e = _H($name);
311 $self->hidden_tag($name, (ref $value && $value->can('id')) ? $value->id : '') .
312 $self->input_tag("$name_e\_name", (ref $value && $value->can('name')) ? $value->name : '', %params) .
313 $self->javascript(<<JS);
314 function autocomplete_customer (selector, column) {
315 \$(function(){ \$(selector).autocomplete({
316 source: function(req, rsp) {
318 url: 'controller.pl?action=Customer/ajax_autocomplete',
323 current: function() { \$('#$name_e').val() },
326 success: function (data){ rsp(data) }
331 select: function(event, ui) {
332 \$('#$name_e').val(ui.item.id);
333 \$('#$name_e\_name').val(ui.item.name);
337 autocomplete_customer('#$name_e\_name');
345 foreach my $file (@_) {
346 $file .= '.js' unless $file =~ m/\.js$/;
347 $file = "js/${file}" unless $file =~ m|/|;
349 $code .= qq|<script type="text/javascript" src="${file}"></script>|;
356 my ($self, $tabs, @slurp) = @_;
357 my %params = _hashify(@slurp);
358 my $id = $params{id} || 'tab_' . _tag_id();
360 $params{selected} *= 1;
362 die 'L.tabbed needs an arrayred of tabs for first argument'
363 unless ref $tabs eq 'ARRAY';
365 my (@header, @blocks);
366 for my $i (0..$#$tabs) {
367 my $tab = $tabs->[$i];
371 my $selected = $params{selected} == $i;
372 my $tab_id = "__tab_id_$i";
373 push @header, $self->li_tag(
374 $self->link('', $tab->{name}, rel => $tab_id),
375 ($selected ? (class => 'selected') : ())
377 push @blocks, $self->div_tag($tab->{data},
378 id => $tab_id, class => 'tabcontent');
381 return '' unless @header;
382 return $self->ul_tag(
383 join('', @header), id => $id, class => 'shadetabs'
386 join('', @blocks), class => 'tabcontentstyle'
389 qq|var $id = new ddtabcontent("$id");$id.setpersist(true);| .
390 qq|$id.setselectedClassTarget("link");$id.init();|
395 my ($self, $name, $src, @slurp) = @_;
396 my %params = _hashify(@slurp);
398 $params{method} ||= 'process';
400 return () if defined $params{if} && !$params{if};
403 if ($params{method} eq 'raw') {
405 } elsif ($params{method} eq 'process') {
406 $data = $self->_context->process($src, %{ $params{args} || {} });
408 die "unknown tag method '$params{method}'";
411 return () unless $data;
413 return +{ name => $name, data => $data };
417 my ($self, $name, $value, @slurp) = @_;
418 my %attributes = _hashify(@slurp);
420 my $rows = delete $attributes{rows} || 1;
421 my $min = delete $attributes{min_rows} || 1;
424 ? $self->textarea_tag($name, $value, %attributes, rows => max $rows, $min)
425 : $self->input_tag($name, $value, %attributes);
428 sub multiselect2side {
429 my ($self, $id, @slurp) = @_;
430 my %params = _hashify(@slurp);
432 $params{labelsx} = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
433 $params{labeldx} = "\"" . _J($params{labeldx} || $::locale->text('Selected')) . "\"";
434 $params{moveOptions} = 'false';
436 my $vars = join(', ', map { "${_}: " . $params{$_} } keys %params);
438 <script type="text/javascript">
439 \$().ready(function() {
440 \$('#${id}').multiselect2side({ ${vars} });
448 sub sortable_element {
449 my ($self, $selector, @slurp) = @_;
450 my %params = _hashify(@slurp);
452 my %attributes = ( distance => 5,
453 helper => <<'JAVASCRIPT' );
454 function(event, ui) {
455 ui.children().each(function() {
456 $(this).width($(this).width());
464 if ($params{url} && $params{with}) {
465 my $as = $params{as} || $params{with};
466 my $filter = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
467 $filter .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
469 $stop_event = <<JAVASCRIPT;
470 \$.post('$params{url}', { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() });
474 if (!$params{dont_recolor}) {
475 $stop_event .= <<JAVASCRIPT;
476 \$('${selector}>*:odd').removeClass('listrow1').removeClass('listrow0').addClass('listrow0');
477 \$('${selector}>*:even').removeClass('listrow1').removeClass('listrow0').addClass('listrow1');
482 $attributes{stop} = <<JAVASCRIPT;
483 function(event, ui) {
490 $params{handle} = '.dragdrop' unless exists $params{handle};
491 $attributes{handle} = "'$params{handle}'" if $params{handle};
493 my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
495 my $code = <<JAVASCRIPT;
496 <script type="text/javascript">
498 \$( "${selector}" ).sortable({ ${attr_str} })
506 sub online_help_tag {
507 my ($self, $tag, @slurp) = @_;
508 my %params = _hashify(@slurp);
509 my $cc = $::myconfig{countrycode};
510 my $file = "doc/online/$cc/$tag.html";
511 my $text = $params{text} || $::locale->text('Help');
513 die 'malformed help tag' unless $tag =~ /^[a-zA-Z0-9_]+$/;
514 return unless -f $file;
515 return $self->html_tag('a', $text, href => $file, class => 'jqModal')
520 require Data::Dumper;
521 return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
530 SL::Templates::Plugin::L -- Layouting / tag generation
534 Usage from a template:
538 [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right' ] ]) %]
540 [% L.select_tag('direction', L.options_for_select([ { direction => 'left', display => 'To the left' },
541 { direction => 'right', display => 'To the right' } ],
542 value => 'direction', title => 'display', default => 'right')) %]
546 A module modeled a bit after Rails' ActionView helpers. Several small
547 functions that create HTML tags from various kinds of data sources.
551 =head2 LOW-LEVEL FUNCTIONS
555 =item C<name_to_id $name>
557 Converts a name to a HTML id by replacing various characters.
559 =item C<attributes %items>
561 Creates a string from all elements in C<%items> suitable for usage as
562 HTML tag attributes. Keys and values are HTML escaped even though keys
563 must not contain non-ASCII characters for browsers to accept them.
565 =item C<html_tag $tag_name, $content_string, %attributes>
567 Creates an opening and closing HTML tag for C<$tag_name> and puts
568 C<$content_string> between the two. If C<$content_string> is undefined
569 or empty then only a E<lt>tag/E<gt> tag will be created. Attributes
570 are key/value pairs added to the opening tag.
572 C<$content_string> is not HTML escaped.
576 =head2 HIGH-LEVEL FUNCTIONS
580 =item C<select_tag $name, $options_string, %attributes>
582 Creates a HTML 'select' tag named C<$name> with the contents
583 C<$options_string> and with arbitrary HTML attributes from
584 C<%attributes>. The tag's C<id> defaults to C<name_to_id($name)>.
586 The C<$options_string> is usually created by the
587 L</options_for_select> function. If C<$options_string> is an array
588 reference then it will be passed to L</options_for_select>
591 =item C<input_tag $name, $value, %attributes>
593 Creates a HTML 'input type=text' tag named C<$name> with the value
594 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
595 tag's C<id> defaults to C<name_to_id($name)>.
597 =item C<hidden_tag $name, $value, %attributes>
599 Creates a HTML 'input type=hidden' tag named C<$name> with the value
600 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
601 tag's C<id> defaults to C<name_to_id($name)>.
603 =item C<submit_tag $name, $value, %attributes>
605 Creates a HTML 'input type=submit class=submit' tag named C<$name> with the
606 value C<$value> and with arbitrary HTML attributes from C<%attributes>. The
607 tag's C<id> defaults to C<name_to_id($name)>.
609 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
610 be added via the C<onclick> handler asking the question given with
611 C<$attributes{confirm}>. If request is only submitted if the user
612 clicks the dialog's ok/yes button.
614 =item C<textarea_tag $name, $value, %attributes>
616 Creates a HTML 'textarea' tag named C<$name> with the content
617 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
618 tag's C<id> defaults to C<name_to_id($name)>.
620 =item C<checkbox_tag $name, %attributes>
622 Creates a HTML 'input type=checkbox' tag named C<$name> with arbitrary
623 HTML attributes from C<%attributes>. The tag's C<id> defaults to
624 C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
626 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
627 created with said C<label>. No attribute named C<label> is created in
630 If C<%attributes> contains a key C<checkall> then the value is taken as a
631 JQuery selector and clicking this checkbox will also toggle all checkboxes
632 matching the selector.
634 =item C<date_tag $name, $value, cal_align =E<gt> $align_code, %attributes>
636 Creates a date input field, with an attached javascript that will open a
637 calendar on click. The javascript ist by default anchoered at the bottom right
638 sight. This can be overridden with C<cal_align>, see Calendar documentation for
639 the details, usually you'll want a two letter abbreviation of the alignment.
640 Right + Bottom becomes C<BL>.
642 =item C<radio_button_tag $name, %attributes>
644 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
645 HTML attributes from C<%attributes>. The tag's C<value> defaults to
646 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
648 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
649 created with said C<label>. No attribute named C<label> is created in
652 =item C<javascript_tag $file1, $file2, $file3...>
654 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
655 tag for each file name parameter passed. Each file name will be
656 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
657 doesn't contain a slash.
659 =item C<stylesheet_tag $file1, $file2, $file3...>
661 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
662 for each file name parameter passed. Each file name will be postfixed
663 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
666 =item C<date_tag $name, $value, cal_align =E<gt> $align_code, %attributes>
668 Creates a date input field, with an attached javascript that will open a
669 calendar on click. The javascript ist by default anchoered at the bottom right
670 sight. This can be overridden with C<cal_align>, see Calendar documentation for
671 the details, usually you'll want a two letter abbreviation of the alignment.
672 Right + Bottom becomes C<BL>.
674 =item C<tabbed \@tab, %attributes>
676 Will create a tabbed area. The tabs should be created with the helper function
680 L.tab(LxERP.t8('Basic Data'), 'part/_main_tab.html'),
681 L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
684 An optional attribute is C<selected>, which accepts the ordinal of a tab which
685 should be selected by default.
687 =item C<areainput_tag $name, $content, %PARAMS>
689 Creates a generic input tag or textarea tag, depending on content size. The
690 mount of desired rows must be given with C<rows> parameter, Accpeted parameters
691 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
693 You can force input by setting rows to 1, and you can force textarea by setting
696 =item C<multiselect2side $id, %params>
698 Creates a JavaScript snippet calling the jQuery function
699 C<multiselect2side> on the select control with the ID C<$id>. The
700 select itself is not created. C<%params> can contain the following
707 The label of the list of available options. Defaults to the
708 translation of 'Available'.
712 The label of the list of selected options. Defaults to the
713 translation of 'Selected'.
717 =item C<sortable_element $selector, %params>
719 Makes the children of the DOM element C<$selector> (a jQuery selector)
720 sortable with the I<jQuery UI Selectable> library. The children can be
721 dragged & dropped around. After dropping an element an URL can be
722 postet to with the element IDs of the sorted children.
724 If this is used then the JavaScript file C<js/jquery-ui.js> must be
725 included manually as well as it isn't loaded via C<$::form-gt;header>.
727 C<%params> can contain the following entries:
733 The URL to POST an AJAX request to after a dragged element has been
734 dropped. The AJAX request's return value is ignored. If given then
735 C<$params{with}> must be given as well.
739 A string that is interpreted as the prefix of the children's ID. Upon
740 POSTing the result each child whose ID starts with C<$params{with}> is
741 considered. The prefix and the following "_" is removed from the
742 ID. The remaining parts of the IDs of those children are posted as a
743 single array parameter. The array parameter's name is either
744 C<$params{as}> or, missing that, C<$params{with}>.
748 Sets the POST parameter name for AJAX request after dropping an
749 element (see C<$params{with}>).
753 An optional jQuery selector specifying which part of the child element
754 is dragable. If the parameter is not given then it defaults to
755 C<.dragdrop> matching DOM elements with the class C<dragdrop>. If the
756 parameter is set and empty then the whole child element is dragable,
757 and clicks through to underlying elements like inputs or links might
760 =item C<dont_recolor>
762 If trueish then the children will not be recolored. The default is to
763 recolor the children by setting the class C<listrow0> on odd and
764 C<listrow1> on even entries.
770 <script type="text/javascript" src="js/jquery-ui.js"></script>
772 <table id="thing_list">
774 <tr><td>This</td><td>That</td></tr>
777 <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
778 <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
779 <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
783 [% L.sortable_element('#thing_list tbody',
784 url => 'controller.pl?action=SystemThings/reorder',
787 recolor_rows => 1) %]
789 After dropping e.g. the third element at the top of the list a POST
790 request would be made to the C<reorder> action of the C<SystemThings>
791 controller with a single parameter called C<thing_ids> -- an array
792 containing the values C<[ 6, 2, 15 ]>.
796 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
800 =head2 CONVERSION FUNCTIONS
804 =item C<options_for_select \@collection, %options>
806 Creates a string suitable for a HTML 'select' tag consisting of one
807 'E<lt>optionE<gt>' tag for each element in C<\@collection>. The value
808 to use and the title to display are extracted from the elements in
809 C<\@collection>. Each element can be one of four things:
813 =item 1. An array reference with at least two elements. The first element is
814 the value, the second element is its title.
816 =item 2. A scalar. The scalar is both the value and the title.
818 =item 3. A hash reference. In this case C<%options> must contain
819 I<value> and I<title> keys that name the keys in the element to use
820 for the value and title respectively.
822 =item 4. A blessed reference. In this case C<%options> must contain
823 I<value> and I<title> keys that name functions called on the blessed
824 reference whose return values are used as the value and title
829 For cases 3 and 4 C<$options{value}> defaults to C<id> and
830 C<$options{title}> defaults to C<$options{value}>.
832 In addition to pure keys/method you can also provide coderefs as I<value_sub>
833 and/or I<title_sub>. If present, these take precedence over keys or methods,
834 and are called with the element as first argument. It must return the value or
837 Lastly a joint coderef I<value_title_sub> may be provided, which in turn takes
838 precedence over each individual sub. It will only be called once for each
839 element and must return a list of value and title.
841 If the option C<with_empty> is set then an empty element (value
842 C<undef>) will be used as the first element. The title to display for
843 this element can be set with the option C<empty_title> and defaults to
846 The option C<default> can be either a scalar or an array reference
847 containing the values of the options which should be set to be
850 =item C<tab, description, target, %PARAMS>
852 Creates a tab for C<tabbed>. The description will be used as displayed name.
853 The target should be a block or template that can be processed. C<tab> supports
854 a C<method> parameter, which can override the process method to apply target.
855 C<method => 'raw'> will just include the given text as is. I was too lazy to
856 implement C<include> properly.
858 Also an C<if> attribute is supported, so that tabs can be suppressed based on
859 some occasion. In this case the supplied block won't even get processed, and
860 the resulting tab will get ignored by C<tabbed>:
862 L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
866 =head1 MODULE AUTHORS
868 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
870 L<http://linet-services.de>