abb6046936e536139b641f65976acd10b994405b
[kivitendo-erp.git] / SL / Template / Plugin / L.pm
1 package SL::Template::Plugin::L;
2
3 use base qw( Template::Plugin );
4 use Template::Plugin;
5 use Data::Dumper;
6 use List::MoreUtils qw(apply);
7 use List::Util qw(max);
8 use Scalar::Util qw(blessed);
9
10 use SL::Presenter;
11 use SL::Presenter::ALL;
12 use SL::Presenter::Simple;
13 use SL::Util qw(_hashify);
14
15 use strict;
16
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;
21 sub _tag_id {
22   return "id_" . ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
23 }
24 }
25
26 sub _H {
27   my $string = shift;
28   return $::locale->quote_special_chars('HTML', $string);
29 }
30
31 sub _J {
32   my $string = shift;
33   $string    =~ s/(\"|\'|\\)/\\$1/g;
34   return $string;
35 }
36
37 sub new {
38   my ($class, $context, @args) = @_;
39
40   return bless {
41     CONTEXT => $context,
42   }, $class;
43 }
44
45 sub _context {
46   die 'not an accessor' if @_ > 1;
47   return $_[0]->{CONTEXT};
48 }
49
50 sub _call_presenter {
51   my ($method, $self, @args) = @_;
52
53   my $presenter              = $::request->presenter;
54
55   splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
56
57   if (my $sub = SL::Presenter::Simple->can($method)) {
58     return $sub->(@args);
59   }
60
61   if ($presenter->can($method)) {
62     return $presenter->$method(@args);
63   }
64
65   $::lxdebug->message(LXDebug::WARN(), "SL::Presenter has no method named '$method'!");
66   return;
67 }
68
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',            @_); }
90
91 sub _set_id_attribute {
92   my ($attributes, $name, $unique) = @_;
93   SL::Presenter::Tag::_set_id_attribute($attributes, $name, $unique);
94 }
95
96 sub ul_tag {
97   my ($self, $content, @slurp) = @_;
98   return $self->html_tag('ul', $content, @slurp);
99 }
100
101 sub li_tag {
102   my ($self, $content, @slurp) = @_;
103   return $self->html_tag('li', $content, @slurp);
104 }
105
106 sub yes_no_tag {
107   my ($self, $name, $value, %attributes) = _hashify(3, @_);
108
109   return $self->select_tag($name, [ [ 1 => $::locale->text('Yes') ], [ 0 => $::locale->text('No') ] ], default => $value ? 1 : 0, %attributes);
110 }
111
112 sub stylesheet_tag {
113   my $self = shift;
114   my $code = '';
115
116   foreach my $file (@_) {
117     $file .= '.css'        unless $file =~ m/\.css$/;
118     $file  = "css/${file}" unless $file =~ m|/|;
119
120     $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
121   }
122
123   return $code;
124 }
125
126
127 # simple version with select_tag
128 sub vendor_selector {
129   my ($self, $name, $value, %params) = _hashify(3, @_);
130
131   my $actual_vendor_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"}) ? $::form->{"$name"}->id : $::form->{"$name"}) :
132                          (ref $value && $value->can('id')) ? $value->id : '';
133
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 },
137                                   'with_empty' => 1,
138                                   %params);
139 }
140
141
142 # simple version with select_tag
143 sub part_selector {
144   my ($self, $name, $value, %params) = _hashify(3, @_);
145
146   my $actual_part_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"})? $::form->{"$name"}->id : $::form->{"$name"}) :
147                        (ref $value && $value->can('id')) ? $value->id : '';
148
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 },
152                            with_empty   => 1,
153                            %params);
154 }
155
156
157 sub javascript_tag {
158   my $self = shift;
159   my $code = '';
160
161   foreach my $file (@_) {
162     $file .= '.js'        unless $file =~ m/\.js$/;
163     $file  = "js/${file}" unless $file =~ m|/|;
164
165     $code .= qq|<script type="text/javascript" src="${file}"></script>|;
166   }
167
168   return $code;
169 }
170
171 sub tabbed {
172   my ($self, $tabs, %params) = _hashify(2, @_);
173   my $id       = $params{id} || 'tab_' . _tag_id();
174
175   $params{selected} *= 1;
176
177   die 'L.tabbed needs an arrayred of tabs for first argument'
178     unless ref $tabs eq 'ARRAY';
179
180   my (@header, @blocks);
181   for my $i (0..$#$tabs) {
182     my $tab = $tabs->[$i];
183
184     next if $tab eq '';
185
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);
189   }
190
191   return '' unless @header;
192
193   my $ul = $self->ul_tag(join('', @header), id => $id);
194   return $self->div_tag(join('', $ul, @blocks), class => 'tabwidget');
195 }
196
197 sub tab {
198   my ($self, $name, $src, %params) = _hashify(3, @_);
199
200   $params{method} ||= 'process';
201
202   return () if defined $params{if} && !$params{if};
203
204   my $data;
205   if ($params{method} eq 'raw') {
206     $data = $src;
207   } elsif ($params{method} eq 'process') {
208     $data = $self->_context->process($src, %{ $params{args} || {} });
209   } else {
210     die "unknown tag method '$params{method}'";
211   }
212
213   return () unless $data;
214
215   return +{ name => $name, data => $data };
216 }
217
218 sub areainput_tag {
219   my ($self, $name, $value, %attributes) = _hashify(3, @_);
220
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);
225
226   $attributes{id} ||= _tag_id();
227   my $id            = $attributes{id};
228
229   return $self->textarea_tag($name, $value, %attributes, rows => $rows, cols => $cols) if $rows > 1;
230
231   return '<span>'
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;\">"
234     . '</span>';
235 }
236
237 sub multiselect2side {
238   my ($self, $id, %params) = _hashify(2, @_);
239
240   $params{labelsx}        = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
241   $params{labeldx}        = "\"" . _J($params{labeldx} || $::locale->text('Selected'))  . "\"";
242   $params{moveOptions}    = 'false';
243
244   my $vars                = join(', ', map { "${_}: " . $params{$_} } keys %params);
245   my $code                = <<EOCODE;
246 <script type="text/javascript">
247   \$().ready(function() {
248     \$('#${id}').multiselect2side({ ${vars} });
249   });
250 </script>
251 EOCODE
252
253   return $code;
254 }
255
256 sub sortable_element {
257   my ($self, $selector, %params) = _hashify(2, @_);
258
259   my %attributes = ( distance => 5,
260                      helper   => <<'JAVASCRIPT' );
261     function(event, ui) {
262       ui.children().each(function() {
263         $(this).width($(this).width());
264       });
265       return ui;
266     }
267 JAVASCRIPT
268
269   my $stop_event = '';
270
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}_', ''); })";
275
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';
280     }
281
282     $stop_event = <<JAVASCRIPT;
283         \$.post('$params{url}'${params_js}, { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() }, $ajax_return);
284 JAVASCRIPT
285   }
286
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');
291 JAVASCRIPT
292   }
293
294   if ($stop_event) {
295     $attributes{stop} = <<JAVASCRIPT;
296       function(event, ui) {
297         ${stop_event}
298         return ui;
299       }
300 JAVASCRIPT
301   }
302
303   $params{handle}     = '.dragdrop' unless exists $params{handle};
304   $attributes{handle} = "'$params{handle}'" if $params{handle};
305
306   my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
307
308   my $code = <<JAVASCRIPT;
309 <script type="text/javascript">
310   \$(function() {
311     \$( "${selector}" ).sortable({ ${attr_str} })
312   });
313 </script>
314 JAVASCRIPT
315
316   return $code;
317 }
318
319 sub dump {
320   my $self = shift;
321   return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
322 }
323
324 sub sortable_table_header {
325   my ($self, $by, %params) = _hashify(2, @_);
326
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});
334
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);
339   }
340
341   $params{ $models->sorted->form_params->[0] } = $by;
342   $params{ $models->sorted->form_params->[1] } = ($new_dir ? '1' : '0');
343
344   return '<a href="' . $models->get_callback(%params) . '">' . _H($title) . $image . '</a>';
345 }
346
347 sub paginate_controls {
348   my ($self, %params) = _hashify(1, @_);
349
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;
354
355   my %paginate_params = $models->get_paginate_args;
356
357   my %template_params = (
358     pages             => \%paginate_params,
359     url_maker         => sub {
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};
363
364       return $models->get_callback(%url_params);
365     },
366     %params,
367   );
368
369   return SL::Presenter->get->render('common/paginate', %template_params);
370 }
371
372 1;
373
374 __END__
375
376 =head1 NAME
377
378 SL::Templates::Plugin::L -- Layouting / tag generation
379
380 =head1 SYNOPSIS
381
382 Usage from a template:
383
384   [% USE L %]
385
386   [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right', 1 ] ]) %]
387
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')) %]
391
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')) %]
395
396 =head1 DESCRIPTION
397
398 A module modeled a bit after Rails' ActionView helpers. Several small
399 functions that create HTML tags from various kinds of data sources.
400
401 The C<id> attribute is usually calculated automatically. This can be
402 overridden by either specifying an C<id> attribute or by setting
403 C<no_id> to trueish.
404
405 =head1 FUNCTIONS
406
407 =head2 LOW-LEVEL FUNCTIONS
408
409 The following items are just forwarded to L<SL::Presenter::Tag>:
410
411 =over 2
412
413 =item * C<name_to_id $name>
414
415 =item * C<stringify_attributes %items>
416
417 =item * C<html_tag $tag_name, $content_string, %attributes>
418
419 =back
420
421 =head2 HIGH-LEVEL FUNCTIONS
422
423 The following functions are just forwarded to L<SL::Presenter::Tag>:
424
425 =over 2
426
427 =item * C<input_tag $name, $value, %attributes>
428
429 =item * C<hidden_tag $name, $value, %attributes>
430
431 =item * C<checkbox_tag $name, %attributes>
432
433 =item * C<select_tag $name, \@collection, %attributes>
434
435 =item * C<link $href, $content, %attributes>
436
437 =back
438
439 Available high-level functions implemented in this module:
440
441 =over 4
442
443 =item C<yes_no_tag $name, $value, %attributes>
444
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
448 L<select_tag>.
449
450 =item C<textarea_tag $name, $value, %attributes>
451
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)>.
455
456 =item C<date_tag $name, $value, %attributes>
457
458 Creates a date input field, with an attached javascript that will open a
459 calendar on click.
460
461 =item C<javascript_tag $file1, $file2, $file3...>
462
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.
467
468 =item C<stylesheet_tag $file1, $file2, $file3...>
469
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
473 contain a slash.
474
475 =item C<tabbed \@tab, %attributes>
476
477 Will create a tabbed area. The tabs should be created with the helper function
478 C<tab>. Example:
479
480   [% L.tabbed([
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),
483   ]) %]
484
485 =item C<areainput_tag $name, $content, %PARAMS>
486
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.
491
492 You can force input by setting rows to 1, and you can force textarea by setting
493 rows to anything >1.
494
495 =item C<multiselect2side $id, %params>
496
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
500 entries:
501
502 =over 2
503
504 =item C<labelsx>
505
506 The label of the list of available options. Defaults to the
507 translation of 'Available'.
508
509 =item C<labeldx>
510
511 The label of the list of selected options. Defaults to the
512 translation of 'Selected'.
513
514 =back
515
516 =item C<sortable_element $selector, %params>
517
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.
522
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>.
525
526 C<%params> can contain the following entries:
527
528 =over 2
529
530 =item C<url>
531
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.
535
536 =item C<ajax_return>
537
538 If trueish then the AJAX request's return is accepted.
539
540 =item C<with>
541
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}>.
548
549 =item C<as>
550
551 Sets the POST parameter name for AJAX request after dropping an
552 element (see C<$params{with}>).
553
554 =item C<handle>
555
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
561 not work.
562
563 =item C<dont_recolor>
564
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.
568
569 =item C<params>
570
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.
573
574 =back
575
576 Example:
577
578   <script type="text/javascript" src="js/jquery-ui.js"></script>
579
580   <table id="thing_list">
581     <thead>
582       <tr><td>This</td><td>That</td></tr>
583     </thead>
584     <tbody>
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>
588     </tbody>
589   <table>
590
591   [% L.sortable_element('#thing_list tbody',
592                         url          => 'controller.pl?action=SystemThings/reorder',
593                         with         => 'thingy',
594                         as           => 'thing_ids',
595                         recolor_rows => 1) %]
596
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 ]>.
601
602 =item C<dump REF>
603
604 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
605
606 =item C<sortable_table_header $by, %params>
607
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>.
611
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.
615
616 The other parameters in C<%params> are passed unmodified to the
617 underlying call to L<SL::Controller::Base::url_for>.
618
619 See the documentation of L<SL::Controller::Helper::Sorted> for an
620 overview and further usage instructions.
621
622 =item C<paginate_controls>
623
624 Create a set of links used to paginate a list view.
625
626 See the documentation of L<SL::Controller::Helper::Paginated> for an
627 overview and further usage instructions.
628
629 =back
630
631 =head2 CONVERSION FUNCTIONS
632
633 =over 4
634
635 =item C<tab, description, target, %PARAMS>
636
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.
642
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>:
646
647   L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
648
649 =item C<truncate $text, [%params]>
650
651 See L<SL::Presenter::Text/truncate>.
652
653 =item C<simple_format $text>
654
655 See L<SL::Presenter::Text/simple_format>.
656
657 =back
658
659 =head1 MODULE AUTHORS
660
661 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
662
663 L<http://linet-services.de>