Controller::Base::url_for: nur noch fragment erlauben
[kivitendo-erp.git] / SL / Template / Plugin / L.pm
1 package SL::Template::Plugin::L;
2
3 use base qw( Template::Plugin );
4 use Template::Plugin;
5 use Data::Dumper;
6 use List::MoreUtils qw(apply);
7 use List::Util qw(max);
8 use Scalar::Util qw(blessed);
9
10 use SL::Presenter;
11 use SL::Presenter::ALL;
12 use SL::Presenter::Simple;
13 use SL::Util qw(_hashify);
14
15 use strict;
16
17 { # This will give you an id for identifying html tags and such.
18   # It's guaranteed to be unique unless you exceed 10 mio calls per request.
19   # Do not use these id's to store information across requests.
20 my $_id_sequence = int rand 1e7;
21 sub _tag_id {
22   return "id_" . ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
23 }
24 }
25
26 sub _H {
27   my $string = shift;
28   return $::locale->quote_special_chars('HTML', $string);
29 }
30
31 sub _J {
32   my $string = shift;
33   $string    =~ s/(\"|\'|\\)/\\$1/g;
34   return $string;
35 }
36
37 sub new {
38   my ($class, $context, @args) = @_;
39
40   return bless {
41     CONTEXT => $context,
42   }, $class;
43 }
44
45 sub _context {
46   die 'not an accessor' if @_ > 1;
47   return $_[0]->{CONTEXT};
48 }
49
50 sub _call_presenter {
51   my ($method, $self, @args) = @_;
52
53   my $presenter              = $::request->presenter;
54
55   splice @args, -1, 1, %{ $args[-1] } if @args && (ref($args[-1]) eq 'HASH');
56
57   if (my $sub = SL::Presenter::Simple->can($method)) {
58     return $sub->(@args);
59   }
60
61   if ($presenter->can($method)) {
62     return $presenter->$method(@args);
63   }
64
65   $::lxdebug->message(LXDebug::WARN(), "SL::Presenter has no method named '$method'!");
66   return;
67 }
68
69 sub name_to_id    { return _call_presenter('name_to_id',    @_); }
70 sub html_tag      { return _call_presenter('html_tag',      @_); }
71 sub hidden_tag    { return _call_presenter('hidden_tag',    @_); }
72 sub select_tag    { return _call_presenter('select_tag',    @_); }
73 sub checkbox_tag  { return _call_presenter('checkbox_tag',  @_); }
74 sub input_tag     { return _call_presenter('input_tag',     @_); }
75 sub javascript    { return _call_presenter('javascript',    @_); }
76 sub truncate      { return _call_presenter('truncate',      @_); }
77 sub simple_format { return _call_presenter('simple_format', @_); }
78 sub button_tag               { return _call_presenter('button_tag',               @_); }
79 sub submit_tag               { return _call_presenter('submit_tag',               @_); }
80 sub ajax_submit_tag          { return _call_presenter('ajax_submit_tag',          @_); }
81 sub link                     { return _call_presenter('link_tag',                 @_); }
82 sub input_number_tag         { return _call_presenter('input_number_tag',         @_); }
83 sub textarea_tag             { return _call_presenter('textarea_tag',             @_); }
84 sub date_tag                 { return _call_presenter('date_tag',                 @_); }
85
86 sub _set_id_attribute {
87   my ($attributes, $name, $unique) = @_;
88   SL::Presenter::Tag::_set_id_attribute($attributes, $name, $unique);
89 }
90
91 sub img_tag {
92   my ($self, %options) = _hashify(1, @_);
93
94   $options{alt} ||= '';
95
96   return $self->html_tag('img', undef, %options);
97 }
98
99 sub radio_button_tag {
100   my ($self, $name, %attributes) = _hashify(2, @_);
101
102   $attributes{value}   = 1 unless exists $attributes{value};
103
104   _set_id_attribute(\%attributes, $name, 1);
105   my $label            = delete $attributes{label};
106
107   _set_id_attribute(\%attributes, $name . '_' . $attributes{value});
108
109   if ($attributes{checked}) {
110     $attributes{checked} = 'checked';
111   } else {
112     delete $attributes{checked};
113   }
114
115   my $code  = $self->html_tag('input', undef,  %attributes, name => $name, type => 'radio');
116   $code    .= $self->html_tag('label', $label, for => $attributes{id}) if $label;
117
118   return $code;
119 }
120
121 sub div_tag {
122   my ($self, $content, @slurp) = @_;
123   return $self->html_tag('div', $content, @slurp);
124 }
125
126 sub ul_tag {
127   my ($self, $content, @slurp) = @_;
128   return $self->html_tag('ul', $content, @slurp);
129 }
130
131 sub li_tag {
132   my ($self, $content, @slurp) = @_;
133   return $self->html_tag('li', $content, @slurp);
134 }
135
136 sub yes_no_tag {
137   my ($self, $name, $value, %attributes) = _hashify(3, @_);
138
139   return $self->select_tag($name, [ [ 1 => $::locale->text('Yes') ], [ 0 => $::locale->text('No') ] ], default => $value ? 1 : 0, %attributes);
140 }
141
142 sub stylesheet_tag {
143   my $self = shift;
144   my $code = '';
145
146   foreach my $file (@_) {
147     $file .= '.css'        unless $file =~ m/\.css$/;
148     $file  = "css/${file}" unless $file =~ m|/|;
149
150     $code .= qq|<link rel="stylesheet" href="${file}" type="text/css" media="screen" />|;
151   }
152
153   return $code;
154 }
155
156
157 # simple version with select_tag
158 sub vendor_selector {
159   my ($self, $name, $value, %params) = _hashify(3, @_);
160
161   my $actual_vendor_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"}) ? $::form->{"$name"}->id : $::form->{"$name"}) :
162                          (ref $value && $value->can('id')) ? $value->id : '';
163
164   return $self->select_tag($name, SL::DB::Manager::Vendor->get_all(),
165                                   default      => $actual_vendor_id,
166                                   title_sub    => sub { $_[0]->vendornumber . " : " . $_[0]->name },
167                                   'with_empty' => 1,
168                                   %params);
169 }
170
171
172 # simple version with select_tag
173 sub part_selector {
174   my ($self, $name, $value, %params) = _hashify(3, @_);
175
176   my $actual_part_id = (defined $::form->{"$name"})? ((ref $::form->{"$name"})? $::form->{"$name"}->id : $::form->{"$name"}) :
177                        (ref $value && $value->can('id')) ? $value->id : '';
178
179   return $self->select_tag($name, SL::DB::Manager::Part->get_all(),
180                            default      => $actual_part_id,
181                            title_sub    => sub { $_[0]->partnumber . " : " . $_[0]->description },
182                            with_empty   => 1,
183                            %params);
184 }
185
186
187 sub javascript_tag {
188   my $self = shift;
189   my $code = '';
190
191   foreach my $file (@_) {
192     $file .= '.js'        unless $file =~ m/\.js$/;
193     $file  = "js/${file}" unless $file =~ m|/|;
194
195     $code .= qq|<script type="text/javascript" src="${file}"></script>|;
196   }
197
198   return $code;
199 }
200
201 sub tabbed {
202   my ($self, $tabs, %params) = _hashify(2, @_);
203   my $id       = $params{id} || 'tab_' . _tag_id();
204
205   $params{selected} *= 1;
206
207   die 'L.tabbed needs an arrayred of tabs for first argument'
208     unless ref $tabs eq 'ARRAY';
209
210   my (@header, @blocks);
211   for my $i (0..$#$tabs) {
212     my $tab = $tabs->[$i];
213
214     next if $tab eq '';
215
216     my $tab_id = "__tab_id_$i";
217     push @header, $self->li_tag($self->link('#' . $tab_id, $tab->{name}));
218     push @blocks, $self->div_tag($tab->{data}, id => $tab_id);
219   }
220
221   return '' unless @header;
222
223   my $ul = $self->ul_tag(join('', @header), id => $id);
224   return $self->div_tag(join('', $ul, @blocks), class => 'tabwidget');
225 }
226
227 sub tab {
228   my ($self, $name, $src, %params) = _hashify(3, @_);
229
230   $params{method} ||= 'process';
231
232   return () if defined $params{if} && !$params{if};
233
234   my $data;
235   if ($params{method} eq 'raw') {
236     $data = $src;
237   } elsif ($params{method} eq 'process') {
238     $data = $self->_context->process($src, %{ $params{args} || {} });
239   } else {
240     die "unknown tag method '$params{method}'";
241   }
242
243   return () unless $data;
244
245   return +{ name => $name, data => $data };
246 }
247
248 sub areainput_tag {
249   my ($self, $name, $value, %attributes) = _hashify(3, @_);
250
251   my $cols    = delete $attributes{cols} || delete $attributes{size};
252   my $minrows = delete $attributes{min_rows} || 1;
253   my $maxrows = delete $attributes{max_rows};
254   my $rows    = $::form->numtextrows($value, $cols, $maxrows, $minrows);
255
256   $attributes{id} ||= _tag_id();
257   my $id            = $attributes{id};
258
259   return $self->textarea_tag($name, $value, %attributes, rows => $rows, cols => $cols) if $rows > 1;
260
261   return '<span>'
262     . $self->input_tag($name, $value, %attributes, size => $cols)
263     . "<img src=\"image/edit-entry.png\" onclick=\"kivi.switch_areainput_to_textarea('${id}')\" style=\"margin-left: 2px;\">"
264     . '</span>';
265 }
266
267 sub multiselect2side {
268   my ($self, $id, %params) = _hashify(2, @_);
269
270   $params{labelsx}        = "\"" . _J($params{labelsx} || $::locale->text('Available')) . "\"";
271   $params{labeldx}        = "\"" . _J($params{labeldx} || $::locale->text('Selected'))  . "\"";
272   $params{moveOptions}    = 'false';
273
274   my $vars                = join(', ', map { "${_}: " . $params{$_} } keys %params);
275   my $code                = <<EOCODE;
276 <script type="text/javascript">
277   \$().ready(function() {
278     \$('#${id}').multiselect2side({ ${vars} });
279   });
280 </script>
281 EOCODE
282
283   return $code;
284 }
285
286 sub sortable_element {
287   my ($self, $selector, %params) = _hashify(2, @_);
288
289   my %attributes = ( distance => 5,
290                      helper   => <<'JAVASCRIPT' );
291     function(event, ui) {
292       ui.children().each(function() {
293         $(this).width($(this).width());
294       });
295       return ui;
296     }
297 JAVASCRIPT
298
299   my $stop_event = '';
300
301   if ($params{url} && $params{with}) {
302     my $as      = $params{as} || $params{with};
303     my $filter  = ".filter(function(idx) { return this.substr(0, " . length($params{with}) . ") == '$params{with}'; })";
304     $filter    .= ".map(function(idx, str) { return str.replace('$params{with}_', ''); })";
305
306     my $params_js = $params{params} ? qq| + ($params{params})| : '';
307
308     $stop_event = <<JAVASCRIPT;
309         \$.post('$params{url}'${params_js}, { '${as}[]': \$(\$('${selector}').sortable('toArray'))${filter}.toArray() });
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. If given then
570 C<$params{with}> must be given as well.
571
572 =item C<with>
573
574 A string that is interpreted as the prefix of the children's ID. Upon
575 POSTing the result each child whose ID starts with C<$params{with}> is
576 considered. The prefix and the following "_" is removed from the
577 ID. The remaining parts of the IDs of those children are posted as a
578 single array parameter. The array parameter's name is either
579 C<$params{as}> or, missing that, C<$params{with}>.
580
581 =item C<as>
582
583 Sets the POST parameter name for AJAX request after dropping an
584 element (see C<$params{with}>).
585
586 =item C<handle>
587
588 An optional jQuery selector specifying which part of the child element
589 is dragable. If the parameter is not given then it defaults to
590 C<.dragdrop> matching DOM elements with the class C<dragdrop>.  If the
591 parameter is set and empty then the whole child element is dragable,
592 and clicks through to underlying elements like inputs or links might
593 not work.
594
595 =item C<dont_recolor>
596
597 If trueish then the children will not be recolored. The default is to
598 recolor the children by setting the class C<listrow0> on odd and
599 C<listrow1> on even entries.
600
601 =item C<params>
602
603 An optional JavaScript string that is evaluated before sending the
604 POST request. The result must be a string that is appended to the URL.
605
606 =back
607
608 Example:
609
610   <script type="text/javascript" src="js/jquery-ui.js"></script>
611
612   <table id="thing_list">
613     <thead>
614       <tr><td>This</td><td>That</td></tr>
615     </thead>
616     <tbody>
617       <tr id="thingy_2"><td>stuff</td><td>more stuff</td></tr>
618       <tr id="thingy_15"><td>stuff</td><td>more stuff</td></tr>
619       <tr id="thingy_6"><td>stuff</td><td>more stuff</td></tr>
620     </tbody>
621   <table>
622
623   [% L.sortable_element('#thing_list tbody',
624                         url          => 'controller.pl?action=SystemThings/reorder',
625                         with         => 'thingy',
626                         as           => 'thing_ids',
627                         recolor_rows => 1) %]
628
629 After dropping e.g. the third element at the top of the list a POST
630 request would be made to the C<reorder> action of the C<SystemThings>
631 controller with a single parameter called C<thing_ids> -- an array
632 containing the values C<[ 6, 2, 15 ]>.
633
634 =item C<dump REF>
635
636 Dumps the Argument using L<Data::Dumper> into a E<lt>preE<gt> block.
637
638 =item C<sortable_table_header $by, %params>
639
640 Create a link and image suitable for placement in a table
641 header. C<$by> must be an index set up by the controller with
642 L<SL::Controller::Helper::make_sorted>.
643
644 The optional parameter C<$params{title}> can override the column title
645 displayed to the user. Otherwise the column title from the
646 controller's sort spec is used.
647
648 The other parameters in C<%params> are passed unmodified to the
649 underlying call to L<SL::Controller::Base::url_for>.
650
651 See the documentation of L<SL::Controller::Helper::Sorted> for an
652 overview and further usage instructions.
653
654 =item C<paginate_controls>
655
656 Create a set of links used to paginate a list view.
657
658 See the documentation of L<SL::Controller::Helper::Paginated> for an
659 overview and further usage instructions.
660
661 =back
662
663 =head2 CONVERSION FUNCTIONS
664
665 =over 4
666
667 =item C<tab, description, target, %PARAMS>
668
669 Creates a tab for C<tabbed>. The description will be used as displayed name.
670 The target should be a block or template that can be processed. C<tab> supports
671 a C<method> parameter, which can override the process method to apply target.
672 C<method => 'raw'> will just include the given text as is. I was too lazy to
673 implement C<include> properly.
674
675 Also an C<if> attribute is supported, so that tabs can be suppressed based on
676 some occasion. In this case the supplied block won't even get processed, and
677 the resulting tab will get ignored by C<tabbed>:
678
679   L.tab('Awesome tab wih much info', '_much_info.html', if => SELF.wants_all)
680
681 =item C<truncate $text, [%params]>
682
683 See L<SL::Presenter::Text/truncate>.
684
685 =item C<simple_format $text>
686
687 See L<SL::Presenter::Text/simple_format>.
688
689 =back
690
691 =head1 MODULE AUTHORS
692
693 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
694
695 L<http://linet-services.de>