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