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