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