93508fcf2c7972ad4b0a74e85432fdf0ed3160f3
[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
86 sub _set_id_attribute {
87   my ($attributes, $name, $unique) = @_;
88   SL::Presenter::Tag::_set_id_attribute($attributes, $name, $unique);
89 }
90
91 sub img_tag {
92   my ($self, %options) = _hashify(1, @_);
93
94   $options{alt} ||= '';
95
96   return $self->html_tag('img', undef, %options);
97 }
98
99 sub radio_button_tag {
100   my ($self, $name, %attributes) = _hashify(2, @_);
101
102   $attributes{value}   = 1 unless exists $attributes{value};
103
104   _set_id_attribute(\%attributes, $name, 1);
105   my $label            = delete $attributes{label};
106
107   _set_id_attribute(\%attributes, $name . '_' . $attributes{value});
108
109   if ($attributes{checked}) {
110     $attributes{checked} = 'checked';
111   } else {
112     delete $attributes{checked};
113   }
114
115   my $code  = $self->html_tag('input', undef,  %attributes, name => $name, type => 'radio');
116   $code    .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
117
118   return $code;
119 }
120
121 sub div_tag {
122   my ($self, $content, @slurp) = @_;
123   return $self->html_tag('div', $content, @slurp);
124 }
125
126 sub ul_tag {
127   my ($self, $content, @slurp) = @_;
128   return $self->html_tag('ul', $content, @slurp);
129 }
130
131 sub li_tag {
132   my ($self, $content, @slurp) = @_;
133   return $self->html_tag('li', $content, @slurp);
134 }
135
136 sub yes_no_tag {
137   my ($self, $name, $value, %attributes) = _hashify(3, @_);
138
139   return $self->select_tag($name, [ [ 1 => $::locale->text('Yes') ], [ 0 => $::locale->text('No') ] ], default => $value ? 1 : 0, %attributes);
140 }
141
142 sub stylesheet_tag {
143   my $self = shift;
144   my $code = '';
145
146   foreach my $file (@_) {
147     $file .= '.css'        unless $file =~ m/\.css$/;
148     $file  = "css/${file}" unless $file =~ m|/|;
149
150     $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
151   }
152
153   return $code;
154 }
155
156
157 # simple version with select_tag
158 sub vendor_selector {
159   my ($self, $name, $value, %params) = _hashify(3, @_);
160
161   my $actual_vendor_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"}) ? $::form->{"$name"}->id : $::form->{"$name"}) :
162                          (ref $value && $value->can('id')) ? $value->id : '';
163
164   return $self->select_tag($name, SL::DB::Manager::Vendor->get_all(),
165                                   default      => $actual_vendor_id,
166                                   title_sub    => sub { $_[0]->vendornumber . " : " . $_[0]->name },
167                                   'with_empty' => 1,
168                                   %params);
169 }
170
171
172 # simple version with select_tag
173 sub part_selector {
174   my ($self, $name, $value, %params) = _hashify(3, @_);
175
176   my $actual_part_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"})? $::form->{"$name"}->id : $::form->{"$name"}) :
177                        (ref $value && $value->can('id')) ? $value->id : '';
178
179   return $self->select_tag($name, SL::DB::Manager::Part->get_all(),
180                            default      => $actual_part_id,
181                            title_sub    => sub { $_[0]->partnumber . " : " . $_[0]->description },
182                            with_empty   => 1,
183                            %params);
184 }
185
186
187 sub javascript_tag {
188   my $self = shift;
189   my $code = '';
190
191   foreach my $file (@_) {
192     $file .= '.js'        unless $file =~ m/\.js$/;
193     $file  = "js/${file}" unless $file =~ m|/|;
194
195     $code .= qq|<script type="text/javascript" src="${file}"></script>|;
196   }
197
198   return $code;
199 }
200
201 sub tabbed {
202   my ($self, $tabs, %params) = _hashify(2, @_);
203   my $id       = $params{id} || 'tab_' . _tag_id();
204
205   $params{selected} *= 1;
206
207   die 'L.tabbed needs an arrayred of tabs for first argument'
208     unless ref $tabs eq 'ARRAY';
209
210   my (@header, @blocks);
211   for my $i (0..$#$tabs) {
212     my $tab = $tabs->[$i];
213
214     next if $tab eq '';
215
216     my $tab_id = "__tab_id_$i";
217     push @header, $self->li_tag($self->link('#' . $tab_id, $tab->{name}));
218     push @blocks, $self->div_tag($tab->{data}, id => $tab_id);
219   }
220
221   return '' unless @header;
222
223   my $ul = $self->ul_tag(join('', @header), id => $id);
224   return $self->div_tag(join('', $ul, @blocks), class => 'tabwidget');
225 }
226
227 sub tab {
228   my ($self, $name, $src, %params) = _hashify(3, @_);
229
230   $params{method} ||= 'process';
231
232   return () if defined $params{if} && !$params{if};
233
234   my $data;
235   if ($params{method} eq 'raw') {
236     $data = $src;
237   } elsif ($params{method} eq 'process') {
238     $data = $self->_context->process($src, %{ $params{args} || {} });
239   } else {
240     die "unknown tag method '$params{method}'";
241   }
242
243   return () unless $data;
244
245   return +{ name => $name, data => $data };
246 }
247
248 sub areainput_tag {
249   my ($self, $name, $value, %attributes) = _hashify(3, @_);
250
251   my $cols    = delete $attributes{cols} || delete $attributes{size};
252   my $minrows = delete $attributes{min_rows} || 1;
253   my $maxrows = delete $attributes{max_rows};
254   my $rows    = $::form->numtextrows($value, $cols, $maxrows, $minrows);
255
256   $attributes{id} ||= _tag_id();
257   my $id            = $attributes{id};
258
259   return $self->textarea_tag($name, $value, %attributes, rows => $rows, cols => $cols) if $rows > 1;
260
261   return '<span>'
262     . $self->input_tag($name, $value, %attributes, size => $cols)
263     . "<img src=\"image/edit-entry.png\" onclick=\"kivi.switch_areainput_to_textarea('${id}')\" style=\"margin-left: 2px;\">"
264     . '</span>';
265 }
266
267 sub multiselect2side {
268   my ($self, $id, %params) = _hashify(2, @_);
269
270   $params{labelsx}        = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
271   $params{labeldx}        = "\"" . _J($params{labeldx} || $::locale->text('Selected'))  . "\"";
272   $params{moveOptions}    = 'false';
273
274   my $vars                = join(', ', map { "${_}: " . $params{$_} } keys %params);
275   my $code                = <<EOCODE;
276 <script type="text/javascript">
277   \$().ready(function() {
278     \$('#${id}').multiselect2side({ ${vars} });
279   });
280 </script>
281 EOCODE
282
283   return $code;
284 }
285
286 sub sortable_element {
287   my ($self, $selector, %params) = _hashify(2, @_);
288
289   my %attributes = ( distance => 5,
290                      helper   => <<'JAVASCRIPT' );
291     function(event, ui) {
292       ui.children().each(function() {
293         $(this).width($(this).width());
294       });
295       return ui;
296     }
297 JAVASCRIPT
298
299   my $stop_event = '';
300
301   if ($params{url} && $params{with}) {
302     my $as      = $params{as} || $params{with};
303     my $filter  = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
304     $filter    .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
305
306     my $params_js = $params{params} ? qq| + ($params{params})| : '';
307     my $ajax_return = '';
308     if ($params{ajax_return}) {
309       $ajax_return = 'kivi.eval_json_result';
310     }
311
312     $stop_event = <<JAVASCRIPT;
313         \$.post('$params{url}'${params_js}, { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() }, $ajax_return);
314 JAVASCRIPT
315   }
316
317   if (!$params{dont_recolor}) {
318     $stop_event .= <<JAVASCRIPT;
319         \$('${selector}>*:odd').removeClass('listrow1').removeClass('listrow0').addClass('listrow0');
320         \$('${selector}>*:even').removeClass('listrow1').removeClass('listrow0').addClass('listrow1');
321 JAVASCRIPT
322   }
323
324   if ($stop_event) {
325     $attributes{stop} = <<JAVASCRIPT;
326       function(event, ui) {
327         ${stop_event}
328         return ui;
329       }
330 JAVASCRIPT
331   }
332
333   $params{handle}     = '.dragdrop' unless exists $params{handle};
334   $attributes{handle} = "'$params{handle}'" if $params{handle};
335
336   my $attr_str = join(', ', map { "${_}: $attributes{$_}" } keys %attributes);
337
338   my $code = <<JAVASCRIPT;
339 <script type="text/javascript">
340   \$(function() {
341     \$( "${selector}" ).sortable({ ${attr_str} })
342   });
343 </script>
344 JAVASCRIPT
345
346   return $code;
347 }
348
349 sub dump {
350   my $self = shift;
351   return '<pre>' . Data::Dumper::Dumper(@_) . '</pre>';
352 }
353
354 sub sortable_table_header {
355   my ($self, $by, %params) = _hashify(2, @_);
356
357   my $controller          = $self->{CONTEXT}->stash->get('SELF');
358   my $models              = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
359   my $sort_spec           = $models->get_sort_spec;
360   my $by_spec             = $sort_spec->{$by};
361   my %current_sort_params = $models->get_current_sort_params;
362   my ($image, $new_dir)   = ('', $current_sort_params{dir});
363   my $title               = delete($params{title}) || $::locale->text($by_spec->{title});
364
365   if ($current_sort_params{sort_by} eq $by) {
366     my $current_dir = $current_sort_params{sort_dir} ? 'up' : 'down';
367     $image          = '<img border="0" src="image/' . $current_dir . '.png">';
368     $new_dir        = 1 - ($current_sort_params{sort_dir} || 0);
369   }
370
371   $params{ $models->sorted->form_params->[0] } = $by;
372   $params{ $models->sorted->form_params->[1] } = ($new_dir ? '1' : '0');
373
374   return '<a href="' . $models->get_callback(%params) . '">' . _H($title) . $image . '</a>';
375 }
376
377 sub paginate_controls {
378   my ($self, %params) = _hashify(1, @_);
379
380   my $controller      = $self->{CONTEXT}->stash->get('SELF');
381   my $models          = $params{models} || $self->{CONTEXT}->stash->get('MODELS');
382   my $pager           = $models->paginated;
383 #  my $paginate_spec   = $controller->get_paginate_spec;
384
385   my %paginate_params = $models->get_paginate_args;
386
387   my %template_params = (
388     pages             => \%paginate_params,
389     url_maker         => sub {
390       my %url_params                                    = _hashify(0, @_);
391       $url_params{ $pager->form_params->[0] } = delete $url_params{page};
392       $url_params{ $pager->form_params->[1] } = delete $url_params{per_page} if exists $url_params{per_page};
393
394       return $models->get_callback(%url_params);
395     },
396     %params,
397   );
398
399   return SL::Presenter->get->render('common/paginate', %template_params);
400 }
401
402 1;
403
404 __END__
405
406 =head1 NAME
407
408 SL::Templates::Plugin::L -- Layouting / tag generation
409
410 =head1 SYNOPSIS
411
412 Usage from a template:
413
414   [% USE L %]
415
416   [% L.select_tag('direction', [ [ 'left', 'To the left' ], [ 'right', 'To the right', 1 ] ]) %]
417
418   [% L.select_tag('direction', [ { direction => 'left',  display => 'To the left'  },
419                                  { direction => 'right', display => 'To the right' } ],
420                                value_key => 'direction', title_key => 'display', default => 'right')) %]
421
422   [% L.select_tag('direction', [ { direction => 'left',  display => 'To the left'  },
423                                  { direction => 'right', display => 'To the right', selected => 1 } ],
424                                value_key => 'direction', title_key => 'display')) %]
425
426 =head1 DESCRIPTION
427
428 A module modeled a bit after Rails' ActionView helpers. Several small
429 functions that create HTML tags from various kinds of data sources.
430
431 The C<id> attribute is usually calculated automatically. This can be
432 overridden by either specifying an C<id> attribute or by setting
433 C<no_id> to trueish.
434
435 =head1 FUNCTIONS
436
437 =head2 LOW-LEVEL FUNCTIONS
438
439 The following items are just forwarded to L<SL::Presenter::Tag>:
440
441 =over 2
442
443 =item * C<name_to_id $name>
444
445 =item * C<stringify_attributes %items>
446
447 =item * C<html_tag $tag_name, $content_string, %attributes>
448
449 =back
450
451 =head2 HIGH-LEVEL FUNCTIONS
452
453 The following functions are just forwarded to L<SL::Presenter::Tag>:
454
455 =over 2
456
457 =item * C<input_tag $name, $value, %attributes>
458
459 =item * C<hidden_tag $name, $value, %attributes>
460
461 =item * C<checkbox_tag $name, %attributes>
462
463 =item * C<select_tag $name, \@collection, %attributes>
464
465 =item * C<link $href, $content, %attributes>
466
467 =back
468
469 Available high-level functions implemented in this module:
470
471 =over 4
472
473 =item C<yes_no_tag $name, $value, %attributes>
474
475 Creates a HTML 'select' tag with the two entries C<yes> and C<no> by
476 calling L<select_tag>. C<$value> determines
477 which entry is selected. The C<%attributes> are passed through to
478 L<select_tag>.
479
480 =item C<textarea_tag $name, $value, %attributes>
481
482 Creates a HTML 'textarea' tag named C<$name> with the content
483 C<$value> and with arbitrary HTML attributes from C<%attributes>. The
484 tag's C<id> defaults to C<name_to_id($name)>.
485
486 =item C<date_tag $name, $value, %attributes>
487
488 Creates a date input field, with an attached javascript that will open a
489 calendar on click.
490
491 =item C<radio_button_tag $name, %attributes>
492
493 Creates a HTML 'input type=radio' tag named C<$name> with arbitrary
494 HTML attributes from C<%attributes>. The tag's C<value> defaults to
495 C<1>. The tag's C<id> defaults to C<name_to_id($name . "_" . $value)>.
496
497 If C<%attributes> contains a key C<label> then a HTML 'label' tag is
498 created with said C<label>. No attribute named C<label> is created in
499 that case.
500
501 =item C<javascript_tag $file1, $file2, $file3...>
502
503 Creates a HTML 'E<lt>script type="text/javascript" src="..."E<gt>'
504 tag for each file name parameter passed. Each file name will be
505 postfixed with '.js' if it isn't already and prefixed with 'js/' if it
506 doesn't contain a slash.
507
508 =item C<stylesheet_tag $file1, $file2, $file3...>
509
510 Creates a HTML 'E<lt>link rel="text/stylesheet" href="..."E<gt>' tag
511 for each file name parameter passed. Each file name will be postfixed
512 with '.css' if it isn't already and prefixed with 'css/' if it doesn't
513 contain a slash.
514
515 =item C<tabbed \@tab, %attributes>
516
517 Will create a tabbed area. The tabs should be created with the helper function
518 C<tab>. Example:
519
520   [% L.tabbed([
521     L.tab(LxERP.t8('Basic Data'),       'part/_main_tab.html'),
522     L.tab(LxERP.t8('Custom Variables'), 'part/_cvar_tab.html', if => SELF.display_cvar_tab),
523   ]) %]
524
525 =item C<areainput_tag $name, $content, %PARAMS>
526
527 Creates a generic input tag or textarea tag, depending on content size. The
528 amount of desired rows must be either given with the C<rows> parameter or can
529 be computed from the value and the C<cols> paramter, Accepted parameters
530 include C<min_rows> for rendering a minimum of rows if a textarea is displayed.
531
532 You can force input by setting rows to 1, and you can force textarea by setting
533 rows to anything >1.
534
535 =item C<multiselect2side $id, %params>
536
537 Creates a JavaScript snippet calling the jQuery function
538 C<multiselect2side> on the select control with the ID C<$id>. The
539 select itself is not created. C<%params> can contain the following
540 entries:
541
542 =over 2
543
544 =item C<labelsx>
545
546 The label of the list of available options. Defaults to the
547 translation of 'Available'.
548
549 =item C<labeldx>
550
551 The label of the list of selected options. Defaults to the
552 translation of 'Selected'.
553
554 =back
555
556 =item C<sortable_element $selector, %params>
557
558 Makes the children of the DOM element C<$selector> (a jQuery selector)
559 sortable with the I<jQuery UI Selectable> library. The children can be
560 dragged & dropped around. After dropping an element an URL can be
561 postet to with the element IDs of the sorted children.
562
563 If this is used then the JavaScript file C<js/jquery-ui.js> must be
564 included manually as well as it isn't loaded via C<$::form-gt;header>.
565
566 C<%params> can contain the following entries:
567
568 =over 2
569
570 =item C<url>
571
572 The URL to POST an AJAX request to after a dragged element has been
573 dropped. The AJAX request's return value is ignored by default. If given then
574 C<$params{with}> must be given as well.
575
576 =item C<ajax_return>
577
578 If trueish then the AJAX request's return is accepted.
579
580 =item C<with>
581
582 A string that is interpreted as the prefix of the children's ID. Upon
583 POSTing the result each child whose ID starts with C<$params{with}> is
584 considered. The prefix and the following "_" is removed from the
585 ID. The remaining parts of the IDs of those children are posted as a
586 single array parameter. The array parameter's name is either
587 C<$params{as}> or, missing that, C<$params{with}>.
588
589 =item C<as>
590
591 Sets the POST parameter name for AJAX request after dropping an
592 element (see C<$params{with}>).
593
594 =item C<handle>
595
596 An optional jQuery selector specifying which part of the child element
597 is dragable. If the parameter is not given then it defaults to
598 C<.dragdrop> matching DOM elements with the class C<dragdrop>.  If the
599 parameter is set and empty then the whole child element is dragable,
600 and clicks through to underlying elements like inputs or links might
601 not work.
602
603 =item C<dont_recolor>
604
605 If trueish then the children will not be recolored. The default is to
606 recolor the children by setting the class C<listrow0> on odd and
607 C<listrow1> on even entries.
608
609 =item C<params>
610
611 An optional JavaScript string that is evaluated before sending the
612 POST request. The result must be a string that is appended to the URL.
613
614 =back
615
616 Example:
617
618   <script type="text/javascript" src="js/jquery-ui.js"></script>
619
620   <table id="thing_list">
621     <thead>
622       <tr><td>This</td><td>That</td></tr>
623     </thead>
624     <tbody>
625       <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
626       <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
627       <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
628     </tbody>
629   <table>
630
631   [% L.sortable_element('#thing_list tbody',
632                         url          => 'controller.pl?action=SystemThings/reorder',
633                         with         => 'thingy',
634                         as           => 'thing_ids',
635                         recolor_rows => 1) %]
636
637 After dropping e.g. the third element at the top of the list a POST
638 request would be made to the C<reorder> action of the C<SystemThings>
639 controller with a single parameter called C<thing_ids> -- an array
640 containing the values C<[ 6, 2, 15 ]>.
641
642 =item C<dump REF>
643
644 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
645
646 =item C<sortable_table_header $by, %params>
647
648 Create a link and image suitable for placement in a table
649 header. C<$by> must be an index set up by the controller with
650 L<SL::Controller::Helper::make_sorted>.
651
652 The optional parameter C<$params{title}> can override the column title
653 displayed to the user. Otherwise the column title from the
654 controller's sort spec is used.
655
656 The other parameters in C<%params> are passed unmodified to the
657 underlying call to L<SL::Controller::Base::url_for>.
658
659 See the documentation of L<SL::Controller::Helper::Sorted> for an
660 overview and further usage instructions.
661
662 =item C<paginate_controls>
663
664 Create a set of links used to paginate a list view.
665
666 See the documentation of L<SL::Controller::Helper::Paginated> for an
667 overview and further usage instructions.
668
669 =back
670
671 =head2 CONVERSION FUNCTIONS
672
673 =over 4
674
675 =item C<tab, description, target, %PARAMS>
676
677 Creates a tab for C<tabbed>. The description will be used as displayed name.
678 The target should be a block or template that can be processed. C<tab> supports
679 a C<method> parameter, which can override the process method to apply target.
680 C<method => 'raw'> will just include the given text as is. I was too lazy to
681 implement C<include> properly.
682
683 Also an C<if> attribute is supported, so that tabs can be suppressed based on
684 some occasion. In this case the supplied block won't even get processed, and
685 the resulting tab will get ignored by C<tabbed>:
686
687   L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
688
689 =item C<truncate $text, [%params]>
690
691 See L<SL::Presenter::Text/truncate>.
692
693 =item C<simple_format $text>
694
695 See L<SL::Presenter::Text/simple_format>.
696
697 =back
698
699 =head1 MODULE AUTHORS
700
701 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
702
703 L<http://linet-services.de>