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 hidden_tag { return _call_presenter('hidden_tag', @_); }
66 sub select_tag { return _call_presenter('select_tag', @_); }
67 sub checkbox_tag { return _call_presenter('checkbox_tag', @_); }
68 sub input_tag { return _call_presenter('input_tag', @_); }
69 sub javascript { return _call_presenter('javascript', @_); }
70 sub truncate { return _call_presenter('truncate', @_); }
71 sub simple_format { return _call_presenter('simple_format', @_); }
72 sub part_picker { return _call_presenter('part_picker', @_); }
73 sub chart_picker { return _call_presenter('chart_picker', @_); }
74 sub customer_vendor_picker { return _call_presenter('customer_vendor_picker', @_); }
75 sub project_picker { return _call_presenter('project_picker', @_); }
76 sub button_tag { return _call_presenter('button_tag', @_); }
77 sub submit_tag { return _call_presenter('submit_tag', @_); }
78 sub ajax_submit_tag { return _call_presenter('ajax_submit_tag', @_); }
79 sub link { return _call_presenter('link', @_); }
81 sub _set_id_attribute {
82 my ($attributes, $name, $unique) = @_;
83 SL::Presenter::Tag::_set_id_attribute($attributes, $name, $unique);
87 my ($self, %options) = _hashify(1, @_);
91 return $self->html_tag('img', undef, %options);
95 my ($self, $name, $content, %attributes) = _hashify(3, @_);
97 _set_id_attribute(\%attributes, $name);
98 $attributes{rows} *= 1; # required by standard
99 $attributes{cols} *= 1; # required by standard
100 $content = $content ? _H($content) : '';
102 return $self->html_tag('textarea', $content, %attributes, name => $name);
105 sub radio_button_tag {
106 my ($self, $name, %attributes) = _hashify(2, @_);
108 $attributes{value} = 1 unless exists $attributes{value};
110 _set_id_attribute(\%attributes, $name, 1);
111 my $label = delete $attributes{label};
113 _set_id_attribute(\%attributes, $name . '_' . $attributes{value});
115 if ($attributes{checked}) {
116 $attributes{checked} = 'checked';
118 delete $attributes{checked};
121 my $code = $self->html_tag('input', undef, %attributes, name => $name, type => 'radio');
122 $code .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
128 my ($self, $content, @slurp) = @_;
129 return $self->html_tag('div', $content, @slurp);
133 my ($self, $content, @slurp) = @_;
134 return $self->html_tag('ul', $content, @slurp);
138 my ($self, $content, @slurp) = @_;
139 return $self->html_tag('li', $content, @slurp);
143 my ($self, $name, $value, %attributes) = _hashify(3, @_);
145 return $self->select_tag($name, [ [ 1 => $::locale->text('Yes') ], [ 0 => $::locale->text('No') ] ], default => $value ? 1 : 0, %attributes);
152 foreach my $file (@_) {
153 $file .= '.css' unless $file =~ m/\.css$/;
154 $file = "css/${file}" unless $file =~ m|/|;
156 $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
162 my $date_tag_id_idx = 0;
164 my ($self, $name, $value, %params) = _hashify(3, @_);
166 _set_id_attribute(\%params, $name);
167 my @onchange = $params{onchange} ? (onChange => delete $params{onchange}) : ();
168 my @classes = $params{no_cal} || $params{readonly} ? () : ('datepicker');
169 push @classes, delete($params{class}) if $params{class};
170 my %class = @classes ? (class => join(' ', @classes)) : ();
172 $::request->presenter->need_reinit_widgets($params{id});
174 return $self->input_tag(
175 $name, blessed($value) ? $value->to_lxoffice : $value,
177 onchange => "check_right_date_format(this);",
183 # simple version with select_tag
184 sub vendor_selector {
185 my ($self, $name, $value, %params) = _hashify(3, @_);
187 my $actual_vendor_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"}) ? $::form->{"$name"}->id : $::form->{"$name"}) :
188 (ref $value && $value->can('id')) ? $value->id : '';
190 return $self->select_tag($name, SL::DB::Manager::Vendor->get_all(),
191 default => $actual_vendor_id,
192 title_sub => sub { $_[0]->vendornumber . " : " . $_[0]->name },
198 # simple version with select_tag
200 my ($self, $name, $value, %params) = _hashify(3, @_);
202 my $actual_part_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"})? $::form->{"$name"}->id : $::form->{"$name"}) :
203 (ref $value && $value->can('id')) ? $value->id : '';
205 return $self->select_tag($name, SL::DB::Manager::Part->get_all(),
206 default => $actual_part_id,
207 title_sub => sub { $_[0]->partnumber . " : " . $_[0]->description },
217 foreach my $file (@_) {
218 $file .= '.js' unless $file =~ m/\.js$/;
219 $file = "js/${file}" unless $file =~ m|/|;
221 $code .= qq|<script type="text/javascript" src="${file}"></script>|;
228 my ($self, $tabs, %params) = _hashify(2, @_);
229 my $id = $params{id} || 'tab_' . _tag_id();
231 $params{selected} *= 1;
233 die 'L.tabbed needs an arrayred of tabs for first argument'
234 unless ref $tabs eq 'ARRAY';
236 my (@header, @blocks);
237 for my $i (0..$#$tabs) {
238 my $tab = $tabs->[$i];
242 my $tab_id = "__tab_id_$i";
243 push @header, $self->li_tag($self->link('#' . $tab_id, $tab->{name}));
244 push @blocks, $self->div_tag($tab->{data}, id => $tab_id);
247 return '' unless @header;
249 my $ul = $self->ul_tag(join('', @header), id => $id);
250 return $self->div_tag(join('', $ul, @blocks), class => 'tabwidget');
254 my ($self, $name, $src, %params) = _hashify(3, @_);
256 $params{method} ||= 'process';
258 return () if defined $params{if} && !$params{if};
261 if ($params{method} eq 'raw') {
263 } elsif ($params{method} eq 'process') {
264 $data = $self->_context->process($src, %{ $params{args} || {} });
266 die "unknown tag method '$params{method}'";
269 return () unless $data;
271 return +{ name => $name, data => $data };
275 my ($self, $name, $value, %attributes) = _hashify(3, @_);
277 my $cols = delete $attributes{cols} || delete $attributes{size};
278 my $minrows = delete $attributes{min_rows} || 1;
279 my $maxrows = delete $attributes{max_rows};
280 my $rows = $::form->numtextrows($value, $cols, $maxrows, $minrows);
282 $attributes{id} ||= _tag_id();
283 my $id = $attributes{id};
285 return $self->textarea_tag($name, $value, %attributes, rows => $rows, cols => $cols) if $rows > 1;
288 . $self->input_tag($name, $value, %attributes, size => $cols)
289 . "<img src=\"image/edit-entry.png\" onclick=\"kivi.switch_areainput_to_textarea('${id}')\" style=\"margin-left: 2px;\">"
293 sub multiselect2side {
294 my ($self, $id, %params) = _hashify(2, @_);
296 $params{labelsx} = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
297 $params{labeldx} = "\"" . _J($params{labeldx} || $::locale->text('Selected')) . "\"";
298 $params{moveOptions} = 'false';
300 my $vars = join(', ', map { "${_}: " . $params{$_} } keys %params);
302 <script type="text/javascript">
303 \$().ready(function() {
304 \$('#${id}').multiselect2side({ ${vars} });
312 sub sortable_element {
313 my ($self, $selector, %params) = _hashify(2, @_);
315 my %attributes = ( distance => 5,
316 helper => <<'JAVASCRIPT' );
317 function(event, ui) {
318 ui.children().each(function() {
319 $(this).width($(this).width());
327 if ($params{url} && $params{with}) {
328 my $as = $params{as} || $params{with};
329 my $filter = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
330 $filter .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
332 my $params_js = $params{params} ? qq| + ($params{params})| : '';
334 $stop_event = <<JAVASCRIPT;
335 \$.post('$params{url}'${params_js}, { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() });
339 if (!$params{dont_recolor}) {
340 $stop_event .= <<JAVASCRIPT;
341 \$('${selector}>*:odd').removeClass('listrow1').removeClass('listrow0').addClass('listrow0');
342 \$('${selector}>*:even').removeClass('listrow1').removeClass('listrow0').addClass('listrow1');
347 $attributes{stop} = <<JAVASCRIPT;
348 function(event, ui) {
355 $params{handle} = '.dragdrop' unless exists $params{handle};
356 $attributes{handle} = "'$params{handle}'" if $params{handle};
358 my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
360 my $code = <<JAVASCRIPT;
361 <script type="text/javascript">
363 \$( "${selector}" ).sortable({ ${attr_str} })
373 return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
376 sub sortable_table_header {
377 my ($self, $by, %params) = _hashify(2, @_);
379 my $controller = $self->{CONTEXT}->stash->get('SELF');
380 my $models = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
381 my $sort_spec = $models->get_sort_spec;
382 my $by_spec = $sort_spec->{$by};
383 my %current_sort_params = $models->get_current_sort_params;
384 my ($image, $new_dir) = ('', $current_sort_params{dir});
385 my $title = delete($params{title}) || $::locale->text($by_spec->{title});
387 if ($current_sort_params{sort_by} eq $by) {
388 my $current_dir = $current_sort_params{sort_dir} ? 'up' : 'down';
389 $image = '<img border="0" src="image/' . $current_dir . '.png">';
390 $new_dir = 1 - ($current_sort_params{sort_dir} || 0);
393 $params{ $models->sorted->form_params->[0] } = $by;
394 $params{ $models->sorted->form_params->[1] } = ($new_dir ? '1' : '0');
396 return '<a href="' . $models->get_callback(%params) . '">' . _H($title) . $image . '</a>';
399 sub paginate_controls {
400 my ($self, %params) = _hashify(1, @_);
402 my $controller = $self->{CONTEXT}->stash->get('SELF');
403 my $models = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
404 my $pager = $models->paginated;
405 # my $paginate_spec = $controller->get_paginate_spec;
407 my %paginate_params = $models->get_paginate_args;
409 my %template_params = (
410 pages => \%paginate_params,
412 my %url_params = _hashify(0, @_);
413 $url_params{ $pager->form_params->[0] } = delete $url_params{page};
414 $url_params{ $pager->form_params->[1] } = delete $url_params{per_page} if exists $url_params{per_page};
416 return $models->get_callback(%url_params);
421 return SL::Presenter->get->render('common/paginate', %template_params);
430 SL::Templates::Plugin::L -- Layouting / tag generation
434 Usage from a template:
438 [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right', 1 ] ]) %]
440 [% L.select_tag('direction', [ { direction => 'left', display => 'To the left' },
441 { direction => 'right', display => 'To the right' } ],
442 value_key => 'direction', title_key => 'display', default => 'right')) %]
444 [% L.select_tag('direction', [ { direction => 'left', display => 'To the left' },
445 { direction => 'right', display => 'To the right', selected => 1 } ],
446 value_key => 'direction', title_key => 'display')) %]
450 A module modeled a bit after Rails' ActionView helpers. Several small
451 functions that create HTML tags from various kinds of data sources.
453 The C<id> attribute is usually calculated automatically. This can be
454 overridden by either specifying an C<id> attribute or by setting
459 =head2 LOW-LEVEL FUNCTIONS
461 The following items are just forwarded to L<SL::Presenter::Tag>:
465 =item * C<name_to_id $name>
467 =item * C<stringify_attributes %items>
469 =item * C<html_tag $tag_name, $content_string, %attributes>
473 =head2 HIGH-LEVEL FUNCTIONS
475 The following functions are just forwarded to L<SL::Presenter::Tag>:
479 =item * C<input_tag $name, $value, %attributes>
481 =item * C<hidden_tag $name, $value, %attributes>
483 =item * C<checkbox_tag $name, %attributes>
485 =item * C<select_tag $name, \@collection, %attributes>
487 =item * C<link $href, $content, %attributes>
491 Available high-level functions implemented in this module:
495 =item C<yes_no_tag $name, $value, %attributes>
497 Creates a HTML 'select' tag with the two entries C<yes> and C<no> by
498 calling L<select_tag>. C<$value> determines
499 which entry is selected. The C<%attributes> are passed through to
502 =item C<textarea_tag $name, $value, %attributes>
504 Creates a HTML 'textarea' tag named C<$name> with the content
505 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
506 tag's C<id> defaults to C<name_to_id($name)>.
508 =item C<date_tag $name, $value, %attributes>
510 Creates a date input field, with an attached javascript that will open a
513 =item C<radio_button_tag $name, %attributes>
515 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
516 HTML attributes from C<%attributes>. The tag's C<value> defaults to
517 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
519 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
520 created with said C<label>. No attribute named C<label> is created in
523 =item C<javascript_tag $file1, $file2, $file3...>
525 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
526 tag for each file name parameter passed. Each file name will be
527 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
528 doesn't contain a slash.
530 =item C<stylesheet_tag $file1, $file2, $file3...>
532 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
533 for each file name parameter passed. Each file name will be postfixed
534 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
537 =item C<tabbed \@tab, %attributes>
539 Will create a tabbed area. The tabs should be created with the helper function
543 L.tab(LxERP.t8('Basic Data'), 'part/_main_tab.html'),
544 L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
547 =item C<areainput_tag $name, $content, %PARAMS>
549 Creates a generic input tag or textarea tag, depending on content size. The
550 amount of desired rows must be either given with the C<rows> parameter or can
551 be computed from the value and the C<cols> paramter, Accepted parameters
552 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
554 You can force input by setting rows to 1, and you can force textarea by setting
557 =item C<multiselect2side $id, %params>
559 Creates a JavaScript snippet calling the jQuery function
560 C<multiselect2side> on the select control with the ID C<$id>. The
561 select itself is not created. C<%params> can contain the following
568 The label of the list of available options. Defaults to the
569 translation of 'Available'.
573 The label of the list of selected options. Defaults to the
574 translation of 'Selected'.
578 =item C<sortable_element $selector, %params>
580 Makes the children of the DOM element C<$selector> (a jQuery selector)
581 sortable with the I<jQuery UI Selectable> library. The children can be
582 dragged & dropped around. After dropping an element an URL can be
583 postet to with the element IDs of the sorted children.
585 If this is used then the JavaScript file C<js/jquery-ui.js> must be
586 included manually as well as it isn't loaded via C<$::form-gt;header>.
588 C<%params> can contain the following entries:
594 The URL to POST an AJAX request to after a dragged element has been
595 dropped. The AJAX request's return value is ignored. If given then
596 C<$params{with}> must be given as well.
600 A string that is interpreted as the prefix of the children's ID. Upon
601 POSTing the result each child whose ID starts with C<$params{with}> is
602 considered. The prefix and the following "_" is removed from the
603 ID. The remaining parts of the IDs of those children are posted as a
604 single array parameter. The array parameter's name is either
605 C<$params{as}> or, missing that, C<$params{with}>.
609 Sets the POST parameter name for AJAX request after dropping an
610 element (see C<$params{with}>).
614 An optional jQuery selector specifying which part of the child element
615 is dragable. If the parameter is not given then it defaults to
616 C<.dragdrop> matching DOM elements with the class C<dragdrop>. If the
617 parameter is set and empty then the whole child element is dragable,
618 and clicks through to underlying elements like inputs or links might
621 =item C<dont_recolor>
623 If trueish then the children will not be recolored. The default is to
624 recolor the children by setting the class C<listrow0> on odd and
625 C<listrow1> on even entries.
629 An optional JavaScript string that is evaluated before sending the
630 POST request. The result must be a string that is appended to the URL.
636 <script type="text/javascript" src="js/jquery-ui.js"></script>
638 <table id="thing_list">
640 <tr><td>This</td><td>That</td></tr>
643 <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
644 <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
645 <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
649 [% L.sortable_element('#thing_list tbody',
650 url => 'controller.pl?action=SystemThings/reorder',
653 recolor_rows => 1) %]
655 After dropping e.g. the third element at the top of the list a POST
656 request would be made to the C<reorder> action of the C<SystemThings>
657 controller with a single parameter called C<thing_ids> -- an array
658 containing the values C<[ 6, 2, 15 ]>.
662 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
664 =item C<sortable_table_header $by, %params>
666 Create a link and image suitable for placement in a table
667 header. C<$by> must be an index set up by the controller with
668 L<SL::Controller::Helper::make_sorted>.
670 The optional parameter C<$params{title}> can override the column title
671 displayed to the user. Otherwise the column title from the
672 controller's sort spec is used.
674 The other parameters in C<%params> are passed unmodified to the
675 underlying call to L<SL::Controller::Base::url_for>.
677 See the documentation of L<SL::Controller::Helper::Sorted> for an
678 overview and further usage instructions.
680 =item C<paginate_controls>
682 Create a set of links used to paginate a list view.
684 See the documentation of L<SL::Controller::Helper::Paginated> for an
685 overview and further usage instructions.
689 =head2 CONVERSION FUNCTIONS
693 =item C<tab, description, target, %PARAMS>
695 Creates a tab for C<tabbed>. The description will be used as displayed name.
696 The target should be a block or template that can be processed. C<tab> supports
697 a C<method> parameter, which can override the process method to apply target.
698 C<method => 'raw'> will just include the given text as is. I was too lazy to
699 implement C<include> properly.
701 Also an C<if> attribute is supported, so that tabs can be suppressed based on
702 some occasion. In this case the supplied block won't even get processed, and
703 the resulting tab will get ignored by C<tabbed>:
705 L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
707 =item C<truncate $text, [%params]>
709 See L<SL::Presenter::Text/truncate>.
711 =item C<simple_format $text>
713 See L<SL::Presenter::Text/simple_format>.
717 =head1 MODULE AUTHORS
719 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
721 L<http://linet-services.de>