refactpored most of the action stuff into roles. should fields be a role too?
[catagits/Reaction.git] / lib / Reaction / UI / ViewPort.pm
1 package Reaction::UI::ViewPort;
2
3 use Reaction::Class;
4 use Scalar::Util qw/blessed/;
5
6 use namespace::clean -except => [ qw(meta) ];
7
8
9 sub DEBUG_EVENTS () { $ENV{REACTION_UI_VIEWPORT_DEBUG_EVENTS} }
10
11 has location => (isa => 'Str', is => 'rw', required => 1);
12 has layout => (isa => 'Str', is => 'rw', lazy_build => 1);
13 has layout_args => (isa => 'HashRef', is => 'ro', default => sub { {} });
14 has outer => (isa => 'Reaction::UI::ViewPort', is => 'rw', weak_ref => 1);
15 has inner => (isa => 'Reaction::UI::ViewPort', is => 'rw');
16 has focus_stack => (
17   isa => 'Reaction::UI::FocusStack', is => 'rw', weak_ref => 1
18 );
19 has _tangent_stacks => (
20   isa => 'HashRef', is => 'ro', default => sub { {} }
21 );
22 has ctx => (isa => 'Catalyst', is => 'ro'); #, required => 1);
23
24 sub _build_layout {
25   '';
26 }
27
28 sub create_tangent {
29   my ($self, $name) = @_;
30   my $t_map = $self->_tangent_stacks;
31   if (exists $t_map->{$name}) {
32     confess "Can't create tangent with already existing name ${name}";
33   }
34   my $loc = join('.', $self->location, $name);
35   my $tangent = Reaction::UI::FocusStack->new(loc_prefix => $loc);
36   $t_map->{$name} = $tangent;
37   return $tangent;
38 }
39
40 sub focus_tangent {
41   my ($self, $name) = @_;
42   if (my $tangent = $self->_tangent_stacks->{$name}) {
43     return $tangent;
44   } else {
45     return;
46   }
47 }
48
49 sub focus_tangents {
50   return keys %{shift->_tangent_stacks};
51 }
52
53 sub child_event_sinks {
54   my $self = shift;
55   return values %{$self->_tangent_stacks};
56 }
57
58 sub apply_events {
59   my ($self, $events) = @_;
60   return unless keys %$events;
61   $self->apply_child_events($events);
62   $self->apply_our_events($events);
63 }
64
65 sub apply_child_events {
66   my ($self, $events) = @_;
67   return unless keys %$events;
68   foreach my $child ($self->child_event_sinks) {
69     confess blessed($child) ."($child) is not a valid object"
70       unless blessed($child) && $child->can('apply_events');
71     $child->apply_events($events);
72   }
73 }
74
75 sub apply_our_events {
76   my ($self, $events) = @_;
77   my @keys = keys %$events;
78   return unless @keys;
79   my $loc = $self->location;
80   my %our_events;
81   foreach my $key (keys %$events) {
82     if ($key =~ m/^${loc}:(.*)$/) {
83       $our_events{$1} = $events->{$key};
84     }
85   }
86   if (keys %our_events) {
87     #warn "$self: events ".join(', ', %our_events)."\n";
88     $self->handle_events(\%our_events);
89   }
90 }
91
92 sub handle_events {
93   my ($self, $events) = @_;
94   my $exists = exists $events->{exists};
95   if ($exists) {
96     my %force = $self->force_events;
97     my @need = grep { !exists $events->{$_} } keys %force;
98     @{$events}{@need} = @force{@need};
99   }
100   foreach my $event ($self->accept_events) {
101     if (exists $events->{$event}) {
102       if (DEBUG_EVENTS) {
103         my $name = join(' at ', $self, $self->location);
104         print STDERR
105           "Applying Event: $event on $name with value: "
106           .(defined $events->{$event} ? $events->{$event} : '<undef>');
107       }
108       $self->$event($events->{$event});
109     }
110   }
111 }
112
113 sub accept_events { () }
114
115 sub force_events { () }
116
117 sub event_id_for {
118   my ($self, $name) = @_;
119   return join(':', $self->location, $name);
120 }
121
122 sub sort_by_spec {
123   my ($self, $spec, $items) = @_;
124   return $items if not defined $spec;
125
126   my @order;
127   if (ref $spec eq 'ARRAY') {
128     @order = @$spec;
129   }
130   elsif (not ref $spec) {
131     return $items unless length $spec;
132     @order = split /\s+/, $spec;
133   }
134
135   my %order_map = map {$_ => 0} @$items;
136   for my $order_num (0..$#order) {
137     $order_map{ $order[$order_num] } = ($#order - $order_num) + 1;
138   }
139
140   return [sort {$order_map{$b} <=> $order_map{$a}} @$items];
141 }
142
143 __PACKAGE__->meta->make_immutable;
144
145
146 1;
147
148
149 =head1 NAME
150
151 Reaction::UI::ViewPort - Page layout building block
152
153 =head1 SYNOPSIS
154
155   # Create a new ViewPort:
156   # $stack isa Reaction::UI::FocusStack object
157   my $vp = $stack->push_viewport('Reaction::UI::ViewPort', layout => 'xthml');
158
159   # Fetch ViewPort higher up the stack (further out)
160   my $outer = $vp->outer();
161
162   # Fetch ViewPort lower down (further in)
163   my $inner = $vp->inner();
164
165   # Create a named tangent stack for this ViewPort
166   my $substack = $vp->create_tangent('name');
167
168   # Retrieve a tangent stack for this ViewPort
169   my $substack = $vp->forcus_tangent('name');
170
171   # Get the names of all the tangent stacks for this ViewPort
172   my @names = $vp->focus_tangents();
173
174   # Fetch all the tangent stacks for this ViewPort
175   # This is called by apply_events
176   my $stacks = $vp->child_event_sinks();
177
178
179   ### The following methods are all called automatically when using
180   ### Reaction::UI::Controller(s)
181   # Resolve current events with this ViewPort
182   $vp->apply_events($ctx, $param_hash);
183
184   # Apply current events to all tangent stacks
185   # This is called by apply_events
186   $vp->apply_child_events($ctx, $params_hash);
187
188   # Apply current events to this ViewPort
189   # This is called by apply_events
190   $vp->apply_our_events($ctx, $params_hash);
191
192 =head1 DESCRIPTION
193
194 A ViewPort describes part of a page, it can be a field, a form or
195 an entire page. ViewPorts are created on a
196 L<Reaction::UI::FocusStack>, usually belonging to a controller or
197 another ViewPort. Each ViewPort knows it's own position in the stack
198 it is in, as well as the stack containing it.
199
200 Each ViewPort has a specific location in the heirarchy of viewports
201 making up a page. The hierarchy is determined as follows: The first
202 ViewPort in a stack is labeled C<0>, the second is C<1> and so on. If
203 a ViewPort is in a named tangent, it's location will contain the name
204 of the tangent in it's location.
205
206 For example, the first ViewPort in the 'left' tangent of the main
207 ViewPort has location C<0.left.0>.
208
209 Several ViewPort attributes are set by
210 L<Reaction::UI::FocusStack/push_viewport> when new ViewPorts are
211 created, these are as follows:
212
213 =over
214
215 =item Automatic:
216
217 =over
218
219 =item outer
220
221 The outer attribute is set to the previous ViewPort in the stack when
222 creating a ViewPort, if the ViewPort is the first in the stack, it
223 will be undef.
224
225 =item inner
226
227 The inner attribute is set to the next ViewPort down in the stack when
228 it is created, if this is the last ViewPort in the stack, it will be
229 undef.
230
231 =item focus_stack
232
233 The focus_stack attribute is set to the L<Reaction::UI::FocusStack>
234 object that created the ViewPort.
235
236 =item ctx
237
238 The ctx attribute will be passed automatically when using
239 L<Reaction::UI::Controller/push_viewport> to create a ViewPort in the
240 base stack of a controller. When creating tangent stacks, you may have
241 to pass it in yourself.
242
243 =back
244
245 =item Optional:
246
247 =over
248
249 =item location
250
251 =item layout
252
253 The layout attribute can either be specifically passed when calling
254 C<push_viewport>, or it will be determined using the last part of the
255 ViewPorts classname.
256
257 =item column_order
258
259 This is generally used by more specialised ViewPorts such as the
260 L<ListView|Reaction::UI::ViewPort::ListView> or
261 L<Action|Reaction::UI::ViewPort::Action>. It can be either a
262 space separated list of column names, or an arrayref of column names.
263
264 =back
265
266 =back
267
268 =head1 METHODS
269
270 =head2 outer
271
272 =over
273
274 =item Arguments: none
275
276 =back
277
278 Fetch the ViewPort outside this one in the page hierarchy.
279
280 =head2 inner
281
282 =over
283
284 =item Arguments: none
285
286 =back
287
288 Fetch the ViewPort inside this one in the page hierarchy.
289
290 =head2 create_tangent
291
292 =over
293
294 =item Arguments: $tangent_name
295
296 =back
297
298 Create a new named L<Reaction::UI::FocusStack> inside this
299 ViewPort. The created FocusStack is returned.
300
301 =head2 focus_tangent
302
303 =over
304
305 =item Arguments: $tangent_name
306
307 =back
308
309 Fetch a named FocusStack from this ViewPort.
310
311 =head2 focus_tangents
312
313 =over
314
315 =item Arguments: none
316
317 =back
318
319 Returns a list of names of all the known tangents in this ViewPort.
320
321 =head2 focus_stack
322
323 Return the L<Reaction::UI::FocusStack> object that this ViewPort is in.
324
325 =head2 apply_events
326
327 =over
328
329 =item Arguments: $ctx, $params_hashref
330
331 =back
332
333 This method is called by the FocusStack object to resolve all events
334 for the ViewPort.
335
336 =head2 apply_child_events
337
338 =over
339
340 =item Arguments: $ctx, $params_hashref
341
342 =back
343
344 Resolve the given events for all the tangents of this ViewPort. Called
345 by L<apply_events>.
346
347 =head2 apply_our_events
348
349 =over
350
351 =item Arguments: $ctx, $events
352
353 =back
354
355 Resolve the given events that match the location of this
356 ViewPort. Called by L<apply_events>.
357
358 =head2 handle_events
359
360 =over
361
362 =item Arguments: $events
363
364 =back
365
366 Actually call the event handlers for this ViewPort. Called by
367 L<apply_our_events>. By default this will do nothing, subclass
368 ViewPort and implement L<accept_events>.
369
370 =head2 accept_events
371
372 =over
373
374 =item Arguments: none
375
376 =back
377
378 Implement this method in a subclass and return a list of events that
379 your ViewPort is accepting.
380
381 =head2 event_id_for
382
383 =over
384
385 =item Arguments: $name
386
387 =back
388
389 Create an id for the given event name and this ViewPort. Generally
390 returns the location and the name, joined with a colon.
391
392 =head2 sort_by_spec
393
394 =over
395
396 =item Arguments: $spec, $items
397
398 =back
399
400 Sorts the given list of items such that the ones that also appear in
401 the spec are at the beginning. This is called by
402 L<Reaction::UI::ViewPort::Action> and
403 L<Reaction::UI::ViewPort::ListView>, and gets passed L<column_order>
404 as the spec argument.
405
406 =head1 AUTHORS
407
408 See L<Reaction::Class> for authors.
409
410 =head1 LICENSE
411
412 See L<Reaction::Class> for the license.
413
414 =cut