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::Presenter::ALL;
12 use SL::Presenter::Simple;
13 use SL::Util qw(_hashify);
17 { # This will give you an id for identifying html tags and such.
18 # It's guaranteed to be unique unless you exceed 10 mio calls per request.
19 # Do not use these id's to store information across requests.
20 my $_id_sequence = int rand 1e7;
22 return "id_" . ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
28 return $::locale->quote_special_chars('HTML', $string);
33 $string =~ s/(\"|\'|\\)/\\$1/g;
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 splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
57 if (my $sub = SL::Presenter::Simple->can($method)) {
61 if ($presenter->can($method)) {
62 return $presenter->$method(@args);
65 $::lxdebug->message(LXDebug::WARN(), "SL::Presenter has no method named '$method'!");
69 sub name_to_id { return _call_presenter('name_to_id', @_); }
70 sub html_tag { return _call_presenter('html_tag', @_); }
71 sub hidden_tag { return _call_presenter('hidden_tag', @_); }
72 sub select_tag { return _call_presenter('select_tag', @_); }
73 sub checkbox_tag { return _call_presenter('checkbox_tag', @_); }
74 sub input_tag { return _call_presenter('input_tag', @_); }
75 sub javascript { return _call_presenter('javascript', @_); }
76 sub truncate { return _call_presenter('truncate', @_); }
77 sub simple_format { return _call_presenter('simple_format', @_); }
78 sub button_tag { return _call_presenter('button_tag', @_); }
79 sub submit_tag { return _call_presenter('submit_tag', @_); }
80 sub ajax_submit_tag { return _call_presenter('ajax_submit_tag', @_); }
81 sub link { return _call_presenter('link', @_); }
82 sub input_number_tag { return _call_presenter('input_number_tag', @_); }
84 sub _set_id_attribute {
85 my ($attributes, $name, $unique) = @_;
86 SL::Presenter::Tag::_set_id_attribute($attributes, $name, $unique);
90 my ($self, %options) = _hashify(1, @_);
94 return $self->html_tag('img', undef, %options);
98 my ($self, $name, $content, %attributes) = _hashify(3, @_);
100 _set_id_attribute(\%attributes, $name);
101 $attributes{rows} *= 1; # required by standard
102 $attributes{cols} *= 1; # required by standard
103 $content = $content ? _H($content) : '';
105 return $self->html_tag('textarea', $content, %attributes, name => $name);
108 sub radio_button_tag {
109 my ($self, $name, %attributes) = _hashify(2, @_);
111 $attributes{value} = 1 unless exists $attributes{value};
113 _set_id_attribute(\%attributes, $name, 1);
114 my $label = delete $attributes{label};
116 _set_id_attribute(\%attributes, $name . '_' . $attributes{value});
118 if ($attributes{checked}) {
119 $attributes{checked} = 'checked';
121 delete $attributes{checked};
124 my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'radio');
125 $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
131 my ($self, $content, @slurp) = @_;
132 return $self->html_tag('div', $content, @slurp);
136 my ($self, $content, @slurp) = @_;
137 return $self->html_tag('ul', $content, @slurp);
141 my ($self, $content, @slurp) = @_;
142 return $self->html_tag('li', $content, @slurp);
146 my ($self, $name, $value, %attributes) = _hashify(3, @_);
148 return $self->select_tag($name, [ [ 1 => $::locale->text('Yes') ], [ 0 => $::locale->text('No') ] ], default => $value ? 1 : 0, %attributes);
155 foreach my $file (@_) {
156 $file .= '.css' unless $file =~ m/\.css$/;
157 $file = "css/${file}" unless $file =~ m|/|;
159 $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
165 my $date_tag_id_idx = 0;
167 my ($self, $name, $value, %params) = _hashify(3, @_);
169 _set_id_attribute(\%params, $name);
170 my @onchange = $params{onchange} ? (onChange => delete $params{onchange}) : ();
171 my @classes = $params{no_cal} || $params{readonly} ? () : ('datepicker');
172 push @classes, delete($params{class}) if $params{class};
173 my %class = @classes ? (class => join(' ', @classes)) : ();
175 $::request->layout->add_javascripts('kivi.Validator.js');
176 $::request->presenter->need_reinit_widgets($params{id});
178 return $self->input_tag(
179 $name, blessed($value) ? $value->to_lxoffice : $value,
181 "data-validate" => "date",
187 # simple version with select_tag
188 sub vendor_selector {
189 my ($self, $name, $value, %params) = _hashify(3, @_);
191 my $actual_vendor_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"}) ? $::form->{"$name"}->id : $::form->{"$name"}) :
192 (ref $value && $value->can('id')) ? $value->id : '';
194 return $self->select_tag($name, SL::DB::Manager::Vendor->get_all(),
195 default => $actual_vendor_id,
196 title_sub => sub { $_[0]->vendornumber . " : " . $_[0]->name },
202 # simple version with select_tag
204 my ($self, $name, $value, %params) = _hashify(3, @_);
206 my $actual_part_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"})? $::form->{"$name"}->id : $::form->{"$name"}) :
207 (ref $value && $value->can('id')) ? $value->id : '';
209 return $self->select_tag($name, SL::DB::Manager::Part->get_all(),
210 default => $actual_part_id,
211 title_sub => sub { $_[0]->partnumber . " : " . $_[0]->description },
221 foreach my $file (@_) {
222 $file .= '.js' unless $file =~ m/\.js$/;
223 $file = "js/${file}" unless $file =~ m|/|;
225 $code .= qq|<script type="text/javascript" src="${file}"></script>|;
232 my ($self, $tabs, %params) = _hashify(2, @_);
233 my $id = $params{id} || 'tab_' . _tag_id();
235 $params{selected} *= 1;
237 die 'L.tabbed needs an arrayred of tabs for first argument'
238 unless ref $tabs eq 'ARRAY';
240 my (@header, @blocks);
241 for my $i (0..$#$tabs) {
242 my $tab = $tabs->[$i];
246 my $tab_id = "__tab_id_$i";
247 push @header, $self->li_tag($self->link('#' . $tab_id, $tab->{name}));
248 push @blocks, $self->div_tag($tab->{data}, id => $tab_id);
251 return '' unless @header;
253 my $ul = $self->ul_tag(join('', @header), id => $id);
254 return $self->div_tag(join('', $ul, @blocks), class => 'tabwidget');
258 my ($self, $name, $src, %params) = _hashify(3, @_);
260 $params{method} ||= 'process';
262 return () if defined $params{if} && !$params{if};
265 if ($params{method} eq 'raw') {
267 } elsif ($params{method} eq 'process') {
268 $data = $self->_context->process($src, %{ $params{args} || {} });
270 die "unknown tag method '$params{method}'";
273 return () unless $data;
275 return +{ name => $name, data => $data };
279 my ($self, $name, $value, %attributes) = _hashify(3, @_);
281 my $cols = delete $attributes{cols} || delete $attributes{size};
282 my $minrows = delete $attributes{min_rows} || 1;
283 my $maxrows = delete $attributes{max_rows};
284 my $rows = $::form->numtextrows($value, $cols, $maxrows, $minrows);
286 $attributes{id} ||= _tag_id();
287 my $id = $attributes{id};
289 return $self->textarea_tag($name, $value, %attributes, rows => $rows, cols => $cols) if $rows > 1;
292 . $self->input_tag($name, $value, %attributes, size => $cols)
293 . "<img src=\"image/edit-entry.png\" onclick=\"kivi.switch_areainput_to_textarea('${id}')\" style=\"margin-left: 2px;\">"
297 sub multiselect2side {
298 my ($self, $id, %params) = _hashify(2, @_);
300 $params{labelsx} = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
301 $params{labeldx} = "\"" . _J($params{labeldx} || $::locale->text('Selected')) . "\"";
302 $params{moveOptions} = 'false';
304 my $vars = join(', ', map { "${_}: " . $params{$_} } keys %params);
306 <script type="text/javascript">
307 \$().ready(function() {
308 \$('#${id}').multiselect2side({ ${vars} });
316 sub sortable_element {
317 my ($self, $selector, %params) = _hashify(2, @_);
319 my %attributes = ( distance => 5,
320 helper => <<'JAVASCRIPT' );
321 function(event, ui) {
322 ui.children().each(function() {
323 $(this).width($(this).width());
331 if ($params{url} && $params{with}) {
332 my $as = $params{as} || $params{with};
333 my $filter = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
334 $filter .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
336 my $params_js = $params{params} ? qq| + ($params{params})| : '';
338 $stop_event = <<JAVASCRIPT;
339 \$.post('$params{url}'${params_js}, { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() });
343 if (!$params{dont_recolor}) {
344 $stop_event .= <<JAVASCRIPT;
345 \$('${selector}>*:odd').removeClass('listrow1').removeClass('listrow0').addClass('listrow0');
346 \$('${selector}>*:even').removeClass('listrow1').removeClass('listrow0').addClass('listrow1');
351 $attributes{stop} = <<JAVASCRIPT;
352 function(event, ui) {
359 $params{handle} = '.dragdrop' unless exists $params{handle};
360 $attributes{handle} = "'$params{handle}'" if $params{handle};
362 my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
364 my $code = <<JAVASCRIPT;
365 <script type="text/javascript">
367 \$( "${selector}" ).sortable({ ${attr_str} })
377 return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
380 sub sortable_table_header {
381 my ($self, $by, %params) = _hashify(2, @_);
383 my $controller = $self->{CONTEXT}->stash->get('SELF');
384 my $models = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
385 my $sort_spec = $models->get_sort_spec;
386 my $by_spec = $sort_spec->{$by};
387 my %current_sort_params = $models->get_current_sort_params;
388 my ($image, $new_dir) = ('', $current_sort_params{dir});
389 my $title = delete($params{title}) || $::locale->text($by_spec->{title});
391 if ($current_sort_params{sort_by} eq $by) {
392 my $current_dir = $current_sort_params{sort_dir} ? 'up' : 'down';
393 $image = '<img border="0" src="image/' . $current_dir . '.png">';
394 $new_dir = 1 - ($current_sort_params{sort_dir} || 0);
397 $params{ $models->sorted->form_params->[0] } = $by;
398 $params{ $models->sorted->form_params->[1] } = ($new_dir ? '1' : '0');
400 return '<a href="' . $models->get_callback(%params) . '">' . _H($title) . $image . '</a>';
403 sub paginate_controls {
404 my ($self, %params) = _hashify(1, @_);
406 my $controller = $self->{CONTEXT}->stash->get('SELF');
407 my $models = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
408 my $pager = $models->paginated;
409 # my $paginate_spec = $controller->get_paginate_spec;
411 my %paginate_params = $models->get_paginate_args;
413 my %template_params = (
414 pages => \%paginate_params,
416 my %url_params = _hashify(0, @_);
417 $url_params{ $pager->form_params->[0] } = delete $url_params{page};
418 $url_params{ $pager->form_params->[1] } = delete $url_params{per_page} if exists $url_params{per_page};
420 return $models->get_callback(%url_params);
425 return SL::Presenter->get->render('common/paginate', %template_params);
434 SL::Templates::Plugin::L -- Layouting / tag generation
438 Usage from a template:
442 [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right', 1 ] ]) %]
444 [% L.select_tag('direction', [ { direction => 'left', display => 'To the left' },
445 { direction => 'right', display => 'To the right' } ],
446 value_key => 'direction', title_key => 'display', default => 'right')) %]
448 [% L.select_tag('direction', [ { direction => 'left', display => 'To the left' },
449 { direction => 'right', display => 'To the right', selected => 1 } ],
450 value_key => 'direction', title_key => 'display')) %]
454 A module modeled a bit after Rails' ActionView helpers. Several small
455 functions that create HTML tags from various kinds of data sources.
457 The C<id> attribute is usually calculated automatically. This can be
458 overridden by either specifying an C<id> attribute or by setting
463 =head2 LOW-LEVEL FUNCTIONS
465 The following items are just forwarded to L<SL::Presenter::Tag>:
469 =item * C<name_to_id $name>
471 =item * C<stringify_attributes %items>
473 =item * C<html_tag $tag_name, $content_string, %attributes>
477 =head2 HIGH-LEVEL FUNCTIONS
479 The following functions are just forwarded to L<SL::Presenter::Tag>:
483 =item * C<input_tag $name, $value, %attributes>
485 =item * C<hidden_tag $name, $value, %attributes>
487 =item * C<checkbox_tag $name, %attributes>
489 =item * C<select_tag $name, \@collection, %attributes>
491 =item * C<link $href, $content, %attributes>
495 Available high-level functions implemented in this module:
499 =item C<yes_no_tag $name, $value, %attributes>
501 Creates a HTML 'select' tag with the two entries C<yes> and C<no> by
502 calling L<select_tag>. C<$value> determines
503 which entry is selected. The C<%attributes> are passed through to
506 =item C<textarea_tag $name, $value, %attributes>
508 Creates a HTML 'textarea' tag named C<$name> with the content
509 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
510 tag's C<id> defaults to C<name_to_id($name)>.
512 =item C<date_tag $name, $value, %attributes>
514 Creates a date input field, with an attached javascript that will open a
517 =item C<radio_button_tag $name, %attributes>
519 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
520 HTML attributes from C<%attributes>. The tag's C<value> defaults to
521 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
523 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
524 created with said C<label>. No attribute named C<label> is created in
527 =item C<javascript_tag $file1, $file2, $file3...>
529 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
530 tag for each file name parameter passed. Each file name will be
531 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
532 doesn't contain a slash.
534 =item C<stylesheet_tag $file1, $file2, $file3...>
536 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
537 for each file name parameter passed. Each file name will be postfixed
538 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
541 =item C<tabbed \@tab, %attributes>
543 Will create a tabbed area. The tabs should be created with the helper function
547 L.tab(LxERP.t8('Basic Data'), 'part/_main_tab.html'),
548 L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
551 =item C<areainput_tag $name, $content, %PARAMS>
553 Creates a generic input tag or textarea tag, depending on content size. The
554 amount of desired rows must be either given with the C<rows> parameter or can
555 be computed from the value and the C<cols> paramter, Accepted parameters
556 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
558 You can force input by setting rows to 1, and you can force textarea by setting
561 =item C<multiselect2side $id, %params>
563 Creates a JavaScript snippet calling the jQuery function
564 C<multiselect2side> on the select control with the ID C<$id>. The
565 select itself is not created. C<%params> can contain the following
572 The label of the list of available options. Defaults to the
573 translation of 'Available'.
577 The label of the list of selected options. Defaults to the
578 translation of 'Selected'.
582 =item C<sortable_element $selector, %params>
584 Makes the children of the DOM element C<$selector> (a jQuery selector)
585 sortable with the I<jQuery UI Selectable> library. The children can be
586 dragged & dropped around. After dropping an element an URL can be
587 postet to with the element IDs of the sorted children.
589 If this is used then the JavaScript file C<js/jquery-ui.js> must be
590 included manually as well as it isn't loaded via C<$::form-gt;header>.
592 C<%params> can contain the following entries:
598 The URL to POST an AJAX request to after a dragged element has been
599 dropped. The AJAX request's return value is ignored. If given then
600 C<$params{with}> must be given as well.
604 A string that is interpreted as the prefix of the children's ID. Upon
605 POSTing the result each child whose ID starts with C<$params{with}> is
606 considered. The prefix and the following "_" is removed from the
607 ID. The remaining parts of the IDs of those children are posted as a
608 single array parameter. The array parameter's name is either
609 C<$params{as}> or, missing that, C<$params{with}>.
613 Sets the POST parameter name for AJAX request after dropping an
614 element (see C<$params{with}>).
618 An optional jQuery selector specifying which part of the child element
619 is dragable. If the parameter is not given then it defaults to
620 C<.dragdrop> matching DOM elements with the class C<dragdrop>. If the
621 parameter is set and empty then the whole child element is dragable,
622 and clicks through to underlying elements like inputs or links might
625 =item C<dont_recolor>
627 If trueish then the children will not be recolored. The default is to
628 recolor the children by setting the class C<listrow0> on odd and
629 C<listrow1> on even entries.
633 An optional JavaScript string that is evaluated before sending the
634 POST request. The result must be a string that is appended to the URL.
640 <script type="text/javascript" src="js/jquery-ui.js"></script>
642 <table id="thing_list">
644 <tr><td>This</td><td>That</td></tr>
647 <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
648 <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
649 <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
653 [% L.sortable_element('#thing_list tbody',
654 url => 'controller.pl?action=SystemThings/reorder',
657 recolor_rows => 1) %]
659 After dropping e.g. the third element at the top of the list a POST
660 request would be made to the C<reorder> action of the C<SystemThings>
661 controller with a single parameter called C<thing_ids> -- an array
662 containing the values C<[ 6, 2, 15 ]>.
666 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
668 =item C<sortable_table_header $by, %params>
670 Create a link and image suitable for placement in a table
671 header. C<$by> must be an index set up by the controller with
672 L<SL::Controller::Helper::make_sorted>.
674 The optional parameter C<$params{title}> can override the column title
675 displayed to the user. Otherwise the column title from the
676 controller's sort spec is used.
678 The other parameters in C<%params> are passed unmodified to the
679 underlying call to L<SL::Controller::Base::url_for>.
681 See the documentation of L<SL::Controller::Helper::Sorted> for an
682 overview and further usage instructions.
684 =item C<paginate_controls>
686 Create a set of links used to paginate a list view.
688 See the documentation of L<SL::Controller::Helper::Paginated> for an
689 overview and further usage instructions.
693 =head2 CONVERSION FUNCTIONS
697 =item C<tab, description, target, %PARAMS>
699 Creates a tab for C<tabbed>. The description will be used as displayed name.
700 The target should be a block or template that can be processed. C<tab> supports
701 a C<method> parameter, which can override the process method to apply target.
702 C<method => 'raw'> will just include the given text as is. I was too lazy to
703 implement C<include> properly.
705 Also an C<if> attribute is supported, so that tabs can be suppressed based on
706 some occasion. In this case the supplied block won't even get processed, and
707 the resulting tab will get ignored by C<tabbed>:
709 L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
711 =item C<truncate $text, [%params]>
713 See L<SL::Presenter::Text/truncate>.
715 =item C<simple_format $text>
717 See L<SL::Presenter::Text/simple_format>.
721 =head1 MODULE AUTHORS
723 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
725 L<http://linet-services.de>