1 package SL::Presenter::MaterialComponents;
5 use SL::HTML::Restrict;
6 use SL::MoreCommon qw(listify);
7 use SL::Presenter::EscapedText qw(escape);
8 use SL::Presenter::Tag qw(html_tag);
9 use Scalar::Util qw(blessed);
10 use List::UtilsBy qw(partition_by);
12 use Exporter qw(import);
23 our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
25 use constant BUTTON => 'btn';
26 use constant BUTTON_FLAT => 'btn-flat';
27 use constant BUTTON_FLOATING => 'btn-floating';
28 use constant BUTTON_LARGE => 'btn-large';
29 use constant BUTTON_SMALL => 'btn-small';
30 use constant DISABLED => 'disabled';
31 use constant LEFT => 'left';
32 use constant MATERIAL_ICONS => 'material-icons';
33 use constant RIGHT => 'right';
34 use constant LARGE => 'large';
35 use constant MEDIUM => 'medium';
36 use constant SMALL => 'small';
37 use constant TINY => 'tiny';
38 use constant INPUT_FIELD => 'input-field';
39 use constant DATEPICKER => 'datepicker';
41 use constant WAVES_EFFECT => 'waves-effect';
42 use constant WAVES_LIGHT => 'waves-light';
45 my %optional_classes = (
49 floating => BUTTON_FLOATING,
50 large => BUTTON_LARGE,
51 small => BUTTON_SMALL,
64 (map { "s$_" } 1..12),
65 (map { "m$_" } 1..12),
66 (map { "l$_" } 1..12),
73 'if (!confirm("'. _J($_[0]) .'")) return false;'
76 sub _confirm_to_onclick {
77 my ($attributes, $onclick) = @_;
79 if ($attributes->{confirm}) {
81 $$onclick = _confirm_js(delete($attributes->{confirm})) . $attributes->{onlick};
85 # used to extract material properties that need to be translated to classes
86 # supports prefixing for delegation
87 # returns a list of classes, mutates the attributes
88 sub _extract_attribute_classes {
89 my ($attributes, $type, $prefix) = @_;
93 for my $key (keys %$attributes) {
95 next unless $key =~ /^${prefix}_(.*)/;
101 if ($optional_classes{$type}{$attr}) {
102 $attributes->{$key} = undef;
103 push @classes, $optional_classes{$type}{$attr};
107 # delete all undefined values
108 my @delete_keys = grep { !defined $attributes->{$_} } keys %$attributes;
109 delete $attributes->{$_} for @delete_keys;
114 # used to extract material classes that are passed directly as classes
115 sub _extract_classes {
116 my ($attributes, $type) = @_;
118 my @classes = map { split / / } listify($attributes->{class});
119 my %classes = partition_by { !!$optional_classes{$type}{$_} } @classes;
121 $attributes->{class} = $classes{''};
125 sub _set_id_attribute {
126 my ($attributes, $name, $unique) = @_;
128 if (!delete($attributes->{no_id}) && !$attributes->{id}) {
129 $attributes->{id} = name_to_id($name);
130 $attributes->{id} .= '_' . $attributes->{value} if $unique;
136 { # This will give you an id for identifying html tags and such.
137 # It's guaranteed to be unique unless you exceed 10 mio calls per request.
138 # Do not use these id's to store information across requests.
139 my $_id_sequence = int rand 1e7;
141 return ( $_id_sequence = ($_id_sequence + 1) % 1e7 );
149 return "id_" . _id();
152 $name =~ s/\[\+?\]/ _id() /ge; # give constructs with [] or [+] unique ids
153 $name =~ s/[^\w_]/_/g;
160 my ($onclick, $value, %attributes) = @_;
162 _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
163 _confirm_to_onclick(\%attributes, \$onclick);
165 my @button_classes = _extract_attribute_classes(\%attributes, "button");
166 my @icon_classes = _extract_attribute_classes(\%attributes, "icon", "icon");
168 $attributes{class} = [
169 grep { $_ } $attributes{class}, WAVES_EFFECT, WAVES_LIGHT, BUTTON, @button_classes
172 if ($attributes{icon}) {
173 $value = icon(delete $attributes{icon}, class => \@icon_classes)
177 html_tag('a', $value, %attributes, onclick => $onclick);
181 my ($name, $value, %attributes) = @_;
183 _set_id_attribute(\%attributes, $attributes{name}) if $attributes{name};
184 _confirm_to_onclick(\%attributes, \($attributes{onclick} //= ''));
186 my @button_classes = _extract_attribute_classes(\%attributes, "button");
187 my @icon_classes = _extract_attribute_classes(\%attributes, "icon", "icon");
189 $attributes{class} = [
190 grep { $_ } $attributes{class}, WAVES_EFFECT, WAVES_LIGHT, BUTTON, @button_classes
193 if ($attributes{icon}) {
194 $value = icon(delete $attributes{icon}, class => \@icon_classes)
198 html_tag('button', $value, type => 'submit', %attributes);
203 my ($name, %attributes) = @_;
205 my @icon_classes = _extract_attribute_classes(\%attributes, "icon");
207 html_tag('i', $name, class => [ grep { $_ } MATERIAL_ICONS, @icon_classes, delete $attributes{class} ], %attributes);
212 my ($name, $value, %attributes) = @_;
214 _set_id_attribute(\%attributes, $attributes{name});
216 my $class = delete $attributes{class};
217 my $icon = $attributes{icon}
218 ? icon(delete $attributes{icon}, class => 'prefix')
221 my $label = $attributes{label}
222 ? html_tag('label', delete $attributes{label}, for => $attributes{id})
225 $attributes{type} //= 'text';
229 html_tag('input', undef, value => $value, %attributes, name => $name) .
231 class => [ grep $_, $class, INPUT_FIELD ],
236 my ($name, $value, %attributes) = @_;
238 _set_id_attribute(\%attributes, $attributes{name});
240 my $class = delete $attributes{class};
241 my $icon = $attributes{icon}
242 ? icon(delete $attributes{icon}, class => 'prefix')
245 my $label = $attributes{label}
246 ? html_tag('label', delete $attributes{label}, for => $attributes{id})
251 html_tag('textarea', $value, class => 'materialize-textarea', %attributes, name => $name) .
253 class => [ grep $_, $class, INPUT_FIELD ],
258 my ($name, $value, %attributes) = @_;
260 _set_id_attribute(\%attributes, $name);
262 my $icon = $attributes{icon}
263 ? icon(delete $attributes{icon}, class => 'prefix')
266 my $label = $attributes{label}
267 ? html_tag('label', delete $attributes{label}, for => $attributes{id})
270 $attributes{type} = 'text'; # required for materialize
272 my @onchange = $attributes{onchange} ? (onChange => delete $attributes{onchange}) : ();
273 my @classes = (delete $attributes{class});
275 $::request->layout->add_javascripts('kivi.Validator.js');
276 $::request->presenter->need_reinit_widgets($attributes{id});
278 $attributes{'data-validate'} = join(' ', "date", grep { $_ } (delete $attributes{'data-validate'}));
283 blessed($value) ? $value->to_lxoffice : $value,
284 size => 11, type => 'text', name => $name,
286 class => DATEPICKER, @onchange,
289 class => [ grep $_, @classes, INPUT_FIELD ],
294 my ($name, $collection, %attributes) = @_;
297 _set_id_attribute(\%attributes, $name);
298 my @size_classes = _extract_classes(\%attributes, "size");
301 my $icon = $attributes{icon}
302 ? icon(delete $attributes{icon}, class => 'prefix')
305 my $label = $attributes{label}
306 ? html_tag('label', delete $attributes{label}, for => $attributes{id}, class => 'active')
309 my $select_html = SL::Presenter::Tag::select_tag($name, $collection, %attributes,
310 class => 'browser-default');
313 $icon . $select_html . $label,
314 class => [ INPUT_FIELD, @size_classes ],
319 my ($name, %attributes) = @_;
321 _set_id_attribute(\%attributes, $name);
323 my $label = $attributes{label}
324 ? html_tag('span', delete $attributes{label})
327 my $checkbox_html = SL::Presenter::Tag::checkbox_tag($name, %attributes);
330 $checkbox_html . $label,
344 SL::Presenter::MaterialComponents - MaterialCSS Component wrapper
351 This is a collection of components in the style of L<SL::Presenter::Tag>
352 intended for materialzecss. They should be useable similarly to their original
353 versions but be well-behaved for materialize.
355 They will also recognize some materialize conventions:
361 Most elements can be decorated with an icon by supplying the C<icon> with the name.
365 Grid classes like C<s12> or C<m6> can be given as keys with any truish value or
372 There is a bug in MaterializeCSS, when using a Materialize select element on an iphone,
373 the wrong element is selected. This is currently worked around in the presenter by using
374 the 'browser-default' class on the select element as well as the 'active' class on the
375 label (to prevent the label from overlapping the select element).
376 This should be fixed, upstream, in MaterializeCSS. However it seems that the project is
377 not maintained anymore (according to github issues[^1]). There is a community fork[^2],
378 which it's still maintained and where the problem seems to be fixed already. It is currently
379 in alpha V. 2.0.3-alpha. Maybe it would be good to consider switching to that fork at some
382 [1]: e.g. https://github.com/Dogfalo/materialize/issues/6688
383 [2]: https://github.com/materializecss/materialize
387 Sven Schöling E<lt>s.schoeling@googlemail.comE<gt>