1use warnings;
2use strict;
3
4package Jifty::Web::PageRegion;
5
6=head1 NAME
7
8Jifty::Web::PageRegion - Defines a page region
9
10=head1 DESCRIPTION
11
12Describes a region of the page which contains a Mason fragment which
13can be updated via AJAX or via query parameters.
14
15=cut
16
17use base qw/Jifty::Object Class::Accessor::Fast/;
18__PACKAGE__->mk_accessors(qw(name force_path force_arguments default_path default_arguments qualified_name parent region_wrapper lazy loading_path class));
19use Jifty::JSON;
20use Encode ();
21
22=head2 new PARAMHASH
23
24Creates a new page region.  The possible arguments in the C<PARAMHASH>
25are:
26
27=over
28
29=item name
30
31The (unqualified) name of the region.  This is used to generate a
32unique id -- it should consist of only letters and numbers.
33
34=item path
35
36The path to the fragment that this page region contains.  Defaults to
37C</__jifty/empty>, which, as its name implies, is empty.
38
39=item arguments (optional) (formerly 'defaults')
40
41Specifies an optional set of parameter defaults.  These should all be
42simple scalars, as they might be passed across HTTP if AJAX is used.
43
44See L<Jifty::Web::Form::Element> for a list of the supported parameters.
45
46=item force_arguments (optional)
47
48Specifies an optional set of parameter values. They will override anything
49sent by the user or set via AJAX.
50
51=item force_path (optional)
52
53A fixed path to the fragment that this page region contains.  Overrides anything set by the user.
54
55=item parent (optional)
56
57The parent L<Jifty::Web::PageRegion> that this region is enclosed in.
58
59=item region_wrapper (optional)
60
61A boolean; whether or not the region, when rendered, will include the
62HTML region preamble that makes Javascript aware of its presence.
63Defaults to true.
64
65=item lazy (optional)
66
67Delays the loading of the fragment until client render-time.
68Obviously, does not work with downlevel browsers which don't support
69javascript.
70
71=item loading_path (optional)
72
73The fragment to display while the client fetches the actual region.
74Make this lightweight, or you'll be losing most of the benefits of
75lazy loading!
76
77=back
78
79=cut
80
81sub new {
82    my $class = shift;
83    my $self = bless {}, $class;
84
85    my %args = (
86                name => undef,
87                path => "/__jifty/empty",
88                defaults => {},
89                parent => undef,
90                force_arguments => {},
91                force_path => undef,
92                region_wrapper => 1,
93                lazy => 0,
94                loading_path => undef,
95                @_
96               );
97
98
99    $args{'arguments'} ||= delete $args{'defaults'};
100
101    # Name is required
102    if (not defined $args{name}) {
103        warn "Name is required for page regions.";
104        return;
105    }
106
107    # References don't go over HTTP very well
108    if (grep {ref $_} values %{$args{arguments}}) {
109        warn "Reference '$args{arguments}{$_}' passed as default for '$_' to region '$args{name}'"
110          for grep {ref $args{arguments}{$_}} keys %{$args{arguments}};
111        return;
112    }
113
114    $self->name($args{name});
115    $self->qualified_name(Jifty->web->qualified_region($self));
116    $self->default_path($args{path});
117    $self->default_arguments($args{arguments});
118    $self->force_arguments($args{force_arguments});
119    $self->force_path($args{force_path});
120    $self->arguments({});
121    $self->parent($args{parent} || Jifty->web->current_region);
122    $self->region_wrapper($args{region_wrapper});
123    $self->lazy($args{lazy});
124    $self->loading_path($args{loading_path});
125    $self->class($args{class});
126
127    # Keep track of the fully qualified name (which should be unique)
128    $self->log->warn("Repeated region: " . $self->qualified_name)
129        if Jifty->web->get_region( $self->qualified_name );
130    Jifty->web->{'regions'}{ $self->qualified_name } = $self;
131
132    return $self;
133}
134
135=head2 name [NAME]
136
137Gets or sets the name of the page region.
138
139=cut
140
141=head2 qualified_name [NAME]
142
143Gets or sets the fully qualified name of the page region.  This should
144be unique on a page.  This is usually set by L</enter>, based on the
145page regions that this region is inside.  See
146L<Jifty::Web/qualified_region>.
147
148=cut
149
150=head2 default_path [PATH]
151
152Gets or sets the default path of the fragment.  This is overridden by
153L</path>.
154
155=cut
156
157=head2 path [PATH]
158
159Gets or sets the path that the fragment actually contains.  This
160overrides L</default_path>.
161
162=cut
163
164sub path {
165    my $self = shift;
166    $self->{path} = shift if @_;
167    return $self->{path} || $self->default_path;
168}
169
170=head2 default_argument NAME [VALUE]
171
172Gets or sets the default value of the C<NAME> argument.  This is used
173as a fallback, and also to allow generated links to minimize the
174amount of state they must transmit.
175
176=cut
177
178sub default_argument {
179    my $self = shift;
180    my $name = shift;
181    $self->{default_arguments}{$name} = shift if @_;
182    return $self->{default_arguments}{$name} || '';
183}
184
185=head2 argument NAME [VALUE]
186
187Gets or sets the actual run-time value of the page region.  This
188usually comes from HTTP parameters.  It overrides the
189L</default_argument> of the same C<NAME>.
190
191=cut
192
193sub argument {
194    my $self = shift;
195    my $name = shift;
196    $self->{arguments}{$name} = shift if @_;
197    return $self->force_arguments->{$name}||$self->{arguments}{$name} || $self->default_argument($name);
198}
199
200=head2 arguments [HASHREF]
201
202Sets all arguments at once, or returns all arguments.  The latter will
203also include all default arguments.
204
205=cut
206
207sub arguments {
208    my $self = shift;
209    $self->{arguments} = shift if @_;
210    return { %{$self->{default_arguments}}, %{$self->{arguments}}, %{$self->force_arguments}};
211}
212
213=head2 enter
214
215Enters the region; this sets the qualified name based on
216L<Jifty::Web/qualified_region>, and uses that to pull runtime values
217for the L</path> and L</argument>s from the
218L<Jifty::Request/state_variables> before overriding them with the "force" versions.
219
220=cut
221
222sub enter {
223    my $self = shift;
224
225    # Add ourselves to the region stack
226    push @{Jifty->web->{'region_stack'}}, $self;
227
228    # Merge in the settings passed in via state variables
229    for my $var (Jifty->web->request->state_variables) {
230        my $key = $var->key;
231        my $value = $var->value || '';
232
233        if ($key =~ /^region-(.*?)\.(.*)/ and $1 eq $self->qualified_name and $value ne $self->default_argument($2)) {
234            $self->argument($2 => $value);
235        }
236        if ($key =~ /^region-(.*)$/ and $1 eq $self->qualified_name and $value ne $self->default_path) {
237            $self->path(URI::Escape::uri_unescape($value));
238        }
239
240        # We should always inherit the state variables from the uplevel request.
241        Jifty->web->set_variable($key => $value);
242    }
243
244    for my $argument (keys %{$self->force_arguments}) {
245            $self->argument($argument => $self->force_arguments->{$argument});
246    }
247
248    $self->path($self->force_path) if ($self->force_path);
249}
250
251=head2 exit
252
253Exits the page region, if it is the most recent one.  Normally, you
254won't need to call this by hand; however, if you are calling L</enter>
255by hand, you will need to call the corresponding C<exit>.
256
257=cut
258
259sub exit {
260    my $self = shift;
261
262    if (Jifty->web->current_region != $self) {
263        $self->log->warn("Attempted to exit page region ".$self->qualified_name." when it wasn't the most recent");
264    } else {
265        pop @{Jifty->web->{'region_stack'}};
266    }
267}
268
269=head2 as_string
270
271Deals with the bulk of the effort to show a page region.  Returns a
272string of the fragment and associated javascript (if any).
273
274=cut
275
276sub as_string {
277    my $self = shift;
278    Jifty->handler->buffer->push(private => 1, from => "PageRegion render of ".$self->path);
279    $self->make_body;
280    return Jifty->handler->buffer->pop;
281}
282
283=head2 render
284
285Calls L</enter>, outputs the results of L</as_string>, and then calls
286L</exit>.  Returns an empty string.
287
288=cut
289
290sub render {
291    my $self = shift;
292
293    $self->enter;
294    $self->make_body;
295    $self->exit;
296
297    return '';
298}
299
300=head2 make_body
301
302Outputs the results of the region to the current buffer.
303
304=cut
305
306sub make_body {
307    my $self = shift;
308    my $buffer = Jifty->handler->buffer;
309
310    my %arguments = %{ $self->arguments };
311
312    # undef arguments cause warnings. We hatesses the warnings, we do.
313    defined $arguments{$_} or delete $arguments{$_} for keys %arguments;
314
315    # We need to tell the browser this is a region and what its
316    # default arguments are as well as the path of the "fragment".  We
317    # do this by passing in a snippet of javascript which encodes this
318    # information.  We only render this region wrapper if we're asked
319    # to (which is true by default)
320    if ( $self->region_wrapper ) {
321         $buffer->append(qq|<script type="text/javascript">\n|
322            . qq|new Region('| . $self->qualified_name . qq|',|
323            . Jifty::JSON::encode_json( \%arguments ) . qq|,|
324            . qq|'| . $self->path . qq|',|
325            . ( $self->parent ? qq|'| . $self->parent->qualified_name . qq|'| : q|null|)
326            . qq|,| . (Jifty->web->form->is_open ? '1' : 'null')
327            . qq|);\n|
328            . qq|</script>|);
329        if ($self->lazy) {
330            $buffer->append(qq|<script type="text/javascript">|
331              . qq|jQuery(function(){ Jifty.update( { 'fragments': [{'region': '|.$self->qualified_name.qq|', 'mode': 'Replace'}], 'actions': {}}, document.getElementById('region-|.$self->qualified_name.qq|'))})|
332              . qq|</script>|);
333        }
334
335        my $class = 'jifty-region';
336        $class .= ' ' . $self->class if $self->class;
337        $buffer->append(qq|<div id="region-| . $self->qualified_name . qq|" class="| . $class . qq|">|);
338
339        if ($self->lazy) {
340            if ($self->loading_path) {
341                local $self->{path} = $self->loading_path;
342                $self->render_as_subrequest(\%arguments);
343            }
344            $buffer->append(qq|</div>|);
345            return;
346        }
347    }
348
349    $self->render_as_subrequest(\%arguments);
350    $buffer->append(qq|</div>|) if ( $self->region_wrapper );
351}
352
353=head2 render_as_subrequest
354
355=cut
356
357sub render_as_subrequest {
358    my ($self, $arguments, $enable_actions) = @_;
359
360    # Make a fake request and throw it at the dispatcher
361    my $subrequest = Jifty->web->request->clone;
362    $subrequest->argument( region => $self );
363    # XXX: use ->arguments?
364    $subrequest->argument( $_ => $arguments->{$_}) for keys %$arguments;
365    $subrequest->template_arguments({});
366    $subrequest->path( $self->path );
367    $subrequest->top_request( Jifty->web->request->top_request );
368
369    my %args;
370    if ($self->path =~ m/\?/) {
371        # XXX: this only happens if we are redirect within region AND
372        # with continuation, which is already taken care of by the
373        # clone.
374        my ($path, $arg) = split(/\?/, $self->path, 2);
375        $subrequest->path( $path );
376        %args = (map { split /=/, $_ } split /&/, $arg);
377        if ($args{'J:C'}) {
378            $subrequest->continuation($args{'J:C'});
379        }
380    }
381    # Remove all of the actions
382    unless ($enable_actions) {
383        $_->active(0) for ($subrequest->actions);
384    }
385    # $subrequest->clear_actions;
386    local Jifty->web->{request} = $subrequest;
387    if ($args{'J:RETURN'}) {
388        my $top = Jifty->web->request->top_request;
389        my $cont = Jifty->web->session->get_continuation($args{'J:RETURN'});
390        $cont->return;
391        # need to set this as subrequest again as it's clobbered by the return
392        Jifty->web->request->top_request($top);
393    }
394
395    # Call into the dispatcher
396    Jifty->handler->dispatcher->handle_request;
397
398    return;
399}
400
401=head2 get_element [RULES]
402
403Returns a CSS2 selector which selects only elements under this region
404which fit the C<RULES>.  This method is used by AJAX code to specify
405where to add new regions.
406
407=cut
408
409sub get_element {
410    my $self = shift;
411    return "#region-" . $self->qualified_name . ' ' . join(' ', @_);
412}
413
414=head2 client_cacheable
415
416Returns the client cacheable state of the regions path. Returns false if the template has not been marked as client cacheable. Otherwise it returns the string "static" or "action" based on the cacheable attribute set on the template.
417
418=cut
419
420sub client_cacheable {
421    my $self = shift;
422    my ($jspr) = Jifty->find_plugin('Jifty::Plugin::JSPageRegion') or return;
423
424    return $jspr->client_cacheable($self->path);
425}
426
427=head2 client_cache_content
428
429Returns the template as JavaScript code.
430
431=cut
432
433sub client_cache_content {
434    my $self = shift;
435    my ($jspr) = Jifty->find_plugin('Jifty::Plugin::JSPageRegion') or return;
436
437    return $jspr->compile_to_js($self->path);
438}
439
4401;
441