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