# BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) # # # LICENSE: # # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. # # This work is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 or visit their web page on the internet at # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. # # # CONTRIBUTION SUBMISSION POLICY: # # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of # the GNU General Public License and is only of importance to you if # you choose to contribute your changes and enhancements to the # community by submitting them to Best Practical Solutions, LLC.) # # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with # Request Tracker, to Best Practical Solutions, LLC, you confirm that # you are the copyright holder for those contributions and you grant # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, # royalty-free, perpetual, license to use, copy, create derivative # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} package RT::Interface::Web::Menu; use strict; use warnings; use base qw/Class::Accessor::Fast/; use URI; use Scalar::Util qw(weaken); __PACKAGE__->mk_accessors(qw( key title description raw_html escape_title sort_order target class )); =head1 NAME RT::Interface::Web::Menu - Handle the API for menu navigation =head1 METHODS =head2 new PARAMHASH Creates a new L object. Possible keys in the I are L, L, L, L, L, L, L, L, L and L. See the subroutines with the respective name below for each option's use. =cut sub new { my $package = shift; my $args = ref($_[0]) eq 'HASH' ? shift @_ : {@_}; my $parent = delete $args->{'parent'}; $args->{sort_order} ||= 0; # Class::Accessor only wants a hashref; my $self = $package->SUPER::new( $args ); # make sure our reference is weak $self->parent($parent) if defined $parent; return $self; } =head2 title [STRING] Sets or returns the string that the menu item will be displayed as. =head2 escape_title [BOOLEAN] Sets or returns whether or not to HTML escape the title before output. =head2 parent [MENU] Gets or sets the parent L of this item; this defaults to null. This ensures that the reference is weakened. =head2 raw_html [STRING] Sets the content of this menu item to a raw blob of HTML. When building the menu, rather than constructing a link, we will return this raw content. No escaping is done. =cut sub parent { my $self = shift; if (@_) { $self->{parent} = shift; weaken $self->{parent}; } return $self->{parent}; } =head2 sort_order [NUMBER] Gets or sets the sort order of the item, as it will be displayed under the parent. This defaults to adding onto the end. =head2 target [STRING] Get or set the frame or pseudo-target for this link. something like L<_blank> =head2 class [STRING] Gets or sets the CSS class the menu item should have in addition to the default classes. This is only used if L isn't specified. =head2 path Gets or sets the URL that the menu's link goes to. If the link provided is not absolute (does not start with a "/"), then is is treated as relative to it's parent's path, and made absolute. =cut sub path { my $self = shift; if (@_) { if (defined($self->{path} = shift)) { my $base = ($self->parent and $self->parent->path) ? $self->parent->path : ""; $base .= "/" unless $base =~ m{/$}; my $uri = URI->new_abs($self->{path}, $base); $self->{path} = $uri->as_string; } } return $self->{path}; } =head2 active [BOOLEAN] Gets or sets if the menu item is marked as active. Setting this cascades to all of the parents of the menu item. This is currently B. =cut sub active { my $self = shift; if (@_) { $self->{active} = shift; $self->parent->active($self->{active}) if defined $self->parent; } return $self->{active}; } =head2 child KEY [, PARAMHASH] If only a I is provided, returns the child with that I. Otherwise, creates or overwrites the child with that key, passing the I to L. Additionally, the paramhash's L defaults to the I, and the L defaults to the pre-existing child's sort order (if a C is being over-written) or the end of the list, if it is a new C. If the paramhash contains a key called C, that will be used instead of creating a new RT::Interface::Web::Menu. =cut sub child { my $self = shift; my $key = shift; my $proto = ref $self || $self; if ( my %args = @_ ) { # Clear children ordering cache delete $self->{children_list}; my $child; if ( $child = $args{menu} ) { $child->parent($self); } else { $child = $proto->new( { parent => $self, key => $key, title => $key, escape_title=> 1, %args } ); } $self->{children}{$key} = $child; $child->sort_order( $args{sort_order} || (scalar values %{ $self->{children} }) ) unless ($child->sort_order()); # URL is relative to parents, and cached, so set it up now $child->path( $child->{path} ); # Figure out the URL my $path = $child->path; # Activate it if ( defined $path and length $path ) { my $base_path = $HTML::Mason::Commands::r->path_info; my $query = $HTML::Mason::Commands::m->cgi_object->query_string; $base_path =~ s!/+!/!g; $base_path .= "?$query" if defined $query and length $query; $base_path =~ s/index\.html$//; $base_path =~ s/\/+$//; $path =~ s/index\.html$//; $path =~ s/\/+$//; if ( $path eq $base_path ) { $self->{children}{$key}->active(1); } } } return $self->{children}{$key}; } =head2 active_child Returns the first active child node, or C is there is none. =cut sub active_child { my $self = shift; foreach my $kid ($self->children) { return $kid if $kid->active; } return undef; } =head2 delete KEY Removes the child with the provided I. =cut sub delete { my $self = shift; my $key = shift; delete $self->{children_list}; delete $self->{children}{$key}; } =head2 has_children Returns true if there are any children on this menu =cut sub has_children { my $self = shift; if (@{ $self->children}) { return 1 } else { return 0; } } =head2 children Returns the children of this menu item in sorted order; as an array in array context, or as an array reference in scalar context. =cut sub children { my $self = shift; my @kids; if ($self->{children_list}) { @kids = @{$self->{children_list}}; } else { @kids = values %{$self->{children} || {}}; @kids = sort {$a->{sort_order} <=> $b->{sort_order}} @kids; $self->{children_list} = \@kids; } return wantarray ? @kids : \@kids; } =head2 add_after Called on a child, inserts a new menu item after it and shifts any other menu items at this level to the right. L by default would insert at the end of the list of children, unless you did manual sort_order calculations. Takes all the regular arguments to L. =cut sub add_after { shift->_insert_sibling("after", @_) } =head2 add_before Called on a child, inserts a new menu item at the child's location and shifts the child and the other menu items at this level to the right. L by default would insert at the end of the list of children, unless you did manual sort_order calculations. Takes all the regular arguments to L. =cut sub add_before { shift->_insert_sibling("before", @_) } sub _insert_sibling { my $self = shift; my $where = shift; my $parent = $self->parent; my $sort_order; for my $contemporary ($parent->children) { if ( $contemporary->key eq $self->key ) { if ($where eq "before") { # Bump the current child and the following $sort_order = $contemporary->sort_order; } elsif ($where eq "after") { # Leave the current child along, bump the rest $sort_order = $contemporary->sort_order + 1; next; } else { # never set $sort_order, act no differently than ->child() } } if ( $sort_order ) { $contemporary->sort_order( $contemporary->sort_order + 1 ); } } $parent->child( @_, sort_order => $sort_order ); } 1;