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