src/Item.php
<?php
/*
* This file is part of Laravel Navigation Builder.
*
* (c) Rocco Howard <rocco@hnh.digital>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace HnhDigital\NavigationBuilder;
use HnhDigital\LaravelHtmlGenerator\Html;
use HnhDigital\PhpNumberConverter\NumberConverter;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
/**
* @method html(string $template, ...$replacements)
*/
class Item
{
/**
* No link specified.
*
* @var string
*/
const LINK_EMPTY = 'empty';
/**
* Link action.
*
* @var string
*/
const LINK_ACTION = 'action';
/**
* Link route.
*
* @var string
*/
const LINK_ROUTE = 'route';
/**
* Link url.
*
* @var string
*/
const LINK_URL = 'url';
/**
* Link url.
*
* @var string
*/
const LINK_INSECURE_URL = 'insecure_url';
/**
* Link external url.
*
* @var string
*/
const LINK_EXTERNAL_URL = 'external_url';
/**
* Link type (default used is empty).
*
* @var string
*/
private $link_type = self::LINK_EMPTY;
/**
* Unique ID for this item.
*
* @var string
*/
public $id = '';
/**
* Object reference to the parent.
*
* @var \Item
*/
public $parent;
/**
* Reference to the parent.
*
* @var string
*/
public $parent_id = '';
/**
* Item has authorization.
*
* @var string
*/
public $authorized = true;
/**
* Object reference to the menu.
*
* @var \HnhDigital\NavigationBuilder\Menu
*/
private $menu;
/**
* Item data.
*
* - title
* - nickname
* - active
*
* @var array
*/
private $data = [];
/**
* Item options.
*
* - open_new_window (setOpenNewWindowOption)
* - hide_if_not_active (setHideIfNotActiveOption)
* - hide_if_active (setHideIfActiveOption)
* - show_in_breadcrumb_if_active (setShowInBreadcrumbIfActiveOption)
*
* @var array
*/
private $option = [];
/**
* Child container attributes.
*
* @var array
*/
private $container_attribute = [];
/**
* Item attributes.
*
* @var array
*/
private $item_attribute = [];
/**
* Link attributes.
*
* @var array
*/
private $link_attribute = [];
/**
* Initializing the menu item.
*
* @param Menu $menu
* @param string $title
* @return void
*/
public function __construct($menu, $title)
{
$this->menu = $menu;
$this->title = $title;
$this->id = uniqid(rand());
}
/**
* Add a menu item as a child.
*
* @param string $title
* @return Item
*/
public function add($title)
{
$item = $this->menu->addItem($title);
$item->parent_id = $this->id;
$item->parent = $this;
return $item;
}
/**
* Checks if the item has any children.
*
* @return bool
*/
public function hasChildren()
{
return count($this->menu->whereParentId($this->id)->all()) or false;
}
/**
* Returns children of the item.
*
* @param bool $depth
* @return \HnhDigital\NavigationBuilder\Collection
*/
public function children($depth = false)
{
return $this->menu->whereParentId($this->id, $depth)->all();
}
/**
* Returns the parent of this item.
*
* @return Item
*/
public function parent()
{
return $this->parent;
}
/**
* Modify an attribute on the item.
* Alias for addItemAttribute($name, $value).
*
* @param string $name
* @param string $value
* @param string $action
* @return Item
*/
public function item($name, $value, $action = 'add')
{
$method_name = $action.'ItemAttribute';
$this->$method_name($name, $value);
return $this;
}
/**
* Modify an attribute on the link.
* Alias for addLinkAttribute($name, $value).
*
* @param string $name
* @param string $value
* @param string $action
* @return Item
*/
public function link($name, $value, $action = 'add')
{
$method_name = $action.'LinkAttribute';
$this->$method_name($name, $value);
return $this;
}
/**
* Set the item to be a action.
*
* @param string $route_name
* @param array $parameters
* @return Item
*/
public function action($name, ...$parameters)
{
$this->link_type = self::LINK_ACTION;
$this->link_value = [$name, $parameters];
$this->checkActive();
return $this;
}
/**
* Set the item to be a route.
*
* @param string $name
* @param array $parameters
* @return Item
*/
public function route($name, $parameters = [])
{
$this->link_type = self::LINK_ROUTE;
$this->link_value = [$name, $parameters];
$this->checkActive();
return $this;
}
/**
* Set the item to be a url.
*
* @param string $url
* @param array $parameters
* @return Item
*/
public function url($url, ...$parameters)
{
$this->link_type = self::LINK_URL;
$this->link_value = [$url, $parameters];
$this->checkActive();
return $this;
}
/**
* Set the item to be a insecure url.
*
* @param string $url
* @param array $parameters
* @return Item
*/
public function insecureUrl($url, ...$parameters)
{
$this->link_type = self::LINK_INSECURE_URL;
$this->link_value = [$url, $parameters];
$this->checkActive();
return $this;
}
/**
* Set the item be an external url.
*
* @param string $url
* @return Item
*/
public function externalUrl($url)
{
$this->link_type = self::LINK_EXTERNAL_URL;
$this->link_value = [$url];
$this->setOpenNewWindowOption();
$this->setActive(false);
return $this;
}
/**
* Check and activate or deactivate.
*
* @return Item
*/
private function checkActive($update_parents = true)
{
$this->setActive($this->generateUrl() == \Request::url(), $update_parents);
return $this;
}
/**
* Set the title.
*
* @param string $value
* @return Item
*/
public function setTitle($value)
{
$current_title = Arr::get($this->data, 'title', '');
$this->data['title'] = $value;
if (Arr::get($this->data, 'nickname', '') == $current_title) {
$this->nickname = $value;
}
return $this;
}
/**
* Set the nickname.
*
* @param string $value
* @return Item
*/
public function setNickname($value)
{
$this->data['nickname'] = strtolower(Str::ascii($value));
return $this;
}
/**
* Get the nickname.
*
* @param string $value
* @return Item
*/
public function getNickname()
{
return strtolower(Str::ascii(Arr::get($this->data, 'nickname', '')));
}
/**
* Get the menu for this item.
*
* @return Menu
*/
public function getMenu()
{
return $this->menu;
}
/**
* Set this item active.
*
* @param bool $active
* @return Item
*/
public function setActive($active = true, $update_parents = true)
{
$this->data['active'] = $active;
$method_name = $active ? 'add' : 'remove';
$method_name .= $this->getActiveOnLinkOption() ? 'Link' : 'Item';
$method_name .= 'Attribute';
$this->$method_name('class', 'active');
// Activate parents.
if ($update_parents && ! is_null($this->parent) && $active) {
$this->parent()->setActive($active);
}
return $this;
}
/**
* Check the html content for sprintf template before allocation.
*
* @param string $template
* @param array $replacements
* @return Item
*/
public function setHtml($template, ...$replacements)
{
$this->data['html'] = count($replacements) ? sprintf($template, ...$replacements) : $template;
return $this;
}
/**
* Generate the url for this item.
*
* @return string
*/
private function generateUrl()
{
$url = '';
// Create the URL.
switch ($this->link_type) {
case self::LINK_ACTION:
$url = action(...$this->link_value);
break;
case self::LINK_ROUTE:
$url = route(...$this->link_value);
break;
case self::LINK_URL:
$url = env('APP_NO_SSL', true) ? url(...$this->link_value) : secure_url(...$this->link_value);
break;
case self::LINK_INSECURE_URL:
$url = url(...$this->link_value);
break;
case self::LINK_EXTERNAL_URL:
$url = stripos($this->link_value[0], 'http') === false ? 'http://' : '';
$url .= $this->link_value[0];
break;
}
return $url;
}
/**
* Activate if listed is item is active.
*
* @param Item $item
* @return bool
*/
public static function activateIfItemIsActive($item)
{
// No menu items provided.
if (empty($list = $item->getActiveIfItemIsActiveOption())) {
return false;
}
// Only a string value has been provided.
if (! is_array($list)) {
$list = [$list];
}
// Move all local menu items to a self key.
// We can have another navigation menu list provided.
$list['self'] = [];
foreach ($list as $key => $value) {
if (is_int($key)) {
$list['self'][] = $value;
unset($list[$key]);
}
}
// Check until we find an active menu item.
foreach ($list as $menu_name => $items) {
// Check this navigation menu (self), or access the provided navigation menu.
$menu = $menu_name == 'self' ? $item->getMenu() : app('Nav')->menu($menu_name);
if (! is_iterable($items)) {
continue;
}
// Check each item.
foreach ($items as $nickname) {
// Get the menu item for this nickname.
$check_item = $menu->get($nickname);
// Item exists, get it to check if it is active, and then,
// if active, set this current item active.
if (! is_null($check_item) && $check_item->checkActive(false) && $check_item->getActive()) {
$item->setActive();
return true;
}
}
}
return false;
}
/**
* Check if listed items are active.
*
* @param Item $item
* @return bool
*/
public function checkItemIsActive($item)
{
if ($this->getActive()) {
return true;
}
if (empty($item_list = $item->getHideIfItemNotActiveOption())) {
return true;
}
if (! is_array($item_list)) {
$item_list = [$item_list];
}
foreach ($item_list as $nickname) {
$check_item = $item->getMenu()->get($nickname);
if (! is_null($check_item) && $check_item->getActive()) {
return true;
}
}
return false;
}
/**
* Add menu as a dropdown.
*
* @param string|array $menu_source
* @return Item
*/
public function makeDropdown($menu_source, $config = [])
{
if (! is_array($menu_source)) {
$menu_source = [$menu_source];
}
$this->setDropdownSourceOption($menu_source)
->setDropdownConfigOption($config);
return $this;
}
/**
* Render dropdown.
*
* @return Item
*/
public function renderDropdown()
{
$menu_source = Arr::get($this->option, 'dropdown_source', null);
$config = Arr::get($this->option, 'dropdown_config', null);
if (is_null($menu_source)) {
return $this;
}
$menu_container = '';
$item_callback = Arr::get($this->option, 'item_callback', null);
foreach ($menu_source as $menu_name) {
$menu = app('Nav')->get($menu_name);
if (is_null($menu) || empty($menu)) {
continue;
}
$menu_container .= $menu->setItemCallbackOption(function (&$item) use ($item_callback) {
$item->addLinkAttribute('class', 'dropdown-item')
->setItemTagOption('div');
if (! is_null($item_callback) && is_callable($item_callback)) {
$item_callback($item);
$item->setItemCallbackOption($item_callback);
}
})->render('');
}
if ($menu_container === '') {
$this->setActive(false)
->addItemAttribute('class', 'hidden');
return $this;
}
$menu_container = Html::div($menu_container)
->addClass(implode(' ', array_merge(['dropdown-menu'], Arr::get($config, 'container.class', []))));
$this->setAfterTagOption($menu_container)
->addItemAttribute('class', 'dropdown')
->addLinkAttribute('class', 'dropdown-toggle')
->addLinkAttribute('data-toggle', 'dropdown');
return $this;
}
/**
* Check if user can use this menu item.
*
* @return Item
*/
public function can($ability, $model, $user = false)
{
if ($user === false) {
$user = auth()->user();
}
$this->authorized = $user->can($ability, $model, $user);
return $this;
}
/**
* Render this item.
*
* @return string
*/
public function render($menu_level = 0)
{
static::activateIfItemIsActive($this);
// Not authorized for this menu.
if (! $this->authorized) {
return '';
}
// Item or parent is not active.
if (! $this->checkItemIsActive($this)) {
return '';
}
// Hide this menu if not active.
if ($this->getHideIfNotActiveOption()) {
return '';
}
// Item is not directly active and marked to hide if not active.
if (! $this->getActive() && $this->getHideIfNotActiveOption()) {
return '';
}
// Item is directly active and marked to hide if active.
if ($this->getActive() && $this->getHideIfItemActiveOption()) {
return '';
}
// Render dropdown if has been requested.
$this->renderDropdown();
// Available options for this item.
$container_tag = Arr::get($this->option, 'container_tag', 'ul');
$container_class = Arr::get($this->option, 'container_class', 'nav');
$item_tag = Arr::get($this->option, 'item_tag', 'li');
$text_only = Arr::get($this->option, 'text_only', false);
$hide_children = Arr::get($this->option, 'hide_children', false);
$force_inactive = Arr::get($this->option, 'force_inactive', false);
$before_tag_html = Arr::get($this->option, 'before_tag', '');
$after_tag_html = Arr::get($this->option, 'after_tag', '');
$no_title = Arr::get($this->option, 'no_title', '');
$html = (! $text_only && $this->html != '') ? $this->html : $this->title;
// Force the menu items to not show active.
if ($force_inactive) {
$this->setActive(false);
}
// Link is not empty.
if ($this->link_type !== self::LINK_EMPTY) {
// Create the link.
$html_link = Html::a()->text($html)
->openNew(! $this->getOpenNewWindowOption())
->href($this->generateUrl());
if (! $no_title) {
$html_link->title($this->title);
}
if (! $text_only) {
$html_link->addAttributes($this->link_attribute);
}
$html = $html_link->s();
} elseif (! empty($this->title)) {
$html = Html::span($html);
if (! $no_title) {
$html->title($this->title);
}
if (! $text_only) {
$html->addAttributes($this->link_attribute);
}
}
// Generate each of the children items.
if (! $hide_children && $this->hasChildren()) {
$child_html = '';
// Grab the callback, we pass this down each child item.
$item_callback = Arr::get($this->option, 'item_callback', null);
// Generate each child menu item (repeat this method)
foreach ($this->children() as $item) {
$item->setItemTagOption($item_tag);
if (! is_null($item_callback) && is_callable($item_callback)) {
$item_callback($item);
$item->setItemCallbackOption($item_callback);
}
$child_html .= $item->render($menu_level + 1);
}
if ($child_html !== '') {
// Name the level
$number_as_word = (new NumberConverter())->ordinal($menu_level);
// Generate the list container
$html_container = Html::$container_tag($child_html)
->addAttributes($this->container_attribute)
->addClass($container_class)
->addClass(sprintf('%s-%s-level', $container_class, $number_as_word));
$html .= $html_container->s();
}
}
if ($this->generateUrl() == \Request::url()
&& ! $this->hasChildren()) {
$this->addItemAttribute('class', 'actual-link');
}
// Create the container and allocate the link.
return Html::$item_tag($before_tag_html.$html.$after_tag_html)->addAttributes($this->item_attribute)->s();
}
/**
* Check if this item has the given property.
*
* @param string $property_name
* @return bool
*/
public function __isset($property_name)
{
return isset($this->data[$property_name]);
}
public function __call(string $name, array $arguments)
{
$original_method_name = Str::snake($name);
preg_match('/^([a-z]+)_([a-z_]+)_([a-z]+)$/', $original_method_name, $matches);
if (count($matches) === 4) {
[$original_method_name, $action, $key, $method_name] = $matches;
$allowed_actions = ['get', 'set', 'add', 'remove', 'append', 'prepend'];
$allowed_keys = ['item', 'link', 'container', 'link'];
$allowed_method_names = ['attribute', 'option'];
if (in_array($action, $allowed_actions)
&& in_array($method_name, $allowed_method_names)) {
// Get calls.
if ($action == 'get' || $action == 'set') {
$array_func = $action;
if (($key == 'item' || $key == 'link') && $method_name == 'attribute') {
$result = Arr::$array_func($this->{$key.'_'.$method_name}, Arr::get($arguments, 0, null), Arr::get($arguments, 1, null));
return $action == 'get' ? $result : $this;
}
if ($method_name == 'option') {
if (count($arguments) > 1) {
$data = $arguments;
} else {
$default = $action == 'get' ? false : true;
$data = Arr::get($arguments, 0, $default);
}
$result = Arr::$array_func($this->option, $key, $data);
return $action == 'get' ? $result : $this;
}
$name = $method_name;
}
// Manipulate values.
if (($action == 'add' || $action == 'remove' || $action == 'append' || $action == 'prepend')
&& ($key == 'item' || $key == 'container' || $key == 'link') && $method_name == 'attribute') {
$input_value = Arr::get($arguments, 1, '');
$current_value = Arr::get($this->{$key.'_'.$method_name}, Arr::get($arguments, 0, null), '');
// Class attributes
if ($arguments[0] == 'class') {
// Convert string to array, trim input and remove possible duplicates.
$current_value_array = explode(' ', $current_value);
$input_value = trim($input_value);
$current_value_array = array_unique($current_value_array);
// Remove class from list
if ($action == 'remove') {
if (($index = array_search($input_value, $current_value_array)) !== false) {
unset($current_value_array[$index]);
}
// Add class to list
} elseif ($action != 'remove') {
$current_value_array[] = $input_value;
}
// Remove duplicates, sort and assign string value.
$current_value_array = array_unique($current_value_array);
sort($current_value_array);
$current_value = trim(implode(' ', $current_value_array));
// Other attributes
} elseif ($arguments[0] != 'class') {
if ($action == 'remove') {
$current_value = str_replace($input_value, '', $current_value);
}
switch ($action) {
case 'add':
case 'append':
$current_value .= $input_value;
break;
case 'prepend':
$current_value = $input_value.$current_value;
break;
}
$current_value = trim($current_value);
}
if (strlen($current_value)) {
Arr::set($this->{$key.'_'.$method_name}, Arr::get($arguments, 0, null), $current_value);
} else {
unset($this->{$key.'_'.$method_name}[Arr::get($arguments, 0, null)]);
}
return $this;
}
}
} else {
[$action, $method_name, $key] = array_pad(explode('_', $original_method_name, 3), 3, '');
}
$name = $method_name;
// Use the magic get/set instead
if (count($arguments) == 0) {
return $this->$name;
}
if (method_exists($this, 'set'.Str::studly($action))) {
$this->{'set'.Str::studly($action)}(...$arguments);
return $this;
}
if (isset($this->$name)) {
$this->$name = Arr::get($arguments, 0, '');
return $this;
}
$this->data[$name] = Arr::get($arguments, 0, '');
return $this;
}
/**
* Set a data value by a name.
*
* @param string $name
* @param string $value
* @return void
*/
public function data($name, $value)
{
$this->data[$name] = $value;
return $this;
}
/**
* Set a data value by a name.
*
* @param string $name
* @param string $value
* @return void
*/
public function __set($name, $value)
{
$name = Str::snake($name);
$set_method = 'set'.Str::studly($name);
if (method_exists($this, $set_method)) {
$this->$set_method($value);
return $this;
}
$this->data[$name] = $value;
return $this;
}
/**
* Return the value of data by name.
*
* @param string $name
* @return mixed
*/
public function __get(string $name)
{
$name = Str::snake($name);
$get_method = 'get'.Str::studly($name);
if (method_exists($this, $get_method)) {
return $this->$get_method();
}
return Arr::get($this->data, $name, '');
}
}