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);
13 { # This will give you an id for identifying html tags and such.
14 # It's guaranteed to be unique unless you exceed 10 mio calls per request.
15 # Do not use these id's to store information across requests.
16 my $_id_sequence = int rand 1e7;
18 return "id_" . ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
24 return $::locale->quote_special_chars('HTML', $string);
29 $string =~ s/(\"|\'|\\)/\\$1/g;
34 return (@_ && (ref($_[0]) eq 'HASH')) ? %{ $_[0] } : @_;
38 my ($class, $context, @args) = @_;
46 die 'not an accessor' if @_ > 1;
47 return $_[0]->{CONTEXT};
51 my ($method, $self, @args) = @_;
53 my $presenter = $::request->presenter;
55 if (!$presenter->can($method)) {
56 $::lxdebug->message(LXDebug::WARN(), "SL::Presenter has no method named '$method'!");
60 splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
62 $presenter->$method(@args);
65 sub name_to_id { return _call_presenter('name_to_id', @_); }
66 sub html_tag { return _call_presenter('html_tag', @_); }
67 sub select_tag { return _call_presenter('select_tag', @_); }
68 sub input_tag { return _call_presenter('input_tag', @_); }
69 sub truncate { return _call_presenter('truncate', @_); }
70 sub simple_format { return _call_presenter('simple_format', @_); }
73 my ($self, @slurp) = @_;
74 my %options = _hashify(@slurp);
78 return $self->html_tag('img', undef, %options);
82 my ($self, $name, $content, @slurp) = @_;
83 my %attributes = _hashify(@slurp);
85 $attributes{id} ||= $self->name_to_id($name);
86 $attributes{rows} *= 1; # required by standard
87 $attributes{cols} *= 1; # required by standard
88 $content = $content ? _H($content) : '';
90 return $self->html_tag('textarea', $content, %attributes, name => $name);
94 my ($self, $name, @slurp) = @_;
95 my %attributes = _hashify(@slurp);
97 $attributes{id} ||= $self->name_to_id($name);
98 $attributes{value} = 1 unless defined $attributes{value};
99 my $label = delete $attributes{label};
100 my $checkall = delete $attributes{checkall};
102 if ($attributes{checked}) {
103 $attributes{checked} = 'checked';
105 delete $attributes{checked};
108 my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'checkbox');
109 $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
110 $code .= $self->javascript(qq|\$('#$attributes{id}').checkall('$checkall');|) if $checkall;
115 sub radio_button_tag {
118 my %attributes = _hashify(@_);
120 $attributes{value} = 1 unless defined $attributes{value};
121 $attributes{id} ||= $self->name_to_id($name . "_" . $attributes{value});
122 my $label = delete $attributes{label};
124 if ($attributes{checked}) {
125 $attributes{checked} = 'checked';
127 delete $attributes{checked};
130 my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'radio');
131 $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
137 my ($self, $name, $value, @slurp) = @_;
138 return $self->input_tag($name, $value, _hashify(@slurp), type => 'hidden');
142 my ($self, $content, @slurp) = @_;
143 return $self->html_tag('div', $content, @slurp);
147 my ($self, $content, @slurp) = @_;
148 return $self->html_tag('ul', $content, @slurp);
152 my ($self, $content, @slurp) = @_;
153 return $self->html_tag('li', $content, @slurp);
157 my ($self, $href, $content, @slurp) = @_;
158 my %params = _hashify(@slurp);
162 return $self->html_tag('a', $content, %params, href => $href);
166 my ($self, $name, $value, @slurp) = @_;
167 my %attributes = _hashify(@slurp);
169 if ( $attributes{confirm} ) {
170 $attributes{onclick} = 'return confirm("'. _J(delete($attributes{confirm})) .'");';
173 return $self->input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
177 my ($self, $onclick, $value, @slurp) = @_;
178 my %attributes = _hashify(@slurp);
180 $attributes{id} ||= $self->name_to_id($attributes{name}) if $attributes{name};
181 $attributes{type} ||= 'button';
183 $onclick = 'if (!confirm("'. _J(delete($attributes{confirm})) .'")) return false; ' . $onclick if $attributes{confirm};
185 return $self->html_tag('input', undef, %attributes, value => $value, onclick => $onclick);
188 sub ajax_submit_tag {
189 my ($self, $url, $form_selector, $text, @slurp) = @_;
192 $form_selector = _J($form_selector);
193 my $onclick = qq|submit_ajax_form('${url}', '${form_selector}')|;
195 return $self->button_tag($onclick, $text, @slurp);
199 my ($self, $name, $value) = splice @_, 0, 3;
200 my %attributes = _hashify(@_);
202 return $self->select_tag($name, [ [ 1 => $::locale->text('Yes') ], [ 0 => $::locale->text('No') ] ], default => $value ? 1 : 0, %attributes);
206 my ($self, $data) = @_;
207 return $self->html_tag('script', $data, type => 'text/javascript');
214 foreach my $file (@_) {
215 $file .= '.css' unless $file =~ m/\.css$/;
216 $file = "css/${file}" unless $file =~ m|/|;
218 $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
224 my $date_tag_id_idx = 0;
226 my ($self, $name, $value, @slurp) = @_;
228 my %params = _hashify(@slurp);
229 my $id = $self->name_to_id($name) . _tag_id();
230 my @onchange = $params{onchange} ? (onChange => delete $params{onchange}) : ();
231 my @class = $params{no_cal} || $params{readonly} ? () : (class => 'datepicker');
233 return $self->input_tag(
234 $name, blessed($value) ? $value->to_lxoffice : $value,
237 onblur => "check_right_date_format(this);",
243 sub customer_picker {
244 my ($self, $name, $value, %params) = @_;
245 my $name_e = _H($name);
247 $::request->{layout}->add_javascripts('autocomplete_customer.js');
249 $self->hidden_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => 'customer_autocomplete') .
250 $self->input_tag("$name_e\_name", (ref $value && $value->can('name')) ? $value->name : '', %params);
253 # simple version with select_tag
254 sub vendor_selector {
255 my ($self, $name, $value, %params) = @_;
257 my $actual_vendor_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"}) ? $::form->{"$name"}->id : $::form->{"$name"}) :
258 (ref $value && $value->can('id')) ? $value->id : '';
260 return $self->select_tag($name, SL::DB::Manager::Vendor->get_all(),
261 default => $actual_vendor_id,
262 title_sub => sub { $_[0]->vendornumber . " : " . $_[0]->name },
268 # simple version with select_tag
270 my ($self, $name, $value, %params) = @_;
272 my $actual_part_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"})? $::form->{"$name"}->id : $::form->{"$name"}) :
273 (ref $value && $value->can('id')) ? $value->id : '';
275 return $self->select_tag($name, SL::DB::Manager::Part->get_all(),
276 default => $actual_part_id,
277 title_sub => sub { $_[0]->partnumber . " : " . $_[0]->description },
287 foreach my $file (@_) {
288 $file .= '.js' unless $file =~ m/\.js$/;
289 $file = "js/${file}" unless $file =~ m|/|;
291 $code .= qq|<script type="text/javascript" src="${file}"></script>|;
298 my ($self, $tabs, @slurp) = @_;
299 my %params = _hashify(@slurp);
300 my $id = $params{id} || 'tab_' . _tag_id();
302 $params{selected} *= 1;
304 die 'L.tabbed needs an arrayred of tabs for first argument'
305 unless ref $tabs eq 'ARRAY';
307 my (@header, @blocks);
308 for my $i (0..$#$tabs) {
309 my $tab = $tabs->[$i];
313 my $tab_id = "__tab_id_$i";
314 push @header, $self->li_tag($self->link('#' . $tab_id, $tab->{name}));
315 push @blocks, $self->div_tag($tab->{data}, id => $tab_id);
318 return '' unless @header;
320 my $ul = $self->ul_tag(join('', @header), id => $id);
321 return $self->div_tag(join('', $ul, @blocks), class => 'tabwidget');
325 my ($self, $name, $src, @slurp) = @_;
326 my %params = _hashify(@slurp);
328 $params{method} ||= 'process';
330 return () if defined $params{if} && !$params{if};
333 if ($params{method} eq 'raw') {
335 } elsif ($params{method} eq 'process') {
336 $data = $self->_context->process($src, %{ $params{args} || {} });
338 die "unknown tag method '$params{method}'";
341 return () unless $data;
343 return +{ name => $name, data => $data };
347 my ($self, $name, $value, @slurp) = @_;
348 my %attributes = _hashify(@slurp);
351 my $min = delete $attributes{min_rows} || 1;
353 if (exists $attributes{cols}) {
354 $cols = delete $attributes{cols};
355 $rows = $::form->numtextrows($value, $cols);
357 $rows = delete $attributes{rows} || 1;
361 ? $self->textarea_tag($name, $value, %attributes, rows => max($rows, $min), ($cols ? (cols => $cols) : ()))
362 : $self->input_tag($name, $value, %attributes, ($cols ? (size => $cols) : ()));
365 sub multiselect2side {
366 my ($self, $id, @slurp) = @_;
367 my %params = _hashify(@slurp);
369 $params{labelsx} = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
370 $params{labeldx} = "\"" . _J($params{labeldx} || $::locale->text('Selected')) . "\"";
371 $params{moveOptions} = 'false';
373 my $vars = join(', ', map { "${_}: " . $params{$_} } keys %params);
375 <script type="text/javascript">
376 \$().ready(function() {
377 \$('#${id}').multiselect2side({ ${vars} });
385 sub sortable_element {
386 my ($self, $selector, @slurp) = @_;
387 my %params = _hashify(@slurp);
389 my %attributes = ( distance => 5,
390 helper => <<'JAVASCRIPT' );
391 function(event, ui) {
392 ui.children().each(function() {
393 $(this).width($(this).width());
401 if ($params{url} && $params{with}) {
402 my $as = $params{as} || $params{with};
403 my $filter = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
404 $filter .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
406 $stop_event = <<JAVASCRIPT;
407 \$.post('$params{url}', { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() });
411 if (!$params{dont_recolor}) {
412 $stop_event .= <<JAVASCRIPT;
413 \$('${selector}>*:odd').removeClass('listrow1').removeClass('listrow0').addClass('listrow0');
414 \$('${selector}>*:even').removeClass('listrow1').removeClass('listrow0').addClass('listrow1');
419 $attributes{stop} = <<JAVASCRIPT;
420 function(event, ui) {
427 $params{handle} = '.dragdrop' unless exists $params{handle};
428 $attributes{handle} = "'$params{handle}'" if $params{handle};
430 my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
432 my $code = <<JAVASCRIPT;
433 <script type="text/javascript">
435 \$( "${selector}" ).sortable({ ${attr_str} })
443 sub online_help_tag {
444 my ($self, $tag, @slurp) = @_;
445 my %params = _hashify(@slurp);
446 my $cc = $::myconfig{countrycode};
447 my $file = "doc/online/$cc/$tag.html";
448 my $text = $params{text} || $::locale->text('Help');
450 die 'malformed help tag' unless $tag =~ /^[a-zA-Z0-9_]+$/;
451 return unless -f $file;
452 return $self->html_tag('a', $text, href => $file, class => 'jqModal')
457 require Data::Dumper;
458 return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
461 sub sortable_table_header {
462 my ($self, $by, @slurp) = @_;
463 my %params = _hashify(@slurp);
465 my $controller = $self->{CONTEXT}->stash->get('SELF');
466 my $sort_spec = $controller->get_sort_spec;
467 my $by_spec = $sort_spec->{$by};
468 my %current_sort_params = $controller->get_current_sort_params;
469 my ($image, $new_dir) = ('', $current_sort_params{dir});
470 my $title = delete($params{title}) || $::locale->text($by_spec->{title});
472 if ($current_sort_params{by} eq $by) {
473 my $current_dir = $current_sort_params{dir} ? 'up' : 'down';
474 $image = '<img border="0" src="image/' . $current_dir . '.png">';
475 $new_dir = 1 - ($current_sort_params{dir} || 0);
478 $params{ $sort_spec->{FORM_PARAMS}->[0] } = $by;
479 $params{ $sort_spec->{FORM_PARAMS}->[1] } = ($new_dir ? '1' : '0');
481 return '<a href="' . $controller->get_callback(%params) . '">' . _H($title) . $image . '</a>';
484 sub paginate_controls {
487 my $controller = $self->{CONTEXT}->stash->get('SELF');
488 my $paginate_spec = $controller->get_paginate_spec;
489 my %paginate_params = $controller->get_current_paginate_params;
491 my %template_params = (
492 pages => \%paginate_params,
494 my %url_params = _hashify(@_);
495 $url_params{ $paginate_spec->{FORM_PARAMS}->[0] } = delete $url_params{page};
496 $url_params{ $paginate_spec->{FORM_PARAMS}->[1] } = delete $url_params{per_page} if exists $url_params{per_page};
498 return $controller->get_callback(%url_params);
502 return SL::Presenter->get->render('common/paginate', %template_params);
511 SL::Templates::Plugin::L -- Layouting / tag generation
515 Usage from a template:
519 [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right', 1 ] ]) %]
521 [% L.select_tag('direction', [ { direction => 'left', display => 'To the left' },
522 { direction => 'right', display => 'To the right' } ],
523 value_key => 'direction', title_key => 'display', default => 'right')) %]
525 [% L.select_tag('direction', [ { direction => 'left', display => 'To the left' },
526 { direction => 'right', display => 'To the right', selected => 1 } ],
527 value_key => 'direction', title_key => 'display')) %]
531 A module modeled a bit after Rails' ActionView helpers. Several small
532 functions that create HTML tags from various kinds of data sources.
536 =head2 LOW-LEVEL FUNCTIONS
538 The following items are just forwarded to L<SL::Presenter::Tag>:
542 =item * C<name_to_id $name>
544 =item * C<stringify_attributes %items>
546 =item * C<html_tag $tag_name, $content_string, %attributes>
550 =head2 HIGH-LEVEL FUNCTIONS
552 The following functions are just forwarded to L<SL::Presenter::Tag>:
556 =item * C<input_tag $name, $value, %attributes>
558 =item * C<select_tag $name, \@collection, %attributes>
562 Available high-level functions implemented in this module:
566 =item C<yes_no_tag $name, $value, %attributes>
568 Creates a HTML 'select' tag with the two entries C<yes> and C<no> by
569 calling L<select_tag>. C<$value> determines
570 which entry is selected. The C<%attributes> are passed through to
573 =item C<hidden_tag $name, $value, %attributes>
575 Creates a HTML 'input type=hidden' tag named C<$name> with the value
576 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
577 tag's C<id> defaults to C<name_to_id($name)>.
579 =item C<submit_tag $name, $value, %attributes>
581 Creates a HTML 'input type=submit class=submit' tag named C<$name> with the
582 value C<$value> and with arbitrary HTML attributes from C<%attributes>. The
583 tag's C<id> defaults to C<name_to_id($name)>.
585 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
586 be added via the C<onclick> handler asking the question given with
587 C<$attributes{confirm}>. The request is only submitted if the user
588 clicks the dialog's ok/yes button.
590 =item C<ajax_submit_tag $url, $form_selector, $text, %attributes>
592 Creates a HTML 'input type="button"' tag with a very specific onclick
593 handler that submits the form given by the jQuery selector
594 C<$form_selector> to the URL C<$url> (the actual JavaScript function
595 called for that is C<submit_ajax_form()> in C<js/client_js.js>). The
596 button's label will be C<$text>.
598 =item C<button_tag $onclick, $text, %attributes>
600 Creates a HTML 'input type="button"' tag with an onclick handler
601 C<$onclick> and a value of C<$text>. The button does not have a name
602 nor an ID by default.
604 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
605 be prepended to the C<$onclick> handler asking the question given with
606 C<$attributes{confirm}>. The request is only submitted if the user
607 clicks the dialog's "ok/yes" button.
609 =item C<textarea_tag $name, $value, %attributes>
611 Creates a HTML 'textarea' tag named C<$name> with the content
612 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
613 tag's C<id> defaults to C<name_to_id($name)>.
615 =item C<checkbox_tag $name, %attributes>
617 Creates a HTML 'input type=checkbox' tag named C<$name> with arbitrary
618 HTML attributes from C<%attributes>. The tag's C<id> defaults to
619 C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
621 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
622 created with said C<label>. No attribute named C<label> is created in
625 If C<%attributes> contains a key C<checkall> then the value is taken as a
626 JQuery selector and clicking this checkbox will also toggle all checkboxes
627 matching the selector.
629 =item C<date_tag $name, $value, %attributes>
631 Creates a date input field, with an attached javascript that will open a
634 =item C<radio_button_tag $name, %attributes>
636 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
637 HTML attributes from C<%attributes>. The tag's C<value> defaults to
638 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
640 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
641 created with said C<label>. No attribute named C<label> is created in
644 =item C<javascript_tag $file1, $file2, $file3...>
646 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
647 tag for each file name parameter passed. Each file name will be
648 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
649 doesn't contain a slash.
651 =item C<stylesheet_tag $file1, $file2, $file3...>
653 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
654 for each file name parameter passed. Each file name will be postfixed
655 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
658 =item C<tabbed \@tab, %attributes>
660 Will create a tabbed area. The tabs should be created with the helper function
664 L.tab(LxERP.t8('Basic Data'), 'part/_main_tab.html'),
665 L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
668 =item C<areainput_tag $name, $content, %PARAMS>
670 Creates a generic input tag or textarea tag, depending on content size. The
671 amount of desired rows must be either given with the C<rows> parameter or can
672 be computed from the value and the C<cols> paramter, Accepted parameters
673 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
675 You can force input by setting rows to 1, and you can force textarea by setting
678 =item C<multiselect2side $id, %params>
680 Creates a JavaScript snippet calling the jQuery function
681 C<multiselect2side> on the select control with the ID C<$id>. The
682 select itself is not created. C<%params> can contain the following
689 The label of the list of available options. Defaults to the
690 translation of 'Available'.
694 The label of the list of selected options. Defaults to the
695 translation of 'Selected'.
699 =item C<sortable_element $selector, %params>
701 Makes the children of the DOM element C<$selector> (a jQuery selector)
702 sortable with the I<jQuery UI Selectable> library. The children can be
703 dragged & dropped around. After dropping an element an URL can be
704 postet to with the element IDs of the sorted children.
706 If this is used then the JavaScript file C<js/jquery-ui.js> must be
707 included manually as well as it isn't loaded via C<$::form-gt;header>.
709 C<%params> can contain the following entries:
715 The URL to POST an AJAX request to after a dragged element has been
716 dropped. The AJAX request's return value is ignored. If given then
717 C<$params{with}> must be given as well.
721 A string that is interpreted as the prefix of the children's ID. Upon
722 POSTing the result each child whose ID starts with C<$params{with}> is
723 considered. The prefix and the following "_" is removed from the
724 ID. The remaining parts of the IDs of those children are posted as a
725 single array parameter. The array parameter's name is either
726 C<$params{as}> or, missing that, C<$params{with}>.
730 Sets the POST parameter name for AJAX request after dropping an
731 element (see C<$params{with}>).
735 An optional jQuery selector specifying which part of the child element
736 is dragable. If the parameter is not given then it defaults to
737 C<.dragdrop> matching DOM elements with the class C<dragdrop>. If the
738 parameter is set and empty then the whole child element is dragable,
739 and clicks through to underlying elements like inputs or links might
742 =item C<dont_recolor>
744 If trueish then the children will not be recolored. The default is to
745 recolor the children by setting the class C<listrow0> on odd and
746 C<listrow1> on even entries.
752 <script type="text/javascript" src="js/jquery-ui.js"></script>
754 <table id="thing_list">
756 <tr><td>This</td><td>That</td></tr>
759 <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
760 <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
761 <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
765 [% L.sortable_element('#thing_list tbody',
766 url => 'controller.pl?action=SystemThings/reorder',
769 recolor_rows => 1) %]
771 After dropping e.g. the third element at the top of the list a POST
772 request would be made to the C<reorder> action of the C<SystemThings>
773 controller with a single parameter called C<thing_ids> -- an array
774 containing the values C<[ 6, 2, 15 ]>.
778 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
780 =item C<sortable_table_header $by, %params>
782 Create a link and image suitable for placement in a table
783 header. C<$by> must be an index set up by the controller with
784 L<SL::Controller::Helper::make_sorted>.
786 The optional parameter C<$params{title}> can override the column title
787 displayed to the user. Otherwise the column title from the
788 controller's sort spec is used.
790 The other parameters in C<%params> are passed unmodified to the
791 underlying call to L<SL::Controller::Base::url_for>.
793 See the documentation of L<SL::Controller::Helper::Sorted> for an
794 overview and further usage instructions.
796 =item C<paginate_controls>
798 Create a set of links used to paginate a list view.
800 See the documentation of L<SL::Controller::Helper::Paginated> for an
801 overview and further usage instructions.
805 =head2 CONVERSION FUNCTIONS
809 =item C<tab, description, target, %PARAMS>
811 Creates a tab for C<tabbed>. The description will be used as displayed name.
812 The target should be a block or template that can be processed. C<tab> supports
813 a C<method> parameter, which can override the process method to apply target.
814 C<method => 'raw'> will just include the given text as is. I was too lazy to
815 implement C<include> properly.
817 Also an C<if> attribute is supported, so that tabs can be suppressed based on
818 some occasion. In this case the supplied block won't even get processed, and
819 the resulting tab will get ignored by C<tabbed>:
821 L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
823 =item C<truncate $text, [%params]>
825 See L<SL::Presenter::Text/truncate>.
827 =item C<simple_format $text>
829 See L<SL::Presenter::Text/simple_format>.
833 =head1 MODULE AUTHORS
835 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
837 L<http://linet-services.de>