Merge branch 'b-3.6.1' of ../kivitendo-erp_20220811
[kivitendo-erp.git] / SL / Presenter / MaterialComponents.pm
diff --git a/SL/Presenter/MaterialComponents.pm b/SL/Presenter/MaterialComponents.pm
new file mode 100644 (file)
index 0000000..30123b7
--- /dev/null
@@ -0,0 +1,354 @@
+package SL::Presenter::MaterialComponents;
+
+use strict;
+
+use SL::HTML::Restrict;
+use SL::MoreCommon qw(listify);
+use SL::Presenter::EscapedText qw(escape);
+use SL::Presenter::Tag qw(html_tag);
+use Scalar::Util qw(blessed);
+use List::UtilsBy qw(partition_by);
+
+use Exporter qw(import);
+our @EXPORT_OK = qw(
+  button_tag
+  input_tag
+  date_tag
+  submit_tag
+  icon
+  select_tag
+  checkbox_tag
+);
+our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
+
+use constant BUTTON          => 'btn';
+use constant BUTTON_FLAT     => 'btn-flat';
+use constant BUTTON_FLOATING => 'btn-floating';
+use constant BUTTON_LARGE    => 'btn-large';
+use constant BUTTON_SMALL    => 'btn-small';
+use constant DISABLED        => 'disabled';
+use constant LEFT            => 'left';
+use constant MATERIAL_ICONS  => 'material-icons';
+use constant RIGHT           => 'right';
+use constant LARGE           => 'large';
+use constant MEDIUM          => 'medium';
+use constant SMALL           => 'small';
+use constant TINY            => 'tiny';
+use constant INPUT_FIELD     => 'input-field';
+use constant DATEPICKER      => 'datepicker';
+
+use constant WAVES_EFFECT    => 'waves-effect';
+use constant WAVES_LIGHT     => 'waves-light';
+
+
+my %optional_classes = (
+  button => {
+    disabled => DISABLED,
+    flat     => BUTTON_FLAT,
+    floating => BUTTON_FLOATING,
+    large    => BUTTON_LARGE,
+    small    => BUTTON_SMALL,
+  },
+  icon => {
+    left   => LEFT,
+    right  => RIGHT,
+    large  => LARGE,
+    medium => MEDIUM,
+    small  => SMALL,
+    tiny   => TINY,
+  },
+  size => {
+    map { $_ => $_ }
+      qw(col row),
+      (map { "s$_" } 1..12),
+      (map { "m$_" } 1..12),
+      (map { "l$_" } 1..12),
+  },
+);
+
+use Carp;
+
+sub _confirm_js {
+  'if (!confirm("'. _J($_[0]) .'")) return false;'
+}
+
+sub _confirm_to_onclick {
+  my ($attributes, $onclick) = @_;
+
+  if ($attributes->{confirm}) {
+    $$onclick //= '';
+    $$onclick = _confirm_js(delete($attributes->{confirm})) . $attributes->{onlick};
+  }
+}
+
+# used to extract material properties that need to be translated to classes
+# supports prefixing for delegation
+# returns a list of classes, mutates the attributes
+sub _extract_attribute_classes {
+  my ($attributes, $type, $prefix) = @_;
+
+  my @classes;
+  my $attr;
+  for my $key (keys %$attributes) {
+    if ($prefix) {
+      next unless $key =~ /^${prefix}_(.*)/;
+      $attr = $1;
+    } else {
+      $attr = $key;
+    }
+
+    if ($optional_classes{$type}{$attr}) {
+      $attributes->{$key} = undef;
+      push @classes, $optional_classes{$type}{$attr};
+    }
+  }
+
+  # delete all undefined values
+  my @delete_keys = grep { !defined $attributes->{$_} } keys %$attributes;
+  delete $attributes->{$_} for @delete_keys;
+
+  @classes;
+}
+
+# used to extract material classes that are passed directly as classes
+sub _extract_classes {
+  my ($attributes, $type) = @_;
+
+  my @classes = map { split / / } listify($attributes->{class});
+  my %classes = partition_by { !!$optional_classes{$type}{$_} } @classes;
+
+  $attributes->{class} = $classes{''};
+  $classes{1};
+}
+
+sub _set_id_attribute {
+  my ($attributes, $name, $unique) = @_;
+
+  if (!delete($attributes->{no_id}) && !$attributes->{id}) {
+    $attributes->{id}  = name_to_id($name);
+    $attributes->{id} .= '_' . $attributes->{value} if $unique;
+  }
+
+  %{ $attributes };
+}
+
+{ # This will give you an id for identifying html tags and such.
+  # It's guaranteed to be unique unless you exceed 10 mio calls per request.
+  # Do not use these id's to store information across requests.
+my $_id_sequence = int rand 1e7;
+sub _id {
+  return ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
+}
+}
+
+sub name_to_id {
+  my ($name) = @_;
+
+  if (!$name) {
+    return "id_" . _id();
+  }
+
+  $name =~ s/\[\+?\]/ _id() /ge; # give constructs with [] or [+] unique ids
+  $name =~ s/[^\w_]/_/g;
+  $name =~ s/_+/_/g;
+
+  return $name;
+}
+
+sub button_tag {
+  my ($onclick, $value, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
+  _confirm_to_onclick(\%attributes, \$onclick);
+
+  my @button_classes = _extract_attribute_classes(\%attributes, "button");
+  my @icon_classes   = _extract_attribute_classes(\%attributes, "icon", "icon");
+
+  $attributes{class} = [
+    grep { $_ } $attributes{class}, WAVES_EFFECT, WAVES_LIGHT, BUTTON, @button_classes
+  ];
+
+  if ($attributes{icon}) {
+    $value = icon(delete $attributes{icon}, class => \@icon_classes)
+           . $value;
+  }
+
+  html_tag('a', $value, %attributes, onclick => $onclick);
+}
+
+sub submit_tag {
+  my ($name, $value, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
+  _confirm_to_onclick(\%attributes, \($attributes{onclick} //= ''));
+
+  my @button_classes = _extract_attribute_classes(\%attributes, "button");
+  my @icon_classes   = _extract_attribute_classes(\%attributes, "icon", "icon");
+
+  $attributes{class} = [
+    grep { $_ } $attributes{class}, WAVES_EFFECT, WAVES_LIGHT, BUTTON, @button_classes
+  ];
+
+  if ($attributes{icon}) {
+    $value = icon(delete $attributes{icon}, class => \@icon_classes)
+           . $value;
+  }
+
+  html_tag('button', $value, type => 'submit',  %attributes);
+}
+
+
+sub icon {
+  my ($name, %attributes) = @_;
+
+  my @icon_classes = _extract_attribute_classes(\%attributes, "icon");
+
+  html_tag('i', $name, class => [ grep { $_ } MATERIAL_ICONS, @icon_classes, delete $attributes{class} ], %attributes);
+}
+
+
+sub input_tag {
+  my ($name, $value, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $attributes{name});
+
+  my $class = delete $attributes{class};
+  my $icon  = $attributes{icon}
+    ? icon(delete $attributes{icon}, class => 'prefix')
+    : '';
+
+  my $label = $attributes{label}
+    ? html_tag('label', delete $attributes{label}, for => $attributes{id})
+    : '';
+
+  $attributes{type} //= 'text';
+
+  html_tag('div',
+    $icon .
+    html_tag('input', undef, value => $value, %attributes, name => $name) .
+    $label,
+    class => [ grep $_, $class, INPUT_FIELD ],
+  );
+}
+
+sub date_tag {
+  my ($name, $value, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $name);
+
+  my $icon  = $attributes{icon}
+    ? icon(delete $attributes{icon}, class => 'prefix')
+    : '';
+
+  my $label = $attributes{label}
+    ? html_tag('label', delete $attributes{label}, for => $attributes{id})
+    : '';
+
+  $attributes{type} = 'text'; # required for materialize
+
+  my @onchange = $attributes{onchange} ? (onChange => delete $attributes{onchange}) : ();
+  my @classes  = (delete $attributes{class});
+
+  $::request->layout->add_javascripts('kivi.Validator.js');
+  $::request->presenter->need_reinit_widgets($attributes{id});
+
+  $attributes{'data-validate'} = join(' ', "date", grep { $_ } (delete $attributes{'data-validate'}));
+
+  html_tag('div',
+    $icon .
+    html_tag('input',
+      blessed($value) ? $value->to_lxoffice : $value,
+      size   => 11, type => 'text', name => $name,
+      %attributes,
+      class => DATEPICKER, @onchange,
+    ) .
+    $label,
+    class => [ grep $_, @classes, INPUT_FIELD ],
+  );
+}
+
+sub select_tag {
+  my ($name, $collection, %attributes) = @_;
+
+
+  _set_id_attribute(\%attributes, $name);
+  my @size_classes   = _extract_classes(\%attributes, "size");
+
+
+  my $icon  = $attributes{icon}
+    ? icon(delete $attributes{icon}, class => 'prefix')
+    : '';
+
+  my $label = $attributes{label}
+    ? html_tag('label', delete $attributes{label}, for => $attributes{id})
+    : '';
+
+  my $select_html = SL::Presenter::Tag::select_tag($name, $collection, %attributes);
+
+  html_tag('div',
+    $icon . $select_html . $label,
+    class => [ INPUT_FIELD, @size_classes ],
+  );
+}
+
+sub checkbox_tag {
+  my ($name, %attributes) = @_;
+
+  _set_id_attribute(\%attributes, $name);
+
+  my $label = $attributes{label}
+    ? html_tag('span', delete $attributes{label})
+    : '';
+
+  my $checkbox_html = SL::Presenter::Tag::checkbox_tag($name, %attributes);
+
+  html_tag('label',
+    $checkbox_html . $label,
+  );
+}
+
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::Presenter::MaterialComponents - MaterialCSS Component wrapper
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+This is a collection of components in the style of L<SL::Presenter::Tag>
+intended for materialzecss. They should be useable similarly to their original
+versions but be well-behaved for materialize.
+
+They will also recognize some materialize conventions:
+
+=over 4
+
+=item icon>
+
+Most elements can be decorated with an icon by supplying the C<icon> with the name.
+
+=item grid classes
+
+Grid classes like C<s12> or C<m6> can be given as keys with any truish value or
+directly as classes.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Sven Schöling E<lt>s.schoeling@googlemail.comE<gt>
+
+=cut