Catalyst-Plugin-Navigation

 view release on metacpan or  search on metacpan

lib/CatalystX/NavigationMenu.pm  view on Meta::CPAN

package CatalystX::NavigationMenu;

use strict;
use warnings;

use Moose;
use CatalystX::NavigationMenuItem;
use namespace::autoclean;

=head1 NAME

CatalystX::NavigationMenu

=head1 SYNOPSIS

	my $nm = CatalystX::NavigationMenu->new();
	$nm->popupate($c);

	my $menu = $nm->get_navigation($c, {level => 0});

=head1 DESCRIPTION

CatalystX::NavigationMenu provides a menu object to be used when creating and
managing menus in Catalyst based on attribute data. For details of the Catalyst
attributes see the L(Catalyst::Plugin::Navigation) documentation.

=cut 

has items => (
	traits => ['Array'],
	is => 'rw',
	isa => 'ArrayRef[CatalystX::NavigationMenuItem]',
	default => sub{[]},
	handles    => {
		all_items    => 'elements',
		insert_item  => 'push',
		shift_item   => 'shift',
		find_item    => 'first',
		count_items  => 'count',
		has_items    => 'is_empty',
		sort_items   => 'sort',
	},
);

=head1 METHODS

=head2 populate($c)

Populates the menu based on the controllers found in the Catalyst object.

=cut

sub populate {
	my ($self, $c) = @_;

	my $dispatcher = $c->dispatcher;
	foreach my $c_name ($c->controllers(qr//)) {
		my $controller = $c->controller($c_name);
		my @actions = $dispatcher->get_containers($controller->action_namespace($c));
		$c->log->debug("Looking at Controller $c_name for navigation entries") if $c->debug;

		foreach my $ac (@actions) {
			my $acts = $ac->actions;
			foreach my $key (keys(%$acts)) {
				my $action = $acts->{$key};
				if ($action->attributes->{Menu}) {
					# And get the menu to insert it into the right location.
					$c->log->debug("Adding action item for path: " . ($action->namespace || '') . '/' . ($action->name || '') . " with parent: " . 
						($action->attributes->{MenuParent}->[0] || '') ) if $c->debug;
					$self->add_action($action);
				}
			}
		}
	}
}

=head2 add_action($action)

Adds an element into the menu based on the Catalyst action provided.

=cut

sub add_action {
	my ($self, $action) = @_;

	# Create the items needed to build the item.
	my $parent = $action->attributes->{MenuParent}->[0] || '';
	my $action_args = $action->attributes->{MenuArgs} || [];
	my $conditions = $action->attributes->{MenuCond} || [];
	my $order = $action->attributes->{MenuOrder}->[0] || 0;
	my $roles = $action->attributes->{MenuRoles} || [];
	my $title = $action->attributes->{MenuTitle}->[0] || '';

	my $item = CatalystX::NavigationMenuItem->new(
		label => $action->attributes->{Menu}->[0],
		title => $title,
		action => $action, 
		path => $action->namespace . '/' . $action->name,
		parent => $parent,
		action_args => $action_args,
		conditions => $conditions,
		order => $order,
		required_roles => $roles,
	);

	$self->add_item($item);
}

=head2 get_child_with_path($path)

Returns a child NavigationMenu item that contains the given path. If no child is found
then undef is returned.

=cut

sub get_child_with_path {
	my ($self, $path) = @_;

	return $self->find_item(sub {$_->contains_path($path)});
}

=head2 add_item($item)

Adds the given menu item to this tree under the appropriate path entry. If the path
entry isn't found then it is added to this tree.

=cut

sub add_item {
	my ($self, $item) = @_;

	# Check to see if we have already added this item.
	my $path = $item->path;
	my $d_item = $self->get_child_with_path($path);
	if ($d_item) {
		return;
	};


	# See if we have an item with 
	my $p_item = $self->get_child_with_path($item->parent);
	if ($p_item) {
		$p_item->add_item($item);
	}
	else {
		if ($item->parent =~ /#/) {
			my @path_parts = split(/(?=#)/, $item->parent);

			my $parent_path = shift(@path_parts);
			# See if we can find the parent for the first part of the path.
			$p_item = $self->get_child_with_path($parent_path);
			if ($p_item) {
				# We have a parent.
				$item->_set_parent(join('', @path_parts));
			}
			else {
				if ($parent_path =~ /^#/) {
					# The parent path is just a label. so create a dummy item.
					$p_item = CatalystX::NavigationMenuItem->new(
						label => $', #The label is everything after the # in the path.
						parent => '', # No parent item.
						path => $parent_path,
					);
				}
				else {
					# We need to create a new container item
					my $label = $path_parts[0];
					$label =~ s/^#//;
					$p_item = CatalystX::NavigationMenuItem->new(
						label => $label, 
						parent => $parent_path,
						path => $label,
					);
				}
				$item->_set_parent(join('', @path_parts));
				$self->add_item($p_item);
			}
			$p_item->add_item($item);
		}
		else {
			# Add the parent item to this menu.
			$self->insert_item($item);
		}
	}

	# Now check to see if there are any children in this menu that need to be
	# added as children of this new item.
	my $child_count = $self->count_items;
	for (my $i = 0; $i < $child_count; $i++) {
		my $child = $self->shift_item;
		if ($child->parent eq $item->path) {
			$item->add_item($child);
		}
		else {
			$self->insert_item($child);
		}
	}
}

=head2 get_navigation($c, $attrs)

Returns an array reference to a menu entry. This will only show one level of a
menu. The values of the array are the values returned by the L(NavigationMenuItem)
nav_entry() method.

=cut

sub get_navigation {
	my ($self, $c, $attrs) = @_;

	my $nav = [];

	# see if we need to get a particular level of menu or not.
	if ($attrs && exists($attrs->{level}) && $attrs->{level} =~ /^\d+$/) {
		if ($attrs->{level} == '0') {
			# We want this menu only.
			foreach my $i ($self->sorted_items) {
				my $entry = $i->nav_entry($c, 0);
				push(@$nav, $entry) if ($entry);
			}
		}
		else {
			my $path = $c->action->namespace . '/' . $c->action->name;
			my $active = $self->get_child_with_path($path);
			if ($active && $active->has_children) {
				$attrs->{level}--;
				return $active->children->get_navigation($c, $attrs);
			}
		}
	}
	else {
		foreach my $i ($self->sorted_items) {
			my $entry = $i->nav_entry($c, 1);
			push(@$nav, $entry) if ($entry);
		}
	}

	return $nav;
}

=head2 sorted_items

Returns the menu items found at this level in sorted order. The sort order is
based on their order value and an alphanumeric sort of the menu label.

=cut

sub sorted_items {
	my ($self) = @_;

	return $self->sort_items(sub {
		if ($_[0]->order == $_[1]->order) {
			# We need to do a name sort on the label then.
			return $_[0]->path cmp $_[1]->path;
		}
		
		return $_[0]->order <=> $_[1]->order;
	});
}

=head2 get_hierarchy([$indent])

Returns a string containing the hierachy of the complete menu found here. This is 
mostly used for debugging that menus are setup correctly.

=cut

sub get_hierarchy {
	my ($self, $indent) = @_;

	my $str = '';
	$indent = '' if (!$indent);

	foreach my $item ($self->all_items) {
		$str .= $indent . $item->path . "\n";
		if ($item->has_children) {
			$str .= $item->children->get_hierarchy($indent . "\t");
		}
	}

	return $str;
}

__PACKAGE__->meta->make_immutable;

1;

__END__

=head1 DEPENDENCIES

L<Catalyst>

=head1 SEE ALSO 

L<CatalystX::NavigationMenuItem>

=head1 AUTHORS

Derek Wueppelmann <derek@roaringpenguin.com>

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2011 Roaring Penguin Software, Inc.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

=cut



( run in 2.198 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )