auf Original-Version zurückgesetzt
[kivitendo-erp.git] / SL / Layout / ActionBar.pm
index edbd44e..5345e9e 100644 (file)
@@ -3,7 +3,14 @@ package SL::Layout::ActionBar;
 use strict;
 use parent qw(SL::Layout::Base);
 
+use Carp;
+use Scalar::Util qw(blessed);
 use SL::Layout::ActionBar::Action;
+use SL::Layout::ActionBar::ComboBox;
+use SL::Layout::ActionBar::Link;
+use SL::Layout::ActionBar::Separator;
+
+use SL::Presenter::Tag qw(html_tag);
 
 use constant HTML_CLASS => 'layout-actionbar';
 
@@ -11,6 +18,12 @@ use Rose::Object::MakeMethods::Generic (
   'scalar --get_set_init' => [ qw(actions) ],
 );
 
+my %class_descriptors = (
+  action    => { class => 'SL::Layout::ActionBar::Action',    num_params => 1, },
+  combobox  => { class => 'SL::Layout::ActionBar::ComboBox',  num_params => 1, },
+  link      => { class => 'SL::Layout::ActionBar::Link',      num_params => 1, },
+  separator => { class => 'SL::Layout::ActionBar::Separator', num_params => 0, },
+);
 
 ###### Layout overrides
 
@@ -18,35 +31,51 @@ sub pre_content {
   my ($self) = @_;
 
   my $content = join '', map { $_->render } @{ $self->actions };
-  $::request->presenter->html_tag('div', $content, class => HTML_CLASS);
+  return if !$content;
+  html_tag('div', $content, class => HTML_CLASS);
 }
 
 sub javascripts_inline {
   join '', map { $_->script } @{ $_[0]->actions };
 }
 
-sub javascripts {
+sub static_javascripts {
   'kivi.ActionBar.js'
 }
 
 ###### interface
 
-sub add_actions {
+sub add {
   my ($self, @actions) = @_;
-  push @{ $self->actions }, map {
-       !ref $_ ? SL::Layout::ActionBar::Action->from_descriptor($_)
-     :  ref $_ && 'ARRAY' eq ref $_ ? SL::Layout::ActionBar::Action->simple($_)
-     :  ref $_ && $_->isa('SL::Layout::Action') ? $_
-     : do { die 'invalid action' };
-  } @actions;
-}
 
-sub init_actions {
-  []
+  push @{ $self->actions }, $self->parse_actions(@actions);
+
+  return $self->actions->[-1];
 }
 
+sub parse_actions {
+  my ($self_or_class, @actions) = @_;
+
+  my @parsed;
+
+  while (my $type = shift(@actions)) {
+    if (blessed($type) && $type->isa('SL::Layout::ActionBar::Action')) {
+      push @parsed, $type;
+      next;
+    }
 
+    my $descriptor = $class_descriptors{lc $type} || croak("Unknown action type '${type}'");
+    my @params     = splice(@actions, 0, $descriptor->{num_params});
 
+    push @parsed, $descriptor->{class}->from_params(@params);
+  }
+
+  return @parsed;
+}
+
+sub init_actions {
+  []
+}
 
 1;
 
@@ -58,65 +87,211 @@ __END__
 
 SL::Layout::ActionBar - Unified action buttons for controllers
 
+=head1 SYNOPSIS
+
+  # short sugared syntax:
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      action => [
+        t8('Description'),
+        call      => [ 'kivi.Javascript.function', @arguments ],
+        accesskey => 'enter',
+        disabled  => $tooltip_with_reason_or_falsish,
+        only_if   => $precomputed_condition,
+        not_if    => $precomputed_condition,
+        id        => 'html-element-id',
+      ],
+      combobox => [
+        action => [...],
+        action => [...],
+        action => [...],
+        action => [...],
+      ],
+      link => [
+        t8('Description'),
+        link => $url,
+      ],
+      'separator',
+    );
+  }
+
+  # full syntax without sugar
+  for my $bar ($::request->layout->get('actionbar')) {
+    $bar->add(
+      (SL::Layout::ActionBar::Action->new(
+        text => t8('Description'),
+        params => {
+          call      => [ 'kivi.Javascript.function', @arguments ],
+          accesskey => 'enter',
+          disabled  => $tooltip_with_reason_or_falsish,
+        },
+      )) x(!!$only_id && !$not_if),
+      SL::Layout::ActionBar::ComboBox->new(
+        actions => [
+          SL::Layout::ActionBar::Action->new(...),
+          SL::Layout::ActionBar::Action->new(...),
+          SL::Layout::ActionBar::Action->new(...),
+          SL::Layout::ActionBar::Action->new(...),
+        ],
+      ),
+      SL::Layout::ActionBar::Link->new(
+        text => t8('Description'),
+        params => {
+          link => $url,
+        },
+      ),
+      SL::Layout::ActionBar::Separator->new,
+    );
+  }
+
 =head1 CONCEPT
 
-This is a layout block that does a unified action bar for any controller who
+This is a layout block that creates an action bar for any controller who
 wants to use it. It's designed to be rendered above the content and to be
-fixed when scrolling.
+fixed when scrolling. It's structured as a container for elements that can be
+extended when needed.
 
-While it can be used as a generic widget container, it's designed to be able to
-provide commonly used functionality as a short cut. These shortcuts include:
+=head1 METHODS
 
 =over 4
 
-=item *
+=item * C<new>
 
-Calling a controller with parameters
+Will be used during initialization of the layout. You should never have to
+instanciate an action bar yourself. Get the current request instances from
 
-=item *
+  $::request->layout->get('actionbar')
 
-Submitting a form with added parameters
+instead.
 
-=item *
+=item * C<add>
 
-Arrangement utility
+Add new elements to the bar. Can be instances of
+L<SL::Layout::ActionBar::Action> or scalar strings matching the sugar syntax
+which is described further down.
 
 =back
 
+=head1 SYNTACTIC SUGAR
 
-=head1 METHODS
+Instead of passing full objects to L</add>, you can instead pass the arguments
+to be used for instantiation to make the code easier to read. The short syntax
+looks like this:
+
+  type => [
+    localized_description,
+    param => value,
+    param => value,
+    ...
+  ]
+
+A string type, followed by the parameters needed for that type. Type may be one of:
 
 =over 4
 
-=item C<add_actions LIST>
+=item * C<action>
 
-Dispatches each each argument to C<add_action>
+=item * C<combobox>
 
-=item C<add_action>
+=item * C<link>
 
+=item * C<separator>
 
-=item C<add_separator>
+=back
 
-=item C<add_
+C<separator> will use no parameters, the other three will expect one arrayref.
 
-=head1 ACCESS FROM CODE
+Two additional pseudo parameters are supported for those:
 
-This is accessable through
+=over 4
 
-  $::request->layout->get('actionbar')
+=item * C<only_if>
+
+=item * C<not_if>
+
+=back
+
+These are meant to reduce enterprise operators (C<()x!!>) when conditionally adding lots
+of elements.
+
+The combobox element is in itself a container and will simply expect the same
+syntax in an arrayref.
 
-=head1 DOM MODEL
+For the full list of parameters supported by the elements, see L<SL::Layout::ActionBar::Action/RECOGNIZED PARAMETERS>.
 
-The entire block is rendered into a div with the class 'layout-actionbar'.
 
-=head1 ACTION WIDGETS
+=head1 GUIDELINES
 
-Each individual action must be an instance of C<SL::Layout::ActionBar::Action>.
+The current implementation follows these design guidelines:
+
+=over 4
+
+=item *
+
+Don't put too many elements into the action bar. Group into comboboxes if
+possible. Consider seven elements a reasonable limit.
+
+=item *
+
+If you've got an update button, put it first and bind the enter accesskey to
+it.
+
+=item *
+
+Put mutating actions (save, post, delete, check out, ship) before the separator
+and non mutating actions (export, search, history, workflow) after the
+separator. Combined actions (save and close) still mutate and go before the
+separator.
+
+=item *
+
+Avoid abusing the actionbar as a secondary menu. As a principle every action
+should act upon the current element or topic.
+
+=item *
+
+Hide elements with C<only_if> if they are known to be useless for the current
+topic, but disable when they would be useful in principle but are not
+applicable right now. For example C<delete> does not make sense in a creating
+form, but makes still sense because the element can be deleted later. This
+keeps the actionbar stable and reduces surprising elements that only appear in
+rare situations.
+
+=item *
+
+Always add a tooltip when disabling an action.
+
+=item *
+
+Try to always add a default action with accesskey enter. Since the actionbar
+lies outside of the main form, the usual submit on enter does not work out of
+the box.
+
+=back
+
+=head1 DOM MODEL AND IMPLEMENTATION DETAILS
+
+The entire block is rendered into a div with the class 'layout-actionbar'. Each
+action will render itself and will get added to the div. To keep the DOM small
+and reduce startup overhead, the presentation is pure CSS and only the sticky
+expansion of comboboxes is done with javascript.
+
+To keep startup times and HTML parsing fast the action data is simply written
+into the data elements of the actions and handlers are added in a ready hook.
 
 =head1 BUGS
 
 none yet. :)
 
+=head1 SEE ALSO
+
+L<SL::Layout::ActioBar::Base>,
+L<SL::Layout::ActioBar::Action>,
+L<SL::Layout::ActioBar::Submit>,
+L<SL::Layout::ActioBar::ComboBox>,
+L<SL::Layout::ActioBar::Separator>,
+L<SL::Layout::ActioBar::Link>,
+
 =head1 AUTHOR
 
 Sven Schoeling E<lt>s.schoeling@linet-services.deE<gt>