lib: Add navigation classes (WIP)

- Lacks custom renderer functionality
- Lacks navigation item priorities
- Lacks permission handling

refs #5600
This commit is contained in:
Eric Lippmann 2015-09-01 12:48:45 +02:00
parent 1014cdfa3c
commit 1122ffafad
6 changed files with 1319 additions and 0 deletions

View File

@ -0,0 +1,21 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation;
/**
* Dropdown navigation item
*
* @see \Icinga\Web\Navigation\Navigation For a usage example.
*/
class DropdownItem extends NavigationItem
{
/**
* {@inheritdoc}
*/
public function init()
{
$this->children->setLayout(Navigation::LAYOUT_DROPDOWN);
}
}

View File

@ -0,0 +1,235 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation;
use ArrayAccess;
use ArrayIterator;
use Countable;
use InvalidArgumentException;
use IteratorAggregate;
/**
* Container for navigation items
*
* Usage example:
* <code>
* <?php
*
* namespace Icinga\Example;
*
* use Icinga\Web\Navigation\DropdownItem;
* use Icinga\Web\Navigation\Navigation;
* use Icinga\Web\Navigation\NavigationItem;
*
* $navigation = new Navigation();
* $navigation->setLayout(Navigation::LAYOUT_TABS);
* $home = new NavigationItem();
* $home
* ->setIcon('home')
* ->setLabel('Home');
* ->setUrl('/home');
* $logout = new NavigationItem();
* $logout
* ->setIcon('logout')
* ->setLabel('Logout')
* ->setUrl('/logout');
* $dropdown = new DropdownItem();
* $dropdown
* ->setIcon('user')
* ->setLabel('Preferences');
* $preferences = new NavigationItem();
* $preferences
* ->setIcon('preferences');
* ->setLabel('preferences')
* ->setUrl('/preferences');
* $dropdown->addChild($preferences);
* $navigation
* ->addItem($home)
* ->addItem($logout);
* ->addItem($dropdown);
* echo $navigation
* ->getRenderer()
* ->setCssClass('example-nav')
* ->render();
*/
class Navigation implements ArrayAccess, Countable, IteratorAggregate
{
/**
* Flag for dropdown layout
*
* @var int
*/
const LAYOUT_DROPDOWN = 1;
/**
* Flag for tabs layout
*
* @var int
*/
const LAYOUT_TABS = 2;
/**
* Navigation items
*
* @var NavigationItem[]
*/
protected $items = array();
/**
* Navigation layout
*
* @var int
*/
protected $layout;
/**
* {@inheritdoc}
*/
public function offsetExists($offset)
{
return isset($this->items[$offset]);
}
/**
* {@inheritdoc}
*/
public function offsetGet($offset)
{
return isset($this->items[$offset]) ? $this->items[$offset] : null;
}
/**
* {@inheritdoc}
*/
public function offsetSet($offset, $value)
{
$this->items[$offset] = $value;
}
/**
* {@inheritdoc}
*/
public function offsetUnset($offset)
{
unset($this->items[$offset]);
}
/**
* {@inheritdoc}
*/
public function count()
{
return count($this->items);
}
/**
* {@inheritdoc}
*/
public function getIterator()
{
return new ArrayIterator($this->items);
}
/**
* Ad a navigation item
*
* @param NavigationItem|array $item The item to append
*
* @return $this
* @throws InvalidArgumentException If the item argument is invalid
*/
public function addItem(NavigationItem $item)
{
if (! $item instanceof NavigationItem) {
if (! is_array($item)) {
throw new InvalidArgumentException(
'Argument item must be either an array or an instance of NavigationItem'
);
}
$item = new NavigationItem($item);
}
$this->items[] = $item;
return $this;
}
/**
* Get the item with the given ID
*
* @param mixed $id
* @param mixed $default
*
* @return NavigationItem|mixed
*/
public function getItem($id, $default = null)
{
if (isset($this->items[$id])) {
return $this->items[$id];
}
return $default;
}
/**
* Get the items
*
* @return array
*/
public function getItems()
{
return $this->items;
}
/**
* Get whether the navigation has items
*
* @return bool
*/
public function hasItems()
{
return ! empty($this->items);
}
/**
* Get whether the navigation is empty
*
* @return bool
*/
public function isEmpty()
{
return empty($this->items);
}
/**
* Get the layout
*
* @return int
*/
public function getLayout()
{
return $this->layout;
}
/**
* Set the layout
*
* @param int $layout
*
* @return $this
*/
public function setLayout($layout)
{
$this->layout = (int) $layout;
return $this;
}
/**
* Get the navigation renderer
*
* @return RecursiveNavigationRenderer
*/
public function getRenderer()
{
return new RecursiveNavigationRenderer($this);
}
}

View File

@ -0,0 +1,483 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation;
use Countable;
use InvalidArgumentException;
use IteratorAggregate;
use Icinga\Application\Icinga;
use Icinga\Web\View;
use Icinga\Web\Url;
/**
* A navigation item
*
* @see \Icinga\Web\Navigation\Navigation For a usage example.
*/
class NavigationItem implements Countable, IteratorAggregate
{
/**
* Alternative markup element if the navigation item has no URL
*
* @var string
*/
const LINK_ALTERNATIVE = 'span';
/**
* Whether the item is active
*
* @var bool
*/
protected $active = false;
/**
* Attributes of the item's element
*
* @var array
*/
protected $attributes = array();
/**
* Item's children
*
* @var Navigation
*/
protected $children;
/**
* Icon
*
* @var string|null
*/
protected $icon;
/**
* Item's ID
*
* @var mixed
*/
protected $id;
/**
* Label
*
* @var string|null
*/
protected $label;
/**
* Parent
*
* @var NavigationItem|null
*/
private $parent;
/**
* URL
*
* @var Url|null
*/
protected $url;
/**
* URL parameters
*
* @var array
*/
protected $urlParameters = array();
/**
* View
*
* @var View|null
*/
protected $view;
/**
* Create a new navigation item
*
* @param array $properties
*/
public function __construct(array $properties = array())
{
if (! empty($properties)) {
$this->setProperties($properties);
}
$this->children = new Navigation();
$this->init();
}
/**
* Initialize the navigation item
*/
public function init()
{
}
/**
* {@inheritdoc}
*/
public function count()
{
return $this->children->count();
}
/**
* {@inheritdoc}
*/
public function getIterator()
{
return $this->children;
}
/**
* Get whether the item is active
*
* @return bool
*/
public function getActive()
{
return $this->active;
}
/**
* Set whether the item is active
*
* Bubbles active state.
*
* @param bool $active
*
* @return $this
*/
public function setActive($active = true)
{
$this->active = (bool) $active;
$parent = $this;
while (($parent = $parent->parent) !== null) {
$parent->setActive($active);
}
return $this;
}
/**
* Get an attribute's value of the item's element
*
* @param string $name Name of the attribute
* @param mixed $default Default value
*
* @return mixed
*/
public function getAttribute($name, $default = null)
{
$name = (string) $name;
if (array_key_exists($name, $this->attributes)) {
return $this->attributes[$name];
}
return $default;
}
/**
* Set an attribute of the item's element
*
* @param string $name Name of the attribute
* @param mixed $value Value of the attribute
*
* @return $this
*/
public function setAttribute($name, $value)
{
$name = (string) $name;
$this->attributes[$name] = $value;
return $this;
}
/**
* Get the item's attributes
*
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Set the item's attributes
*
* @param array $attributes
*
* @return $this
*/
public function setAttributes(array $attributes)
{
foreach ($attributes as $name => $value) {
$this->setAttribute($name, $value);
}
return $this;
}
/**
* Add a child item to this item
*
* Bubbles active state.
*
* @param NavigationItem|array $child The child to add
*
* @return $this
* @throws InvalidArgumentException If the child argument is invalid
*/
public function addChild($child)
{
if (! $child instanceof NavigationItem) {
if (! is_array($child)) {
throw new InvalidArgumentException(
'Argument child must be either an array or an instance of NavigationItem'
);
}
$child = new NavigationItem($child);
}
$child->parent = $this;
$this->children->addItem($child);
if ($child->getActive()) {
$this->setActive();
}
return $this;
}
/**
* Get the item's children
*
* @return Navigation
*/
public function getChildren()
{
return $this->children;
}
/**
* Get whether the item has children
*
* @return bool
*/
public function hasChildren()
{
return ! $this->children->isEmpty();
}
/**
* Set children
*
* @param Navigation $children
*
* @return $this
*/
public function setChildren(Navigation $children)
{
$this->children = $children;
return $this;
}
/**
* Get the icon
*
* @return string|null
*/
public function getIcon()
{
return $this->icon;
}
/**
* Set the icon
*
* @param string $icon
*
* @return $this
*/
public function setIcon($icon)
{
$this->icon = (string) $icon;
return $this;
}
/**
* Get the item's ID
*
* @return mixed
*/
public function getId()
{
return $this->id;
}
/**
* Set the item's ID
*
* @param mixed $id ID of the item
*
* @return $this
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* Get the label
*
* @return string|null
*/
public function getLabel()
{
return $this->label;
}
/**
* Set the label
*
* @param string $label
*
* @return $this
*/
public function setLabel($label)
{
$this->label = (string) $label;
return $this;
}
/**
* Get the URL
*
* @return Url|null
*/
public function getUrl()
{
if ($this->url !== null && ! $this->url instanceof Url) {
$this->url = Url::fromPath((string) $this->url);
}
return $this->url;
}
/**
* Set the URL
*
* @param Url|string $url
*
* @return $this
*/
public function setUrl($url)
{
$this->url = $url;
return $this;
}
/**
* Get the URL parameters
*
* @return array
*/
public function getUrlParameters()
{
return $this->urlParameters;
}
/**
* Set the URL parameters
*
* @param array $urlParameters
*
* @return $this
*/
public function setUrlParameters(array $urlParameters)
{
$this->urlParameters = $urlParameters;
return $this;
}
/**
* Get the view
*
* @return View
*/
public function getView()
{
if ($this->view === null) {
$this->view = Icinga::app()->getViewRenderer()->view;
}
return $this->view;
}
/**
* Set the view
*
* @param View $view
*
* @return $this
*/
public function setView(View $view)
{
$this->view = $view;
return $this;
}
/**
* Set properties for the item
*
* @param array $properties
*
* @return $this
*/
public function setProperties(array $properties = array())
{
foreach ($properties as $name => $value) {
$setter = 'set' . ucfirst($name);
if (method_exists($this, $setter)) {
$this->$setter($value);
}
}
return $this;
}
/**
* Render the navigation item to HTML
*
* @return string
*/
public function render()
{
$view = $this->getView();
$label = $view->escape($this->getLabel());
if (null !== $icon = $this->getIcon()) {
$label = $view->icon($icon) . $label;
}
if (null !== $url = $this->getUrl()) {
$content = sprintf(
'<a%s href="%s">%s</a>',
$view->propertiesToString($this->getAttributes()),
$view->url($url, $this->getUrlParameters()),
$label
);
} else {
$content = sprintf(
'<%1$s%2$s>%3$s</%1$s>',
static::LINK_ALTERNATIVE,
$view->propertiesToString($this->getAttributes()),
$label
);
}
return $content;
}
/**
* Render the navigation item to HTML
*
* @return string
*/
public function __toString()
{
return $this->render();
}
}

View File

@ -0,0 +1,340 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation;
use ArrayIterator;
use RecursiveIterator;
use Icinga\Application\Icinga;
use Icinga\Web\View;
/**
* Renderer for single level navigation
*/
class NavigationRenderer implements RecursiveIterator, NavigationRendererInterface
{
/**
* Content to render
*
* @var array
*/
private $content = array();
/**
* CSS class for the navigation element
*
* @var string|null
*/
protected $cssClass;
/**
* Flags
*
* @var int
*/
private $flags;
/**
* The heading for the navigation
*
* @var string
*/
protected $heading;
/**
* Flag for checking whether to call begin/endMarkup() or not
*7
* @var bool
*/
private $inIteration = false;
/**
* Iterator over navigation
*
* @var ArrayIterator
*/
private $iterator;
/**
* Current navigation
*
* @var Navigation
*/
private $navigation;
/**
* View
*
* @var View|null
*/
protected $view;
/**
* Create a new navigation renderer
*
* @param Navigation $navigation
* @param int $flags
*/
public function __construct(Navigation $navigation, $flags = 0)
{
$this->iterator = $navigation->getIterator();
$this->navigation = $navigation;
$this->flags = $flags;
}
/**
* {@inheritdoc}
*/
public function getChildren()
{
return new static($this->current()->getChildren(), $this->flags & static::NAV_DISABLE);
}
/**
* {@inheritdoc}
*/
public function hasChildren()
{
return $this->current()->hasChildren();
}
/**
* {@inheritdoc}
* @return \Icinga\Web\Navigation\NavigationItem
*/
public function current()
{
return $this->iterator->current();
}
/**
* {@inheritdoc}
*/
public function key()
{
return $this->iterator->key();
}
/**
* {@inheritdoc}
*/
public function next()
{
$this->iterator->next();
}
/**
* {@inheritdoc}
*/
public function rewind()
{
$this->iterator->rewind();
if (! (bool) ($this->flags & static::NAV_DISABLE) && ! $this->inIteration) {
$this->content[] = $this->beginMarkup();
$this->inIteration = true;
}
}
/**
* {@inheritdoc}
*/
public function valid()
{
$valid = $this->iterator->valid();
if (! (bool) ($this->flags & static::NAV_DISABLE) && ! $valid && $this->inIteration) {
$this->content[] = $this->endMarkup();
$this->inIteration = false;
}
return $valid;
}
/**
* Begin navigation markup
*
* @return string
*/
public function beginMarkup()
{
$content = array();
if ($this->flags & static::NAV_MAJOR) {
$el = 'nav';
} else {
$el = 'div';
}
if (($elCssClass = $this->getCssClass()) !== null) {
$elCss = ' class="' . $elCssClass . '"';
} else {
$elCss = '';
}
$content[] = sprintf(
'<%s%s role="navigation">',
$el,
$elCss
);
$content[] = sprintf(
'<h%1$d class="sr-only" tabindex="-1">%2$s</h%1$d>',
static::HEADING_RANK,
$this->getView()->escape($this->getHeading())
);
$content[] = $this->beginChildrenMarkup();
return implode("\n", $content);
}
/**
* End navigation markup
*
* @return string
*/
public function endMarkup()
{
$content = array();
$content[] = $this->endChildrenMarkup();
if ($this->flags & static::NAV_MAJOR) {
$content[] = '</nav>';
} else {
$content[] = '</div>';
}
return implode("\n", $content);
}
/**
* Begin children markup
*
* @return string
*/
public function beginChildrenMarkup()
{
$ulCssClass = static::CSS_CLASS_NAV;
if ($this->navigation->getLayout() & Navigation::LAYOUT_TABS) {
$ulCssClass .= ' ' . static::CSS_CLASS_NAV_TABS;
}
if ($this->navigation->getLayout() & Navigation::LAYOUT_DROPDOWN) {
$ulCssClass .= ' ' . static::CSS_CLASS_NAV_DROPDOWN;
}
return '<ul class="' . $ulCssClass . '">';
}
/**
* End children markup
*
* @return string
*/
public function endChildrenMarkup()
{
return '</ul>';
}
/**
* Begin item markup
*
* @param NavigationItem $item
*
* @return string
*/
public function beginItemMarkup(NavigationItem $item)
{
$cssClass = array();
if ($item->getActive()) {
$cssClass[] = static::CSS_CLASS_ACTIVE;
}
if ($item->hasChildren()
&& $item->getChildren()->getLayout() === Navigation::LAYOUT_DROPDOWN
) {
$cssClass[] = static::CSS_CLASS_DROPDOWN;
$item
->setAttribute('class', static::CSS_CLASS_DROPDOWN_TOGGLE)
->setIcon(static::DROPDOWN_TOGGLE_ICON)
->setUrl('#');
}
if (! empty($cssClass)) {
$content = sprintf('<li class="%s">', implode(' ', $cssClass));
} else {
$content = '<li>';
}
return $content;
}
/**
* End item markup
*
* @return string
*/
public function endItemMarkup()
{
return '</li>';
}
/**
* {@inheritdoc}
*/
public function getCssClass()
{
return $this->cssClass;
}
/**
* {@inheritdoc}
*/
public function setCssClass($class)
{
$this->cssClass = trim((string) $class);
return $this;
}
/**
* {@inheritdoc}
*/
public function getHeading()
{
return $this->heading;
}
/**
* {@inheritdoc}
*/
public function setHeading($heading)
{
$this->heading = (string) $heading;
return $this;
}
/**
* Get the view
*
* @return View
*/
public function getView()
{
if ($this->view === null) {
$this->view = Icinga::app()->getViewRenderer()->view;
}
return $this->view;
}
/**
* Set the view
*
* @param View $view
*
* @return $this
*/
public function setView(View $view)
{
$this->view = $view;
return $this;
}
/**
* {@inheritdoc}
*/
public function render()
{
foreach ($this as $navigationItem) {
/** @var \Icinga\Web\Navigation\NavigationItem $navigationItem */
$this->content[] = $this->beginItemMarkup($navigationItem);
$this->content[] = $navigationItem->render();
$this->content[] = $this->endItemMarkup();
}
return implode("\n", $this->content);
}
}

View File

@ -0,0 +1,122 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation;
use Icinga\Web\View;
/**
* Interface for navigation renderer
*/
interface NavigationRendererInterface
{
/**
* CSS class for active items
*
* @var string
*/
const CSS_CLASS_ACTIVE = 'active';
/**
* CSS class for dropdown items
*
* @var string
*/
const CSS_CLASS_DROPDOWN = 'dropdown';
/**
* CSS class for dropdown's trigger
*/
const CSS_CLASS_DROPDOWN_TOGGLE = 'dropdown-toggle';
/**
* CSS class for the ul element
*
* @var string
*/
const CSS_CLASS_NAV = 'nav';
/**
* CSS class for the ul element w/ dropdown layout
*
* @var string
*/
const CSS_CLASS_NAV_DROPDOWN = 'dropdown-menu';
/**
* CSS class for the ul element w/ tabs layout
*
* @var string
*/
const CSS_CLASS_NAV_TABS = 'nav-tabs';
/**
* Icon for the dropdown's trigger
*
* @var string
*/
const DROPDOWN_TOGGLE_ICON = 'menu';
/**
* Heading rank
*
* @var int
*/
const HEADING_RANK = 2;
/**
* Flag for major navigation
*
* With this flag the outer navigation element will be nav instead of div
*
* @var int
*/
const NAV_MAJOR = 1;
/**
* Flag for disabling the outer navigation element
*
* @var int
*/
const NAV_DISABLE = 2;
/**
* Get the CSS class for the outer element
*
* @return string|null
*/
public function getCssClass();
/**
* Set the CSS class for the outer element
*
* @param string $class
*
* @return $this
*/
public function setCssClass($class);
/**
* Get the heading
*
* @return string
*/
public function getHeading();
/**
* Set the heading
*
* @param string $heading
*
* @return $this
*/
public function setHeading($heading);
/**
* Render navigation to HTML
*
* @return string
*/
public function render();
}

View File

@ -0,0 +1,118 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Navigation;
use RecursiveIteratorIterator;
use Icinga\Web\View;
/**
* Renderer for multi level navigation
*
* @method NavigationRenderer getInnerIterator() {
* {@inheritdoc}
* }
*/
class RecursiveNavigationRenderer extends RecursiveIteratorIterator implements NavigationRendererInterface
{
/**
* Content to render
*
* @var array
*/
private $content = array();
/**
* Create a new recursive navigation renderer
*
* @param Navigation $navigation
* @param int $flags
*/
public function __construct(Navigation $navigation, $flags = 0)
{
$navigationRenderer = new NavigationRenderer($navigation, $flags & static::NAV_DISABLE);
parent::__construct($navigationRenderer, RecursiveIteratorIterator::SELF_FIRST);
}
/**
* {@inheritdoc}
*/
public function beginIteration()
{
$this->content[] = $this->getInnerIterator()->beginMarkup();
}
/**
* {@inheritdoc}
*/
public function endIteration()
{
$this->content[] = $this->getInnerIterator()->endMarkup();
}
/**
* {@inheritdoc}
*/
public function beginChildren()
{
$this->content[] = $this->getInnerIterator()->beginChildrenMarkup();
}
/**
* {@inheritdoc}
*/
public function endChildren()
{
$this->content[] = $this->getInnerIterator()->endChildrenMarkup();
}
/**
* {@inheritdoc}
*/
public function getCssClass()
{
return $this->getInnerIterator()->getCssClass();
}
/**
* {@inheritdoc}
*/
public function setCssClass($class)
{
$this->getInnerIterator()->setCssClass($class);
return $this;
}
/**
* {@inheritdoc}
*/
public function getHeading()
{
return $this->getInnerIterator()->getHeading();
}
/**
* {@inheritdoc}
*/
public function setHeading($heading)
{
$this->getInnerIterator()->setHeading($heading);
return $this;
}
/**
* {@inheritdoc}
*/
public function render()
{
foreach ($this as $navigationItem) {
/** @var \Icinga\Web\Navigation\NavigationItem $navigationItem */
$this->content[] = $this->getInnerIterator()->beginItemMarkup($navigationItem);
$this->content[] = $navigationItem->render();
if (! $navigationItem->hasChildren()) {
$this->content[] = $this->getInnerIterator()->endItemMarkup();
}
}
return implode("\n", $this->content);
}
}