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