marei: tl2019 compat
[kivitendo-erp.git] / SL / Layout / ActionBar.pm
1 package SL::Layout::ActionBar;
2
3 use strict;
4 use parent qw(SL::Layout::Base);
5
6 use Carp;
7 use Scalar::Util qw(blessed);
8 use SL::Layout::ActionBar::Action;
9 use SL::Layout::ActionBar::ComboBox;
10 use SL::Layout::ActionBar::Link;
11 use SL::Layout::ActionBar::Separator;
12
13 use SL::Presenter::Tag qw(html_tag);
14
15 use constant HTML_CLASS => 'layout-actionbar';
16
17 use Rose::Object::MakeMethods::Generic (
18   'scalar --get_set_init' => [ qw(actions) ],
19 );
20
21 my %class_descriptors = (
22   action    => { class => 'SL::Layout::ActionBar::Action',    num_params => 1, },
23   combobox  => { class => 'SL::Layout::ActionBar::ComboBox',  num_params => 1, },
24   link      => { class => 'SL::Layout::ActionBar::Link',      num_params => 1, },
25   separator => { class => 'SL::Layout::ActionBar::Separator', num_params => 0, },
26 );
27
28 ###### Layout overrides
29
30 sub pre_content {
31   my ($self) = @_;
32
33   my $content = join '', map { $_->render } @{ $self->actions };
34   return if !$content;
35   html_tag('div', $content, class => HTML_CLASS);
36 }
37
38 sub javascripts_inline {
39   join '', map { $_->script } @{ $_[0]->actions };
40 }
41
42 sub javascripts {
43   'kivi.ActionBar.js'
44 }
45
46 ###### interface
47
48 sub add {
49   my ($self, @actions) = @_;
50
51   push @{ $self->actions }, $self->parse_actions(@actions);
52
53   return $self->actions->[-1];
54 }
55
56 sub parse_actions {
57   my ($self_or_class, @actions) = @_;
58
59   my @parsed;
60
61   while (my $type = shift(@actions)) {
62     if (blessed($type) && $type->isa('SL::Layout::ActionBar::Action')) {
63       push @parsed, $type;
64       next;
65     }
66
67     my $descriptor = $class_descriptors{lc $type} || croak("Unknown action type '${type}'");
68     my @params     = splice(@actions, 0, $descriptor->{num_params});
69
70     push @parsed, $descriptor->{class}->from_params(@params);
71   }
72
73   return @parsed;
74 }
75
76 sub init_actions {
77   []
78 }
79
80 1;
81
82 __END__
83
84 =encoding utf-8
85
86 =head1 NAME
87
88 SL::Layout::ActionBar - Unified action buttons for controllers
89
90 =head1 SYNOPSIS
91
92   # short sugared syntax:
93   for my $bar ($::request->layout->get('actionbar')) {
94     $bar->add(
95       action => [
96         t8('Description'),
97         call      => [ 'kivi.Javascript.function', @arguments ],
98         accesskey => 'enter',
99         disabled  => $tooltip_with_reason_or_falsish,
100         only_if   => $precomputed_condition,
101         not_if    => $precomputed_condition,
102         id        => 'html-element-id',
103       ],
104       combobox => [
105         action => [...],
106         action => [...],
107         action => [...],
108         action => [...],
109       ],
110       link => [
111         t8('Description'),
112         link => $url,
113       ],
114       'separator',
115     );
116   }
117
118   # full syntax without sugar
119   for my $bar ($::request->layout->get('actionbar')) {
120     $bar->add(
121       (SL::Layout::ActionBar::Action->new(
122         text => t8('Description'),
123         params => {
124           call      => [ 'kivi.Javascript.function', @arguments ],
125           accesskey => 'enter',
126           disabled  => $tooltip_with_reason_or_falsish,
127         },
128       )) x(!!$only_id && !$not_if),
129       SL::Layout::ActionBar::ComboBox->new(
130         actions => [
131           SL::Layout::ActionBar::Action->new(...),
132           SL::Layout::ActionBar::Action->new(...),
133           SL::Layout::ActionBar::Action->new(...),
134           SL::Layout::ActionBar::Action->new(...),
135         ],
136       ),
137       SL::Layout::ActionBar::Link->new(
138         text => t8('Description'),
139         params => {
140           link => $url,
141         },
142       ),
143       SL::Layout::ActionBar::Separator->new,
144     );
145   }
146
147 =head1 CONCEPT
148
149 This is a layout block that creates an action bar for any controller who
150 wants to use it. It's designed to be rendered above the content and to be
151 fixed when scrolling. It's structured as a container for elements that can be
152 extended when needed.
153
154 =head1 METHODS
155
156 =over 4
157
158 =item * C<new>
159
160 Will be used during initialization of the layout. You should never have to
161 instanciate an action bar yourself. Get the current request instances from
162
163   $::request->layout->get('actionbar')
164
165 instead.
166
167 =item * C<add>
168
169 Add new elements to the bar. Can be instances of
170 L<SL::Layout::ActionBar::Action> or scalar strings matching the sugar syntax
171 which is described further down.
172
173 =back
174
175 =head1 SYNTACTIC SUGAR
176
177 Instead of passing full objects to L</add>, you can instead pass the arguments
178 to be used for instantiation to make the code easier to read. The short syntax
179 looks like this:
180
181   type => [
182     localized_description,
183     param => value,
184     param => value,
185     ...
186   ]
187
188 A string type, followed by the parameters needed for that type. Type may be one of:
189
190 =over 4
191
192 =item * C<action>
193
194 =item * C<combobox>
195
196 =item * C<link>
197
198 =item * C<separator>
199
200 =back
201
202 C<separator> will use no parameters, the other three will expect one arrayref.
203
204 Two additional pseudo parameters are supported for those:
205
206 =over 4
207
208 =item * C<only_if>
209
210 =item * C<not_if>
211
212 =back
213
214 These are meant to reduce enterprise operators (C<()x!!>) when conditionally adding lots
215 of elements.
216
217 The combobox element is in itself a container and will simply expect the same
218 syntax in an arrayref.
219
220 For the full list of parameters supported by the elements, see L<SL::Layout::ActionBar::Action/RECOGNIZED PARAMETERS>.
221
222
223 =head1 GUIDELINES
224
225 The current implementation follows these design guidelines:
226
227 =over 4
228
229 =item *
230
231 Don't put too many elements into the action bar. Group into comboboxes if
232 possible. Consider seven elements a reasonable limit.
233
234 =item *
235
236 If you've got an update button, put it first and bind the enter accesskey to
237 it.
238
239 =item *
240
241 Put mutating actions (save, post, delete, check out, ship) before the separator
242 and non mutating actions (export, search, history, workflow) after the
243 separator. Combined actions (save and close) still mutate and go before the
244 separator.
245
246 =item *
247
248 Avoid abusing the actionbar as a secondary menu. As a principle every action
249 should act upon the current element or topic.
250
251 =item *
252
253 Hide elements with C<only_if> if they are known to be useless for the current
254 topic, but disable when they would be useful in principle but are not
255 applicable right now. For example C<delete> does not make sense in a creating
256 form, but makes still sense because the element can be deleted later. This
257 keeps the actionbar stable and reduces surprising elements that only appear in
258 rare situations.
259
260 =item *
261
262 Always add a tooltip when disabling an action.
263
264 =item *
265
266 Try to always add a default action with accesskey enter. Since the actionbar
267 lies outside of the main form, the usual submit on enter does not work out of
268 the box.
269
270 =back
271
272 =head1 DOM MODEL AND IMPLEMENTATION DETAILS
273
274 The entire block is rendered into a div with the class 'layout-actionbar'. Each
275 action will render itself and will get added to the div. To keep the DOM small
276 and reduce startup overhead, the presentation is pure CSS and only the sticky
277 expansion of comboboxes is done with javascript.
278
279 To keep startup times and HTML parsing fast the action data is simply written
280 into the data elements of the actions and handlers are added in a ready hook.
281
282 =head1 BUGS
283
284 none yet. :)
285
286 =head1 SEE ALSO
287
288 L<SL::Layout::ActioBar::Base>,
289 L<SL::Layout::ActioBar::Action>,
290 L<SL::Layout::ActioBar::Submit>,
291 L<SL::Layout::ActioBar::ComboBox>,
292 L<SL::Layout::ActioBar::Separator>,
293 L<SL::Layout::ActioBar::Link>,
294
295 =head1 AUTHOR
296
297 Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>
298
299 =cut