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