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