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