1 package SL::Template::Plugin::L;
3 use base qw( Template::Plugin );
6 use List::MoreUtils qw(apply);
7 use List::Util qw(max);
8 use Scalar::Util qw(blessed);
11 use SL::Util qw(_hashify);
15 { # This will give you an id for identifying html tags and such.
16 # It's guaranteed to be unique unless you exceed 10 mio calls per request.
17 # Do not use these id's to store information across requests.
18 my $_id_sequence = int rand 1e7;
20 return "id_" . ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
26 return $::locale->quote_special_chars('HTML', $string);
31 $string =~ s/(\"|\'|\\)/\\$1/g;
36 my ($class, $context, @args) = @_;
44 die 'not an accessor' if @_ > 1;
45 return $_[0]->{CONTEXT};
49 my ($method, $self, @args) = @_;
51 my $presenter = $::request->presenter;
53 if (!$presenter->can($method)) {
54 $::lxdebug->message(LXDebug::WARN(), "SL::Presenter has no method named '$method'!");
58 splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
60 $presenter->$method(@args);
63 sub name_to_id { return _call_presenter('name_to_id', @_); }
64 sub html_tag { return _call_presenter('html_tag', @_); }
65 sub select_tag { return _call_presenter('select_tag', @_); }
66 sub input_tag { return _call_presenter('input_tag', @_); }
67 sub truncate { return _call_presenter('truncate', @_); }
68 sub simple_format { return _call_presenter('simple_format', @_); }
69 sub part_picker { return _call_presenter('part_picker', @_); }
70 sub customer_vendor_picker { return _call_presenter('customer_vendor_picker', @_); }
72 sub _set_id_attribute {
73 my ($attributes, $name, $unique) = @_;
74 SL::Presenter::Tag::_set_id_attribute($attributes, $name, $unique);
78 my ($self, %options) = _hashify(1, @_);
82 return $self->html_tag('img', undef, %options);
86 my ($self, $name, $content, %attributes) = _hashify(3, @_);
88 _set_id_attribute(\%attributes, $name);
89 $attributes{rows} *= 1; # required by standard
90 $attributes{cols} *= 1; # required by standard
91 $content = $content ? _H($content) : '';
93 return $self->html_tag('textarea', $content, %attributes, name => $name);
97 my ($self, $name, %attributes) = _hashify(2, @_);
99 _set_id_attribute(\%attributes, $name);
100 $attributes{value} = 1 unless defined $attributes{value};
101 my $label = delete $attributes{label};
102 my $checkall = delete $attributes{checkall};
103 my $for_submit = delete $attributes{for_submit};
105 if ($attributes{checked}) {
106 $attributes{checked} = 'checked';
108 delete $attributes{checked};
112 $code .= $self->hidden_tag($name, 0, %attributes, id => $attributes{id} . '_hidden') if $for_submit;
113 $code .= $self->html_tag('input', undef, %attributes, name => $name, type => 'checkbox');
114 $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
115 $code .= $self->javascript(qq|\$('#$attributes{id}').checkall('$checkall');|) if $checkall;
120 sub radio_button_tag {
121 my ($self, $name, %attributes) = _hashify(2, @_);
123 $attributes{value} = 1 unless exists $attributes{value};
125 _set_id_attribute(\%attributes, $name, 1);
126 my $label = delete $attributes{label};
128 if ($attributes{checked}) {
129 $attributes{checked} = 'checked';
131 delete $attributes{checked};
134 my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'radio');
135 $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
141 my ($self, $name, $value, %attributes) = _hashify(3, @_);
142 return $self->input_tag($name, $value, %attributes, type => 'hidden');
146 my ($self, $content, @slurp) = @_;
147 return $self->html_tag('div', $content, @slurp);
151 my ($self, $content, @slurp) = @_;
152 return $self->html_tag('ul', $content, @slurp);
156 my ($self, $content, @slurp) = @_;
157 return $self->html_tag('li', $content, @slurp);
161 my ($self, $href, $content, %params) = _hashify(3, @_);
165 return $self->html_tag('a', $content, %params, href => $href);
169 my ($self, $name, $value, %attributes) = _hashify(3, @_);
171 if ( $attributes{confirm} ) {
172 $attributes{onclick} = 'return confirm("'. _J(delete($attributes{confirm})) .'");';
175 return $self->input_tag($name, $value, %attributes, type => 'submit', class => 'submit');
179 my ($self, $onclick, $value, %attributes) = _hashify(3, @_);
181 _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
182 $attributes{type} ||= 'button';
184 $onclick = 'if (!confirm("'. _J(delete($attributes{confirm})) .'")) return false; ' . $onclick if $attributes{confirm};
186 return $self->html_tag('input', undef, %attributes, value => $value, onclick => $onclick);
189 sub ajax_submit_tag {
190 my ($self, $url, $form_selector, $text, @slurp) = @_;
193 $form_selector = _J($form_selector);
194 my $onclick = qq|kivi.submit_ajax_form('${url}', '${form_selector}')|;
196 return $self->button_tag($onclick, $text, @slurp);
200 my ($self, $name, $value, %attributes) = _hashify(3, @_);
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, %params) = _hashify(3, @_);
228 _set_id_attribute(\%params, $name);
229 my @onchange = $params{onchange} ? (onChange => delete $params{onchange}) : ();
230 my @classes = $params{no_cal} || $params{readonly} ? () : ('datepicker');
231 push @classes, delete($params{class}) if $params{class};
232 my %class = @classes ? (class => join(' ', @classes)) : ();
234 $::request->presenter->need_reinit_widgets($params{id});
236 return $self->input_tag(
237 $name, blessed($value) ? $value->to_lxoffice : $value,
239 onblur => "check_right_date_format(this);",
245 # simple version with select_tag
246 sub vendor_selector {
247 my ($self, $name, $value, %params) = _hashify(3, @_);
249 my $actual_vendor_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"}) ? $::form->{"$name"}->id : $::form->{"$name"}) :
250 (ref $value && $value->can('id')) ? $value->id : '';
252 return $self->select_tag($name, SL::DB::Manager::Vendor->get_all(),
253 default => $actual_vendor_id,
254 title_sub => sub { $_[0]->vendornumber . " : " . $_[0]->name },
260 # simple version with select_tag
262 my ($self, $name, $value, %params) = _hashify(3, @_);
264 my $actual_part_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"})? $::form->{"$name"}->id : $::form->{"$name"}) :
265 (ref $value && $value->can('id')) ? $value->id : '';
267 return $self->select_tag($name, SL::DB::Manager::Part->get_all(),
268 default => $actual_part_id,
269 title_sub => sub { $_[0]->partnumber . " : " . $_[0]->description },
279 foreach my $file (@_) {
280 $file .= '.js' unless $file =~ m/\.js$/;
281 $file = "js/${file}" unless $file =~ m|/|;
283 $code .= qq|<script type="text/javascript" src="${file}"></script>|;
290 my ($self, $tabs, %params) = _hashify(2, @_);
291 my $id = $params{id} || 'tab_' . _tag_id();
293 $params{selected} *= 1;
295 die 'L.tabbed needs an arrayred of tabs for first argument'
296 unless ref $tabs eq 'ARRAY';
298 my (@header, @blocks);
299 for my $i (0..$#$tabs) {
300 my $tab = $tabs->[$i];
304 my $tab_id = "__tab_id_$i";
305 push @header, $self->li_tag($self->link('#' . $tab_id, $tab->{name}));
306 push @blocks, $self->div_tag($tab->{data}, id => $tab_id);
309 return '' unless @header;
311 my $ul = $self->ul_tag(join('', @header), id => $id);
312 return $self->div_tag(join('', $ul, @blocks), class => 'tabwidget');
316 my ($self, $name, $src, %params) = _hashify(3, @_);
318 $params{method} ||= 'process';
320 return () if defined $params{if} && !$params{if};
323 if ($params{method} eq 'raw') {
325 } elsif ($params{method} eq 'process') {
326 $data = $self->_context->process($src, %{ $params{args} || {} });
328 die "unknown tag method '$params{method}'";
331 return () unless $data;
333 return +{ name => $name, data => $data };
337 my ($self, $name, $value, %attributes) = _hashify(3, @_);
339 my $cols = delete $attributes{cols} || delete $attributes{size};
340 my $minrows = delete $attributes{min_rows} || 1;
341 my $maxrows = delete $attributes{max_rows};
342 my $rows = $::form->numtextrows($value, $cols, $maxrows, $minrows);
345 ? $self->textarea_tag($name, $value, %attributes, rows => $rows, cols => $cols)
346 : $self->input_tag($name, $value, %attributes, size => $cols);
349 sub multiselect2side {
350 my ($self, $id, %params) = _hashify(2, @_);
352 $params{labelsx} = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
353 $params{labeldx} = "\"" . _J($params{labeldx} || $::locale->text('Selected')) . "\"";
354 $params{moveOptions} = 'false';
356 my $vars = join(', ', map { "${_}: " . $params{$_} } keys %params);
358 <script type="text/javascript">
359 \$().ready(function() {
360 \$('#${id}').multiselect2side({ ${vars} });
368 sub sortable_element {
369 my ($self, $selector, %params) = _hashify(2, @_);
371 my %attributes = ( distance => 5,
372 helper => <<'JAVASCRIPT' );
373 function(event, ui) {
374 ui.children().each(function() {
375 $(this).width($(this).width());
383 if ($params{url} && $params{with}) {
384 my $as = $params{as} || $params{with};
385 my $filter = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
386 $filter .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
388 my $params_js = $params{params} ? qq| + ($params{params})| : '';
390 $stop_event = <<JAVASCRIPT;
391 \$.post('$params{url}'${params_js}, { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() });
395 if (!$params{dont_recolor}) {
396 $stop_event .= <<JAVASCRIPT;
397 \$('${selector}>*:odd').removeClass('listrow1').removeClass('listrow0').addClass('listrow0');
398 \$('${selector}>*:even').removeClass('listrow1').removeClass('listrow0').addClass('listrow1');
403 $attributes{stop} = <<JAVASCRIPT;
404 function(event, ui) {
411 $params{handle} = '.dragdrop' unless exists $params{handle};
412 $attributes{handle} = "'$params{handle}'" if $params{handle};
414 my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
416 my $code = <<JAVASCRIPT;
417 <script type="text/javascript">
419 \$( "${selector}" ).sortable({ ${attr_str} })
429 return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
432 sub sortable_table_header {
433 my ($self, $by, %params) = _hashify(2, @_);
435 my $controller = $self->{CONTEXT}->stash->get('SELF');
436 my $models = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
437 my $sort_spec = $models->get_sort_spec;
438 my $by_spec = $sort_spec->{$by};
439 my %current_sort_params = $models->get_current_sort_params;
440 my ($image, $new_dir) = ('', $current_sort_params{dir});
441 my $title = delete($params{title}) || $::locale->text($by_spec->{title});
443 if ($current_sort_params{sort_by} eq $by) {
444 my $current_dir = $current_sort_params{sort_dir} ? 'up' : 'down';
445 $image = '<img border="0" src="image/' . $current_dir . '.png">';
446 $new_dir = 1 - ($current_sort_params{sort_dir} || 0);
449 $params{ $models->sorted->form_params->[0] } = $by;
450 $params{ $models->sorted->form_params->[1] } = ($new_dir ? '1' : '0');
452 return '<a href="' . $models->get_callback(%params) . '">' . _H($title) . $image . '</a>';
455 sub paginate_controls {
456 my ($self, %params) = _hashify(1, @_);
458 my $controller = $self->{CONTEXT}->stash->get('SELF');
459 my $models = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
460 my $pager = $models->paginated;
461 # my $paginate_spec = $controller->get_paginate_spec;
463 my %paginate_params = $models->get_paginate_args;
465 my %template_params = (
466 pages => \%paginate_params,
468 my %url_params = _hashify(0, @_);
469 $url_params{ $pager->form_params->[0] } = delete $url_params{page};
470 $url_params{ $pager->form_params->[1] } = delete $url_params{per_page} if exists $url_params{per_page};
472 return $models->get_callback(%url_params);
477 return SL::Presenter->get->render('common/paginate', %template_params);
486 SL::Templates::Plugin::L -- Layouting / tag generation
490 Usage from a template:
494 [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right', 1 ] ]) %]
496 [% L.select_tag('direction', [ { direction => 'left', display => 'To the left' },
497 { direction => 'right', display => 'To the right' } ],
498 value_key => 'direction', title_key => 'display', default => 'right')) %]
500 [% L.select_tag('direction', [ { direction => 'left', display => 'To the left' },
501 { direction => 'right', display => 'To the right', selected => 1 } ],
502 value_key => 'direction', title_key => 'display')) %]
506 A module modeled a bit after Rails' ActionView helpers. Several small
507 functions that create HTML tags from various kinds of data sources.
509 The C<id> attribute is usually calculated automatically. This can be
510 overridden by either specifying an C<id> attribute or by setting
515 =head2 LOW-LEVEL FUNCTIONS
517 The following items are just forwarded to L<SL::Presenter::Tag>:
521 =item * C<name_to_id $name>
523 =item * C<stringify_attributes %items>
525 =item * C<html_tag $tag_name, $content_string, %attributes>
529 =head2 HIGH-LEVEL FUNCTIONS
531 The following functions are just forwarded to L<SL::Presenter::Tag>:
535 =item * C<input_tag $name, $value, %attributes>
537 =item * C<select_tag $name, \@collection, %attributes>
541 Available high-level functions implemented in this module:
545 =item C<yes_no_tag $name, $value, %attributes>
547 Creates a HTML 'select' tag with the two entries C<yes> and C<no> by
548 calling L<select_tag>. C<$value> determines
549 which entry is selected. The C<%attributes> are passed through to
552 =item C<hidden_tag $name, $value, %attributes>
554 Creates a HTML 'input type=hidden' tag named C<$name> with the value
555 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
556 tag's C<id> defaults to C<name_to_id($name)>.
558 =item C<submit_tag $name, $value, %attributes>
560 Creates a HTML 'input type=submit class=submit' tag named C<$name> with the
561 value C<$value> and with arbitrary HTML attributes from C<%attributes>. The
562 tag's C<id> defaults to C<name_to_id($name)>.
564 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
565 be added via the C<onclick> handler asking the question given with
566 C<$attributes{confirm}>. The request is only submitted if the user
567 clicks the dialog's ok/yes button.
569 =item C<ajax_submit_tag $url, $form_selector, $text, %attributes>
571 Creates a HTML 'input type="button"' tag with a very specific onclick
572 handler that submits the form given by the jQuery selector
573 C<$form_selector> to the URL C<$url> (the actual JavaScript function
574 called for that is C<kivi.submit_ajax_form()> in
575 C<js/client_js.js>). The button's label will be C<$text>.
577 =item C<button_tag $onclick, $text, %attributes>
579 Creates a HTML 'input type="button"' tag with an onclick handler
580 C<$onclick> and a value of C<$text>. The button does not have a name
581 nor an ID by default.
583 If C<$attributes{confirm}> is set then a JavaScript popup dialog will
584 be prepended to the C<$onclick> handler asking the question given with
585 C<$attributes{confirm}>. The request is only submitted if the user
586 clicks the dialog's "ok/yes" button.
588 =item C<textarea_tag $name, $value, %attributes>
590 Creates a HTML 'textarea' tag named C<$name> with the content
591 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
592 tag's C<id> defaults to C<name_to_id($name)>.
594 =item C<checkbox_tag $name, %attributes>
596 Creates a HTML 'input type=checkbox' tag named C<$name> with arbitrary
597 HTML attributes from C<%attributes>. The tag's C<id> defaults to
598 C<name_to_id($name)>. The tag's C<value> defaults to C<1>.
600 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
601 created with said C<label>. No attribute named C<label> is created in
604 If C<%attributes> contains a key C<checkall> then the value is taken as a
605 JQuery selector and clicking this checkbox will also toggle all checkboxes
606 matching the selector.
608 =item C<date_tag $name, $value, %attributes>
610 Creates a date input field, with an attached javascript that will open a
613 =item C<radio_button_tag $name, %attributes>
615 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
616 HTML attributes from C<%attributes>. The tag's C<value> defaults to
617 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
619 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
620 created with said C<label>. No attribute named C<label> is created in
623 =item C<javascript_tag $file1, $file2, $file3...>
625 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
626 tag for each file name parameter passed. Each file name will be
627 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
628 doesn't contain a slash.
630 =item C<stylesheet_tag $file1, $file2, $file3...>
632 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
633 for each file name parameter passed. Each file name will be postfixed
634 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
637 =item C<tabbed \@tab, %attributes>
639 Will create a tabbed area. The tabs should be created with the helper function
643 L.tab(LxERP.t8('Basic Data'), 'part/_main_tab.html'),
644 L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
647 =item C<areainput_tag $name, $content, %PARAMS>
649 Creates a generic input tag or textarea tag, depending on content size. The
650 amount of desired rows must be either given with the C<rows> parameter or can
651 be computed from the value and the C<cols> paramter, Accepted parameters
652 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
654 You can force input by setting rows to 1, and you can force textarea by setting
657 =item C<multiselect2side $id, %params>
659 Creates a JavaScript snippet calling the jQuery function
660 C<multiselect2side> on the select control with the ID C<$id>. The
661 select itself is not created. C<%params> can contain the following
668 The label of the list of available options. Defaults to the
669 translation of 'Available'.
673 The label of the list of selected options. Defaults to the
674 translation of 'Selected'.
678 =item C<sortable_element $selector, %params>
680 Makes the children of the DOM element C<$selector> (a jQuery selector)
681 sortable with the I<jQuery UI Selectable> library. The children can be
682 dragged & dropped around. After dropping an element an URL can be
683 postet to with the element IDs of the sorted children.
685 If this is used then the JavaScript file C<js/jquery-ui.js> must be
686 included manually as well as it isn't loaded via C<$::form-gt;header>.
688 C<%params> can contain the following entries:
694 The URL to POST an AJAX request to after a dragged element has been
695 dropped. The AJAX request's return value is ignored. If given then
696 C<$params{with}> must be given as well.
700 A string that is interpreted as the prefix of the children's ID. Upon
701 POSTing the result each child whose ID starts with C<$params{with}> is
702 considered. The prefix and the following "_" is removed from the
703 ID. The remaining parts of the IDs of those children are posted as a
704 single array parameter. The array parameter's name is either
705 C<$params{as}> or, missing that, C<$params{with}>.
709 Sets the POST parameter name for AJAX request after dropping an
710 element (see C<$params{with}>).
714 An optional jQuery selector specifying which part of the child element
715 is dragable. If the parameter is not given then it defaults to
716 C<.dragdrop> matching DOM elements with the class C<dragdrop>. If the
717 parameter is set and empty then the whole child element is dragable,
718 and clicks through to underlying elements like inputs or links might
721 =item C<dont_recolor>
723 If trueish then the children will not be recolored. The default is to
724 recolor the children by setting the class C<listrow0> on odd and
725 C<listrow1> on even entries.
729 An optional JavaScript string that is evaluated before sending the
730 POST request. The result must be a string that is appended to the URL.
736 <script type="text/javascript" src="js/jquery-ui.js"></script>
738 <table id="thing_list">
740 <tr><td>This</td><td>That</td></tr>
743 <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
744 <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
745 <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
749 [% L.sortable_element('#thing_list tbody',
750 url => 'controller.pl?action=SystemThings/reorder',
753 recolor_rows => 1) %]
755 After dropping e.g. the third element at the top of the list a POST
756 request would be made to the C<reorder> action of the C<SystemThings>
757 controller with a single parameter called C<thing_ids> -- an array
758 containing the values C<[ 6, 2, 15 ]>.
762 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
764 =item C<sortable_table_header $by, %params>
766 Create a link and image suitable for placement in a table
767 header. C<$by> must be an index set up by the controller with
768 L<SL::Controller::Helper::make_sorted>.
770 The optional parameter C<$params{title}> can override the column title
771 displayed to the user. Otherwise the column title from the
772 controller's sort spec is used.
774 The other parameters in C<%params> are passed unmodified to the
775 underlying call to L<SL::Controller::Base::url_for>.
777 See the documentation of L<SL::Controller::Helper::Sorted> for an
778 overview and further usage instructions.
780 =item C<paginate_controls>
782 Create a set of links used to paginate a list view.
784 See the documentation of L<SL::Controller::Helper::Paginated> for an
785 overview and further usage instructions.
789 =head2 CONVERSION FUNCTIONS
793 =item C<tab, description, target, %PARAMS>
795 Creates a tab for C<tabbed>. The description will be used as displayed name.
796 The target should be a block or template that can be processed. C<tab> supports
797 a C<method> parameter, which can override the process method to apply target.
798 C<method => 'raw'> will just include the given text as is. I was too lazy to
799 implement C<include> properly.
801 Also an C<if> attribute is supported, so that tabs can be suppressed based on
802 some occasion. In this case the supplied block won't even get processed, and
803 the resulting tab will get ignored by C<tabbed>:
805 L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
807 =item C<truncate $text, [%params]>
809 See L<SL::Presenter::Text/truncate>.
811 =item C<simple_format $text>
813 See L<SL::Presenter::Text/simple_format>.
817 =head1 MODULE AUTHORS
819 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
821 L<http://linet-services.de>