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