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_tag', @_); }
82 sub input_number_tag { return _call_presenter('input_number_tag', @_); }
83 sub textarea_tag { return _call_presenter('textarea_tag', @_); }
84 sub date_tag { return _call_presenter('date_tag', @_); }
85 sub div_tag { return _call_presenter('div_tag', @_); }
86 sub radio_button_tag { return _call_presenter('radio_button_tag', @_); }
87 sub img_tag { return _call_presenter('img_tag', @_); }
88 sub restricted_html { return _call_presenter('restricted_html', @_); }
89 sub stripped_html { return _call_presenter('stripped_html', @_); }
91 sub _set_id_attribute {
92 my ($attributes, $name, $unique) = @_;
93 SL::Presenter::Tag::_set_id_attribute($attributes, $name, $unique);
97 my ($self, $content, @slurp) = @_;
98 return $self->html_tag('ul', $content, @slurp);
102 my ($self, $content, @slurp) = @_;
103 return $self->html_tag('li', $content, @slurp);
107 my ($self, $name, $value, %attributes) = _hashify(3, @_);
109 return $self->select_tag($name, [ [ 1 => $::locale->text('Yes') ], [ 0 => $::locale->text('No') ] ], default => $value ? 1 : 0, %attributes);
116 foreach my $file (@_) {
117 $file .= '.css' unless $file =~ m/\.css$/;
118 $file = "css/${file}" unless $file =~ m|/|;
120 $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
127 # simple version with select_tag
128 sub vendor_selector {
129 my ($self, $name, $value, %params) = _hashify(3, @_);
131 my $actual_vendor_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"}) ? $::form->{"$name"}->id : $::form->{"$name"}) :
132 (ref $value && $value->can('id')) ? $value->id : '';
134 return $self->select_tag($name, SL::DB::Manager::Vendor->get_all(),
135 default => $actual_vendor_id,
136 title_sub => sub { $_[0]->vendornumber . " : " . $_[0]->name },
142 # simple version with select_tag
144 my ($self, $name, $value, %params) = _hashify(3, @_);
146 my $actual_part_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"})? $::form->{"$name"}->id : $::form->{"$name"}) :
147 (ref $value && $value->can('id')) ? $value->id : '';
149 return $self->select_tag($name, SL::DB::Manager::Part->get_all(),
150 default => $actual_part_id,
151 title_sub => sub { $_[0]->partnumber . " : " . $_[0]->description },
161 foreach my $file (@_) {
162 $file .= '.js' unless $file =~ m/\.js$/;
163 $file = "js/${file}" unless $file =~ m|/|;
165 $code .= qq|<script type="text/javascript" src="${file}"></script>|;
172 my ($self, $tabs, %params) = _hashify(2, @_);
173 my $id = $params{id} || 'tab_' . _tag_id();
175 $params{selected} *= 1;
177 die 'L.tabbed needs an arrayred of tabs for first argument'
178 unless ref $tabs eq 'ARRAY';
180 my (@header, @blocks);
181 for my $i (0..$#$tabs) {
182 my $tab = $tabs->[$i];
186 my $tab_id = "__tab_id_$i";
187 push @header, $self->li_tag($self->link('#' . $tab_id, $tab->{name}));
188 push @blocks, $self->div_tag($tab->{data}, id => $tab_id);
191 return '' unless @header;
193 my $ul = $self->ul_tag(join('', @header), id => $id);
194 return $self->div_tag(join('', $ul, @blocks), class => 'tabwidget');
198 my ($self, $name, $src, %params) = _hashify(3, @_);
200 $params{method} ||= 'process';
202 return () if defined $params{if} && !$params{if};
205 if ($params{method} eq 'raw') {
207 } elsif ($params{method} eq 'process') {
208 $data = $self->_context->process($src, %{ $params{args} || {} });
210 die "unknown tag method '$params{method}'";
213 return () unless $data;
215 return +{ name => $name, data => $data };
219 my ($self, $name, $value, %attributes) = _hashify(3, @_);
221 my $cols = delete $attributes{cols} || delete $attributes{size};
222 my $minrows = delete $attributes{min_rows} || 1;
223 my $maxrows = delete $attributes{max_rows};
224 my $rows = $::form->numtextrows($value, $cols, $maxrows, $minrows);
226 $attributes{id} ||= _tag_id();
227 my $id = $attributes{id};
229 return $self->textarea_tag($name, $value, %attributes, rows => $rows, cols => $cols) if $rows > 1;
232 . $self->input_tag($name, $value, %attributes, size => $cols)
233 . "<img src=\"image/edit-entry.png\" onclick=\"kivi.switch_areainput_to_textarea('${id}')\" style=\"margin-left: 2px;\">"
237 sub multiselect2side {
238 my ($self, $id, %params) = _hashify(2, @_);
240 $params{labelsx} = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
241 $params{labeldx} = "\"" . _J($params{labeldx} || $::locale->text('Selected')) . "\"";
242 $params{moveOptions} = 'false';
244 my $vars = join(', ', map { "${_}: " . $params{$_} } keys %params);
246 <script type="text/javascript">
247 \$().ready(function() {
248 \$('#${id}').multiselect2side({ ${vars} });
256 sub sortable_element {
257 my ($self, $selector, %params) = _hashify(2, @_);
259 my %attributes = ( distance => 5,
260 helper => <<'JAVASCRIPT' );
261 function(event, ui) {
262 ui.children().each(function() {
263 $(this).width($(this).width());
271 if ($params{url} && $params{with}) {
272 my $as = $params{as} || $params{with};
273 my $filter = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
274 $filter .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
276 my $params_js = $params{params} ? qq| + ($params{params})| : '';
277 my $ajax_return = '';
278 if ($params{ajax_return}) {
279 $ajax_return = 'kivi.eval_json_result';
282 $stop_event = <<JAVASCRIPT;
283 \$.post('$params{url}'${params_js}, { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() }, $ajax_return);
287 if (!$params{dont_recolor}) {
288 $stop_event .= <<JAVASCRIPT;
289 \$('${selector}>*:odd').removeClass('listrow1').removeClass('listrow0').addClass('listrow0');
290 \$('${selector}>*:even').removeClass('listrow1').removeClass('listrow0').addClass('listrow1');
295 $attributes{stop} = <<JAVASCRIPT;
296 function(event, ui) {
303 $params{handle} = '.dragdrop' unless exists $params{handle};
304 $attributes{handle} = "'$params{handle}'" if $params{handle};
306 my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
308 my $code = <<JAVASCRIPT;
309 <script type="text/javascript">
311 \$( "${selector}" ).sortable({ ${attr_str} })
321 return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
324 sub sortable_table_header {
325 my ($self, $by, %params) = _hashify(2, @_);
327 my $controller = $self->{CONTEXT}->stash->get('SELF');
328 my $models = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
329 my $sort_spec = $models->get_sort_spec;
330 my $by_spec = $sort_spec->{$by};
331 my %current_sort_params = $models->get_current_sort_params;
332 my ($image, $new_dir) = ('', $current_sort_params{dir});
333 my $title = delete($params{title}) || $::locale->text($by_spec->{title});
335 if ($current_sort_params{sort_by} eq $by) {
336 my $current_dir = $current_sort_params{sort_dir} ? 'up' : 'down';
337 $image = '<img border="0" src="image/' . $current_dir . '.png">';
338 $new_dir = 1 - ($current_sort_params{sort_dir} || 0);
341 $params{ $models->sorted->form_params->[0] } = $by;
342 $params{ $models->sorted->form_params->[1] } = ($new_dir ? '1' : '0');
344 return '<a href="' . $models->get_callback(%params) . '">' . _H($title) . $image . '</a>';
347 sub paginate_controls {
348 my ($self, %params) = _hashify(1, @_);
350 my $controller = $self->{CONTEXT}->stash->get('SELF');
351 my $models = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
352 my $pager = $models->paginated;
353 # my $paginate_spec = $controller->get_paginate_spec;
355 my %paginate_params = $models->get_paginate_args;
357 my %template_params = (
358 pages => \%paginate_params,
360 my %url_params = _hashify(0, @_);
361 $url_params{ $pager->form_params->[0] } = delete $url_params{page};
362 $url_params{ $pager->form_params->[1] } = delete $url_params{per_page} if exists $url_params{per_page};
364 return $models->get_callback(%url_params);
369 return SL::Presenter->get->render('common/paginate', %template_params);
378 SL::Templates::Plugin::L -- Layouting / tag generation
382 Usage from a template:
386 [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right', 1 ] ]) %]
388 [% L.select_tag('direction', [ { direction => 'left', display => 'To the left' },
389 { direction => 'right', display => 'To the right' } ],
390 value_key => 'direction', title_key => 'display', default => 'right')) %]
392 [% L.select_tag('direction', [ { direction => 'left', display => 'To the left' },
393 { direction => 'right', display => 'To the right', selected => 1 } ],
394 value_key => 'direction', title_key => 'display')) %]
398 A module modeled a bit after Rails' ActionView helpers. Several small
399 functions that create HTML tags from various kinds of data sources.
401 The C<id> attribute is usually calculated automatically. This can be
402 overridden by either specifying an C<id> attribute or by setting
407 =head2 LOW-LEVEL FUNCTIONS
409 The following items are just forwarded to L<SL::Presenter::Tag>:
413 =item * C<name_to_id $name>
415 =item * C<stringify_attributes %items>
417 =item * C<html_tag $tag_name, $content_string, %attributes>
421 =head2 HIGH-LEVEL FUNCTIONS
423 The following functions are just forwarded to L<SL::Presenter::Tag>:
427 =item * C<input_tag $name, $value, %attributes>
429 =item * C<hidden_tag $name, $value, %attributes>
431 =item * C<checkbox_tag $name, %attributes>
433 =item * C<select_tag $name, \@collection, %attributes>
435 =item * C<link $href, $content, %attributes>
439 Available high-level functions implemented in this module:
443 =item C<yes_no_tag $name, $value, %attributes>
445 Creates a HTML 'select' tag with the two entries C<yes> and C<no> by
446 calling L<select_tag>. C<$value> determines
447 which entry is selected. The C<%attributes> are passed through to
450 =item C<textarea_tag $name, $value, %attributes>
452 Creates a HTML 'textarea' tag named C<$name> with the content
453 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
454 tag's C<id> defaults to C<name_to_id($name)>.
456 =item C<date_tag $name, $value, %attributes>
458 Creates a date input field, with an attached javascript that will open a
461 =item C<javascript_tag $file1, $file2, $file3...>
463 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
464 tag for each file name parameter passed. Each file name will be
465 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
466 doesn't contain a slash.
468 =item C<stylesheet_tag $file1, $file2, $file3...>
470 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
471 for each file name parameter passed. Each file name will be postfixed
472 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
475 =item C<tabbed \@tab, %attributes>
477 Will create a tabbed area. The tabs should be created with the helper function
481 L.tab(LxERP.t8('Basic Data'), 'part/_main_tab.html'),
482 L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
485 =item C<areainput_tag $name, $content, %PARAMS>
487 Creates a generic input tag or textarea tag, depending on content size. The
488 amount of desired rows must be either given with the C<rows> parameter or can
489 be computed from the value and the C<cols> paramter, Accepted parameters
490 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
492 You can force input by setting rows to 1, and you can force textarea by setting
495 =item C<multiselect2side $id, %params>
497 Creates a JavaScript snippet calling the jQuery function
498 C<multiselect2side> on the select control with the ID C<$id>. The
499 select itself is not created. C<%params> can contain the following
506 The label of the list of available options. Defaults to the
507 translation of 'Available'.
511 The label of the list of selected options. Defaults to the
512 translation of 'Selected'.
516 =item C<sortable_element $selector, %params>
518 Makes the children of the DOM element C<$selector> (a jQuery selector)
519 sortable with the I<jQuery UI Selectable> library. The children can be
520 dragged & dropped around. After dropping an element an URL can be
521 postet to with the element IDs of the sorted children.
523 If this is used then the JavaScript file C<js/jquery-ui.js> must be
524 included manually as well as it isn't loaded via C<$::form-gt;header>.
526 C<%params> can contain the following entries:
532 The URL to POST an AJAX request to after a dragged element has been
533 dropped. The AJAX request's return value is ignored by default. If given then
534 C<$params{with}> must be given as well.
538 If trueish then the AJAX request's return is accepted.
542 A string that is interpreted as the prefix of the children's ID. Upon
543 POSTing the result each child whose ID starts with C<$params{with}> is
544 considered. The prefix and the following "_" is removed from the
545 ID. The remaining parts of the IDs of those children are posted as a
546 single array parameter. The array parameter's name is either
547 C<$params{as}> or, missing that, C<$params{with}>.
551 Sets the POST parameter name for AJAX request after dropping an
552 element (see C<$params{with}>).
556 An optional jQuery selector specifying which part of the child element
557 is dragable. If the parameter is not given then it defaults to
558 C<.dragdrop> matching DOM elements with the class C<dragdrop>. If the
559 parameter is set and empty then the whole child element is dragable,
560 and clicks through to underlying elements like inputs or links might
563 =item C<dont_recolor>
565 If trueish then the children will not be recolored. The default is to
566 recolor the children by setting the class C<listrow0> on odd and
567 C<listrow1> on even entries.
571 An optional JavaScript string that is evaluated before sending the
572 POST request. The result must be a string that is appended to the URL.
578 <script type="text/javascript" src="js/jquery-ui.js"></script>
580 <table id="thing_list">
582 <tr><td>This</td><td>That</td></tr>
585 <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
586 <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
587 <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
591 [% L.sortable_element('#thing_list tbody',
592 url => 'controller.pl?action=SystemThings/reorder',
595 recolor_rows => 1) %]
597 After dropping e.g. the third element at the top of the list a POST
598 request would be made to the C<reorder> action of the C<SystemThings>
599 controller with a single parameter called C<thing_ids> -- an array
600 containing the values C<[ 6, 2, 15 ]>.
604 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
606 =item C<sortable_table_header $by, %params>
608 Create a link and image suitable for placement in a table
609 header. C<$by> must be an index set up by the controller with
610 L<SL::Controller::Helper::make_sorted>.
612 The optional parameter C<$params{title}> can override the column title
613 displayed to the user. Otherwise the column title from the
614 controller's sort spec is used.
616 The other parameters in C<%params> are passed unmodified to the
617 underlying call to L<SL::Controller::Base::url_for>.
619 See the documentation of L<SL::Controller::Helper::Sorted> for an
620 overview and further usage instructions.
622 =item C<paginate_controls>
624 Create a set of links used to paginate a list view.
626 See the documentation of L<SL::Controller::Helper::Paginated> for an
627 overview and further usage instructions.
631 =head2 CONVERSION FUNCTIONS
635 =item C<tab, description, target, %PARAMS>
637 Creates a tab for C<tabbed>. The description will be used as displayed name.
638 The target should be a block or template that can be processed. C<tab> supports
639 a C<method> parameter, which can override the process method to apply target.
640 C<method => 'raw'> will just include the given text as is. I was too lazy to
641 implement C<include> properly.
643 Also an C<if> attribute is supported, so that tabs can be suppressed based on
644 some occasion. In this case the supplied block won't even get processed, and
645 the resulting tab will get ignored by C<tabbed>:
647 L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
649 =item C<truncate $text, [%params]>
651 See L<SL::Presenter::Text/truncate>.
653 =item C<simple_format $text>
655 See L<SL::Presenter::Text/simple_format>.
659 =head1 MODULE AUTHORS
661 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
663 L<http://linet-services.de>