diff options
Diffstat (limited to 'lib/Reaction/UI/ViewPort/Field')
-rw-r--r-- | lib/Reaction/UI/ViewPort/Field/Boolean.pm | 32 | ||||
-rw-r--r-- | lib/Reaction/UI/ViewPort/Field/ChooseMany.pm | 139 | ||||
-rw-r--r-- | lib/Reaction/UI/ViewPort/Field/ChooseOne.pm | 138 | ||||
-rw-r--r-- | lib/Reaction/UI/ViewPort/Field/DateTime.pm | 89 | ||||
-rw-r--r-- | lib/Reaction/UI/ViewPort/Field/File.pm | 45 | ||||
-rw-r--r-- | lib/Reaction/UI/ViewPort/Field/HiddenArray.pm | 42 | ||||
-rw-r--r-- | lib/Reaction/UI/ViewPort/Field/Number.pm | 31 | ||||
-rw-r--r-- | lib/Reaction/UI/ViewPort/Field/Password.pm | 32 | ||||
-rw-r--r-- | lib/Reaction/UI/ViewPort/Field/String.pm | 34 | ||||
-rw-r--r-- | lib/Reaction/UI/ViewPort/Field/Text.pm | 32 | ||||
-rw-r--r-- | lib/Reaction/UI/ViewPort/Field/TimeRange.pm | 151 |
11 files changed, 765 insertions, 0 deletions
diff --git a/lib/Reaction/UI/ViewPort/Field/Boolean.pm b/lib/Reaction/UI/ViewPort/Field/Boolean.pm new file mode 100644 index 0000000..34f7aae --- /dev/null +++ b/lib/Reaction/UI/ViewPort/Field/Boolean.pm @@ -0,0 +1,32 @@ +package Reaction::UI::ViewPort::Field::Boolean; + +use Reaction::Class; + +class Boolean is 'Reaction::UI::ViewPort::Field', which { + + has '+value' => (isa => 'Bool'); + has '+layout' => (default => 'checkbox'); + +}; + +1; + +=head1 NAME + +Reaction::UI::ViewPort::Field::Boolean + +=head1 DESCRIPTION + +=head1 SEE ALSO + +=head2 L<Reaction::UI::ViewPort::Field> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/ViewPort/Field/ChooseMany.pm b/lib/Reaction/UI/ViewPort/Field/ChooseMany.pm new file mode 100644 index 0000000..0ea4ed0 --- /dev/null +++ b/lib/Reaction/UI/ViewPort/Field/ChooseMany.pm @@ -0,0 +1,139 @@ +package Reaction::UI::ViewPort::Field::ChooseMany; + +use Reaction::Class; + +class ChooseMany is 'Reaction::UI::ViewPort::Field::ChooseOne', which { + + has '+layout' => (default => 'dual_select_group'); + + has '+value' => (isa => 'ArrayRef'); + + has available_value_names => + (isa => 'ArrayRef', is => 'ro', lazy_build => 1); + + has value_names => (isa => 'ArrayRef', is => 'ro', lazy_build => 1); + + my $listify = sub { # quick utility function, $listify->($arg) + return (defined($_[0]) + ? (ref($_[0]) eq 'ARRAY' + ? $_[0] # \@arr => \@arr + : [$_[0]]) # $scalar => [$scalar] + : []); # undef => [] + }; + + around value => sub { + my $orig = shift; + my $self = shift; + if (@_) { + my $value = $listify->(shift); + if (defined $value) { + $_ = $self->str_to_ident($_) for @$value; + my $checked = $self->attribute->check_valid_value($self->action, $value); + # i.e. fail if any of the values fail + confess "Not a valid set of values" + if (@$checked < @$value || grep { !defined($_) } @$checked); + + $value = $checked; + } + $orig->($self, $value); + } else { + $orig->($self); + } + }; + + override build_value => sub { + return super() || []; + }; + + implements is_current_value => as { + my ($self, $check_value) = @_; + my @our_values = @{$self->value||[]}; + #$check_value = $check_value->id if ref($check_value); + #return grep { $_->id eq $check_value } @our_values; + $check_value = $self->obj_to_str($check_value) if ref($check_value); + return grep { $self->obj_to_str($_) eq $check_value } @our_values; + }; + + implements current_values => as { + my $self = shift; + my @all = grep { $self->is_current_value($_) } @{$self->valid_values}; + return [ @all ]; + }; + + implements available_values => as { + my $self = shift; + my @all = grep { !$self->is_current_value($_) } @{$self->valid_values}; + return [ @all ]; + }; + + implements build_available_value_names => as { + my $self = shift; + my @all = @{$self->available_values}; + my $meth = $self->value_map_method; + my @names = map { $_->$meth } @all; + return [ sort @names ]; + }; + + implements build_value_names => as { + my $self = shift; + my @all = @{$self->value||[]}; + my $meth = $self->value_map_method; + my @names = map { $_->$meth } @all; + return [ sort @names ]; + }; + + around handle_events => sub { + my $orig = shift; + my ($self, $events) = @_; + my $ev_value = $listify->($events->{value}); + if (delete $events->{add_all_values}) { + $events->{value} = $self->valid_values; + } + if (delete $events->{do_add_values} && exists $events->{add_values}) { + my $add = $listify->(delete $events->{add_values}); + $events->{value} = [ @{$ev_value}, @$add ]; + } + if (delete $events->{remove_all_values}) { + $events->{value} = []; + } + if (delete $events->{do_remove_values} && exists $events->{remove_values}) { + my $remove = $listify->(delete $events->{remove_values}); + my %r = map { ($_ => 1) } @$remove; + $events->{value} = [ grep { !$r{$_} } @{$ev_value} ]; + } + return $orig->(@_); + }; + +}; + +1; + +=head1 NAME + +Reaction::UI::ViewPort::Field::ChooseMany + +=head1 DESCRIPTION + +=head1 METHODS + +=head2 is_current_value + +=head2 current_values + +=head2 available_values + +=head2 available_value_names + +=head1 SEE ALSO + +=head2 L<Reaction::UI::ViewPort::Field> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/ViewPort/Field/ChooseOne.pm b/lib/Reaction/UI/ViewPort/Field/ChooseOne.pm new file mode 100644 index 0000000..ea0db1d --- /dev/null +++ b/lib/Reaction/UI/ViewPort/Field/ChooseOne.pm @@ -0,0 +1,138 @@ +package Reaction::UI::ViewPort::Field::ChooseOne; + +use Reaction::Class; +use URI; +use Scalar::Util 'blessed'; + +class ChooseOne is 'Reaction::UI::ViewPort::Field', which { + + has '+layout' => (default => 'select'); + + has valid_value_names => (isa => 'ArrayRef', is => 'ro', lazy_build => 1); + + has valid_values => (isa => 'ArrayRef', is => 'ro', lazy_build => 1); + + has name_to_value_map => (isa => 'HashRef', is => 'ro', lazy_build => 1); + + has value_to_name_map => (isa => 'HashRef', is => 'ro', lazy_build => 1); + + has value_map_method => ( + isa => 'Str', is => 'ro', required => 1, default => sub { 'display_name' }, + ); + + around value => sub { + my $orig = shift; + my $self = shift; + if (@_) { + my $value = shift; + if (defined $value) { + if (!ref $value) { + $value = $self->str_to_ident($value); + } + my $checked = $self->attribute->check_valid_value($self->action, $value); + confess "${value} is not a valid value" unless defined($checked); + $value = $checked; + } + $orig->($self, $value); + } else { + $orig->($self); + } + }; + + implements build_valid_values => as { + my $self = shift; + return [ $self->attribute->all_valid_values($self->action) ]; + }; + + implements build_valid_value_names => as { + my $self = shift; + my $all = $self->valid_values; + my $meth = $self->value_map_method; + my @names = map { $_->$meth } @$all; + return [ sort @names ]; + }; + + implements build_name_to_value_map => as { + my $self = shift; + my $all = $self->valid_values; + my $meth = $self->value_map_method; + my %map; + $map{$_->$meth} = $self->obj_to_str($_) for @$all; + return \%map; + }; + + implements build_value_to_name_map => as { + my $self = shift; + my $all = $self->valid_values; + my $meth = $self->value_map_method; + my %map; + $map{$self->obj_to_str($_)} = $_->$meth for @$all; + return \%map; + }; + + implements is_current_value => as { + my ($self, $check_value) = @_; + my $our_value = $self->value; + return unless ref($our_value); + $check_value = $self->obj_to_str($check_value) if ref($check_value); + return $self->obj_to_str($our_value) eq $check_value; + }; + + implements str_to_ident => as { + my ($self, $str) = @_; + my $u = URI->new('','http'); + $u->query($str); + return { $u->query_form }; + }; + + implements obj_to_str => as { + my ($self, $obj) = @_; + return $obj unless ref($obj); + confess "${obj} not an object" unless blessed($obj); + my $ident = $obj->ident_condition; + my $u = URI->new('', 'http'); + $u->query_form(%$ident); + return $u->query; + }; + +}; + +1; + +=head1 NAME + +Reaction::UI::ViewPort::Field::ChooseOne + +=head1 DESCRIPTION + +=head1 METHODS + +=head2 is_current_value + +=head2 value + +=head2 valid_values + +=head2 valid_value_names + +=head2 value_to_name_map + +=head2 name_to_value_map + +=head2 str_to_ident + +=head2 obj_to_str + +=head1 SEE ALSO + +=head2 L<Reaction::UI::ViewPort::Field> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/ViewPort/Field/DateTime.pm b/lib/Reaction/UI/ViewPort/Field/DateTime.pm new file mode 100644 index 0000000..2b8509f --- /dev/null +++ b/lib/Reaction/UI/ViewPort/Field/DateTime.pm @@ -0,0 +1,89 @@ +package Reaction::UI::ViewPort::Field::DateTime; + +use Reaction::Class; +use Reaction::Types::DateTime; +use Time::ParseDate (); + +class DateTime is 'Reaction::UI::ViewPort::Field', which { + + has '+value' => (isa => 'DateTime'); + + has '+layout' => (default => 'dt_textfield'); + + has value_string => ( + isa => 'Str', is => 'rw', lazy_build => 1, + trigger_adopt('value_string') + ); + + has value_string_default_format => ( + isa => 'Str', is => 'rw', required => 1, default => sub { "%F %H:%M:%S" } + ); + + implements build_value_string => as { + my $self = shift; + + # XXX + #<mst> aha, I know why the fucker's lazy + #<mst> it's because if value's calculated + #<mst> it needs to be possible to clear it + #<mst> eval { $self->value } ... is probably the best solution atm + my $value = eval { $self->value }; + return '' unless $self->has_value; + my $format = $self->value_string_default_format; + return $value->strftime($format) if $value; + return ''; + }; + + implements adopt_value_string => as { + my ($self) = @_; + my $value = $self->value_string; + my ($epoch) = Time::ParseDate::parsedate($value, UK => 1); + if (defined $epoch) { + my $dt = 'DateTime'->from_epoch( epoch => $epoch ); + $self->value($dt); + } else { + $self->message("Could not parse date or time"); + $self->clear_value; + $self->needs_sync(1); + } + }; + + override accept_events => sub { + ('value_string', super()); + }; + +}; + +1; + +=head1 NAME + +Reaction::UI::ViewPort::Field::DateTime + +=head1 DESCRIPTION + +=head1 METHODS + +=head2 value_string + +Accessor for the string representation of the DateTime object. + +=head2 value_string_default_format + +By default it is set to "%F %H:%M:%S". + +=head1 SEE ALSO + +=head2 L<DateTime> + +=head2 L<Reaction::UI::ViewPort::Field> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/ViewPort/Field/File.pm b/lib/Reaction/UI/ViewPort/Field/File.pm new file mode 100644 index 0000000..557826d --- /dev/null +++ b/lib/Reaction/UI/ViewPort/Field/File.pm @@ -0,0 +1,45 @@ +package Reaction::UI::ViewPort::Field::File; + +use Reaction::Class; +use Reaction::Types::File; + +class File is 'Reaction::UI::ViewPort::Field', which { + + has '+value' => (isa => 'File', required => 0); + + has '+layout' => (default => 'file'); + + override apply_our_events => sub { + my ($self, $ctx, $events) = @_; + my $value_key = join(':', $self->location, 'value'); + if (my $upload = $ctx->req->upload($value_key)) { + local $events->{$value_key} = $upload; + return super(); + } else { + return super(); + } + }; + +}; + +1; + +=head1 NAME + +Reaction::UI::ViewPort::Field::File + +=head1 DESCRIPTION + +=head1 SEE ALSO + +=head2 L<Reaction::UI::ViewPort::Field> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/ViewPort/Field/HiddenArray.pm b/lib/Reaction/UI/ViewPort/Field/HiddenArray.pm new file mode 100644 index 0000000..7f8cc73 --- /dev/null +++ b/lib/Reaction/UI/ViewPort/Field/HiddenArray.pm @@ -0,0 +1,42 @@ +package Reaction::UI::ViewPort::Field::HiddenArray; + +use Reaction::Class; + +class HiddenArray is 'Reaction::UI::ViewPort::Field', which { + + has '+value' => (isa => 'ArrayRef'); + + around value => sub { + my $orig = shift; + my $self = shift; + if (@_) { + $orig->($self, (ref $_[0] eq 'ARRAY' ? $_[0] : [ $_[0] ])); + $self->sync_to_action; + } else { + $orig->($self); + } + }; + +}; + +1; + +=head1 NAME + +Reaction::UI::ViewPort::Field::HiddenArray + +=head1 DESCRIPTION + +=head1 SEE ALSO + +=head2 L<Reaction::UI::ViewPort::Field> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/ViewPort/Field/Number.pm b/lib/Reaction/UI/ViewPort/Field/Number.pm new file mode 100644 index 0000000..e4e925f --- /dev/null +++ b/lib/Reaction/UI/ViewPort/Field/Number.pm @@ -0,0 +1,31 @@ +package Reaction::UI::ViewPort::Field::Number; + +use Reaction::Class; + +class Number is 'Reaction::UI::ViewPort::Field', which { + + has '+layout' => (default => 'textfield'); + +}; + +1; + +=head1 NAME + +Reaction::UI::ViewPort::Field::Number + +=head1 DESCRIPTION + +=head1 SEE ALSO + +=head2 L<Reaction::UI::ViewPort::Field> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/ViewPort/Field/Password.pm b/lib/Reaction/UI/ViewPort/Field/Password.pm new file mode 100644 index 0000000..d70ed62 --- /dev/null +++ b/lib/Reaction/UI/ViewPort/Field/Password.pm @@ -0,0 +1,32 @@ +package Reaction::UI::ViewPort::Field::Password; + +use Reaction::Class; + +class Password is 'Reaction::UI::ViewPort::Field::String', which { + + has '+value' => (isa => 'SimpleStr'); + has '+layout' => (default => 'password'); + +}; + +1; + +=head1 NAME + +Reaction::UI::ViewPort::Field::Password + +=head1 DESCRIPTION + +=head1 SEE ALSO + +=head2 L<Reaction::UI::ViewPort::Field> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/ViewPort/Field/String.pm b/lib/Reaction/UI/ViewPort/Field/String.pm new file mode 100644 index 0000000..4be6bdc --- /dev/null +++ b/lib/Reaction/UI/ViewPort/Field/String.pm @@ -0,0 +1,34 @@ +package Reaction::UI::ViewPort::Field::String; + +use Reaction::Class; + +class String is 'Reaction::UI::ViewPort::Field', which { + + has '+value' => (isa => 'Str'); # accept over 255 chars in case, upstream + # constraint from model should catch it + + has '+layout' => (default => 'textfield'); + +}; + +1; + +=head1 NAME + +Reaction::UI::ViewPort::Field::String + +=head1 DESCRIPTION + +=head1 SEE ALSO + +=head2 L<Reaction::UI::ViewPort::Field> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/ViewPort/Field/Text.pm b/lib/Reaction/UI/ViewPort/Field/Text.pm new file mode 100644 index 0000000..d4e89f8 --- /dev/null +++ b/lib/Reaction/UI/ViewPort/Field/Text.pm @@ -0,0 +1,32 @@ +package Reaction::UI::ViewPort::Field::Text; + +use Reaction::Class; + +class Text is 'Reaction::UI::ViewPort::Field', which { + + has '+value' => (isa => 'Str'); + has '+layout' => (default => 'textarea'); + +}; + +1; + +=head1 NAME + +Reaction::UI::ViewPort::Field::Text + +=head1 DESCRIPTION + +=head1 SEE ALSO + +=head2 L<Reaction::UI::ViewPort::Field> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut diff --git a/lib/Reaction/UI/ViewPort/Field/TimeRange.pm b/lib/Reaction/UI/ViewPort/Field/TimeRange.pm new file mode 100644 index 0000000..3619b5e --- /dev/null +++ b/lib/Reaction/UI/ViewPort/Field/TimeRange.pm @@ -0,0 +1,151 @@ +package Reaction::UI::ViewPort::Field::TimeRange; + +use Reaction::Class; +use Reaction::Types::DateTime; +use DateTime; +use DateTime::SpanSet; +use Time::ParseDate (); + +class TimeRange is 'Reaction::UI::ViewPort::Field', which { + + has '+value' => (isa => 'DateTime::SpanSet'); + + has '+layout' => (default => 'timerange'); + + has value_string => + (isa => 'Str', is => 'rw', lazy_fail => 1, trigger_adopt('value_string')); + + has delete_label => ( + isa => 'Str', is => 'rw', required => 1, default => sub { 'Delete' }, + ); + + has parent => ( + isa => 'Reaction::UI::ViewPort::TimeRangeCollection', + is => 'ro', + required => 1, + is_weak_ref => 1 + ); + + implements build_value_string => as { + my $self = shift; + #return '' unless $self->has_value; + #return $self->value_string; + }; + + implements value_array => as { + my $self = shift; + return split(',', $self->value_string); + }; + + implements adopt_value_string => as { + my ($self) = @_; + my @values = $self->value_array; + for my $idx (0 .. 3) { # last value is repeat + if (length $values[$idx]) { + my ($epoch) = Time::ParseDate::parsedate($values[$idx], UK => 1); + $values[$idx] = DateTime->from_epoch( epoch => $epoch ); + } + } + $self->value($self->range_to_spanset(@values)); + }; + + implements range_to_spanset => as { + my ($self, $time_from, $time_to, $repeat_from, $repeat_to, $pattern) = @_; + my $spanset = DateTime::SpanSet->empty_set; + if (!$pattern || $pattern eq 'none') { + my $span = DateTime::Span->from_datetimes( + start => $time_from, end => $time_to + ); + $spanset = $spanset->union( $span ); + } else { + my $duration = $time_to - $time_from; + my %args = ( days => $time_from->day + 2, + hours => $time_from->hour, + minutes => $time_from->minute, + seconds => $time_from->second ); + + delete $args{'days'} if ($pattern eq 'daily'); + delete @args{qw/hours days/} if ($pattern eq 'hourly'); + $args{'days'} = $time_from->day if ($pattern eq 'monthly'); + my $start_set = DateTime::Event::Recurrence->$pattern( %args ); + my $iter = $start_set->iterator( start => $repeat_from, end => $repeat_to ); + while ( my $dt = $iter->next ) { + my $endtime = $dt + $duration; + my $new_span = DateTime::Span->from_datetimes( + start => $dt, + end => $endtime + ); + $spanset = $spanset->union( $new_span ); + } + } + return $spanset; + }; + + implements delete => as { + my ($self) = @_; + $self->parent->remove_range_vp($self); + }; + + override accept_events => sub { ('value_string', 'delete', super()) }; + +}; + +1; + +=head1 NAME + +Reaction::UI::ViewPort::Field::TimeRange + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=head1 METHODS + +=head2 value + + Accessor for a L<DateTime::SpanSet> object. + +=head2 value_string + + Returns: Encoded range string representing the value. + +=head2 value_array + + Returns: Arrayref of the elements of C<value_string>. + +=head2 parent + + L<Reaction::UI::ViewPort::TimeRangeCollection> object. + +=head2 range_to_spanset + + Arguments: $self, $time_from, $time_to, $repeat_from, $repeat_to, $pattern + where $time_from, $time_to, $repeat_from, $repeat_to are L<DateTime> + objects, and $pattern is a L<DateTime::Event::Recurrence> method name + + Returns: $spanset + +=head2 delete + + Removes TimeRange from C<parent> collection. + +=head2 delete_label + + Label for the delete option. Default: 'Delete'. + +=head1 SEE ALSO + +=head2 L<Reaction::UI::ViewPort::Field> + +=head2 L<Reaction::UI::ViewPort::TimeRangeCollection> + +=head1 AUTHORS + +See L<Reaction::Class> for authors. + +=head1 LICENSE + +See L<Reaction::Class> for the license. + +=cut |