<?php
|
namespace JVBase\ui;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Menu/Navigation UI component with fluent interface
|
*
|
* Usage:
|
* $menu = new Menu('primary-nav');
|
* $menu->addItem()->text('Home')->url('/')->icon('house');
|
*
|
* $menu->addItem()->text('About')
|
* ->url('/about/')
|
* ->submenu(function($submenu) {
|
* $submenu->addItem()->text('Team')->url('/about/team/');
|
* $submenu->addItem()->text('History')->url('/about/history/');
|
* });
|
*
|
* echo $menu->render();
|
*/
|
class Navigation {
|
private string $id;
|
private array $items = [];
|
private array $classes = [];
|
protected array $defaultMenuClasses = [];
|
private bool $isNav = true;
|
private bool $hasToggle = false;
|
protected array $defaultItemClasses = [];
|
private int $counter = 0;
|
|
public function __construct(string $id = '') {
|
$this->id = $id ?: 'menu-' . uniqid();
|
}
|
|
public function getID():string
|
{
|
return $this->id;
|
}
|
|
|
/**
|
* Add a menu item
|
*
|
* @return MenuItem
|
*/
|
public function addItem(?string $text = null, ?string $icon = null): MenuItem {
|
$item = new MenuItem(++$this->counter);
|
$this->items[] = $item;
|
if ($text) {
|
$item->text($text);
|
}
|
if ($icon) {
|
$item->icon($icon);
|
}
|
if (!empty($this->defaultItemClasses)) {
|
foreach ($this->defaultItemClasses as $class) {
|
$item->addClass($class);
|
}
|
}
|
return $item;
|
}
|
|
/**
|
* Add CSS class to the nav element
|
*
|
* @param string $class
|
* @return self
|
*/
|
public function addClass(string $class): self {
|
$this->classes[] = $class;
|
return $this;
|
}
|
public function addMenuClass(string $class):self {
|
$this->menuClasses[] = $class;
|
return $this;
|
}
|
|
public function defaultMenuClasses(array $classes):self {
|
$classes = array_filter($classes, fn ($class) => is_string($class));
|
$this->defaultMenuClasses = $classes;
|
return $this;
|
}
|
|
public function defaultItemClasses(array $classes): self {
|
$classes = array_filter($classes, fn ($class) => is_string($class));
|
$this->defaultItemClasses = $classes;
|
return $this;
|
}
|
|
/**
|
* Set whether this nav has a toggle button
|
*
|
* @param bool $hasToggle
|
* @return self
|
*/
|
public function hasToggle(bool $hasToggle = true): self {
|
$this->hasToggle = $hasToggle;
|
return $this;
|
}
|
|
|
public function isNav(bool $isNav = true):self {
|
$this->isNav = $isNav;
|
return $this;
|
}
|
|
/**
|
* Render the menu HTML
|
*
|
* @param bool $return Whether to return or echo
|
* @return string
|
*/
|
public function render(bool $return = true): string {
|
if (empty($this->items)) {
|
return '';
|
}
|
|
$classStr = !empty($this->classes) ? ' class="' . esc_attr(implode(' ', array_merge([$this->id],$this->classes))) . '"' : '';
|
|
$html = '';
|
if ($this->isNav) {
|
$html = '<nav id="' . esc_attr($this->id).'"' . $classStr.'>';
|
|
if ($this->hasToggle) {
|
$html .= '<button class="toggle main" type="button" aria-expanded="false" aria-controls="' . esc_attr($this->id) . '">
|
' . jvbIcon('list') . '
|
<span class="screen-reader-text">Toggle Menu</span>
|
</button>';
|
}
|
}
|
if (!$this->isNav) {
|
$classStr = (empty($this->defaultMenuClasses)) ? '' : ' class="'.implode(' ', $this->defaultMenuClasses).'"';
|
}
|
|
$html .= '<ul id="' . esc_attr($this->id) . '-list" '.$classStr.'>';
|
|
foreach ($this->items as $item) {
|
$html .= $item->render();
|
}
|
|
$html .= '</ul>';
|
if ($this->isNav) {
|
$html .= '</nav>';
|
}
|
|
|
if ($return) {
|
return $html;
|
}
|
|
echo $html;
|
return $html;
|
}
|
}
|
|
/**
|
* Individual menu item with support for submenus
|
*/
|
class MenuItem {
|
private int $id;
|
private string $text = '';
|
private ?string $url = null;
|
private ?string $icon = null;
|
private ?Navigation $submenu = null;
|
private array $classes = [];
|
private array $menuClasses = [];
|
private array $attributes = [];
|
private bool $current = false;
|
|
public function __construct(int $id) {
|
$this->id = $id;
|
}
|
|
/**
|
* Set the menu item text
|
*
|
* @param string $text
|
* @return self
|
*/
|
public function text(string $text): self {
|
$this->text = $text;
|
return $this;
|
}
|
|
/**
|
* Set the menu item URL
|
*
|
* @param string $url
|
* @return self
|
*/
|
public function url(string $url): self {
|
$this->url = $url;
|
return $this;
|
}
|
|
/**
|
* Set the menu item icon
|
*
|
* @param string $icon
|
* @return self
|
*/
|
public function icon(string $icon): self {
|
$this->icon = (str_starts_with($icon, '<i')) ? $icon : jvbIcon($icon);
|
return $this;
|
}
|
|
protected function renderClasses(array $classes):string {
|
return empty($classes) ? '' : ' class="'.implode(' ', array_filter($classes, fn($class) => is_string($class))).'"';
|
}
|
|
/**
|
* Add a submenu
|
*
|
* @param ?string $id
|
* @return Navigation
|
*/
|
public function submenu(?string $id = null): Navigation {
|
if (!$id) {
|
$id = 'submenu-' . uniqid();
|
}
|
$submenu = new Navigation($id);
|
$submenu->isNav(false);
|
|
if (!empty($this->defaultMenuClasses)) {
|
foreach ($this->defaultMenuClasses as $class) {
|
$submenu->addClass($class);
|
}
|
}
|
$this->submenu = $submenu;
|
return $submenu;
|
}
|
|
/**
|
* Add CSS class to the list item
|
*
|
* @param string $class
|
* @return self
|
*/
|
public function addClass(string $class): self {
|
$this->classes[] = $class;
|
return $this;
|
}
|
|
/**
|
* Mark this item as current/active
|
*
|
* @param bool $current
|
* @return self
|
*/
|
public function current(bool $current = true): self {
|
$this->current = $current;
|
if ($current) {
|
$this->addClass('current');
|
}
|
return $this;
|
}
|
|
/**
|
* Add custom attribute to the link element
|
*
|
* @param string $key
|
* @param string $value
|
* @return self
|
*/
|
public function attribute(string $key, string $value): self {
|
$this->attributes[$key] = $value;
|
return $this;
|
}
|
|
/**
|
* Render the menu item HTML
|
*
|
* @return string
|
*/
|
public function render(): string {
|
$classes = $this->classes;
|
if ($this->submenu) {
|
$classes[] = 'has-submenu';
|
}
|
|
$classStr = $this->renderClasses($classes);
|
|
$html = '<li' . $classStr . '>';
|
$html .= '<div class="row nowrap">';
|
// Render link or button
|
if ($this->url) {
|
$attrs = '';
|
foreach ($this->attributes as $key => $value) {
|
$attrs .= ' ' . esc_attr($key) . '="' . esc_attr($value) . '"';
|
}
|
|
$html .= '<a href="' . esc_url($this->url) . '"' . $attrs . '>';
|
} else {
|
$html .= '<span class="a">';
|
}
|
|
if ($this->icon) {
|
$html .= $this->icon;
|
}
|
$html .= '<span class="title">'.esc_html($this->text) . '</span>';
|
|
|
$html .= ($this->url) ? '</a>' : '</span>';
|
|
// Render submenu if exists
|
if ($this->submenu) {
|
$html .= '<button class="toggle"
|
data-action="toggle-submenu"
|
title="Toggle Submenu"
|
aria-label="Open '.$this->submenu->getID().' Submenu" aria-expanded="false" aria-controls="'.$this->submenu->getID().'">'.
|
jvbIcon('caret-down', ['title'=>'Toggle Submenu']).
|
'</button>';
|
$html .= '</div>';
|
$html .= $this->submenu->render();
|
}else {
|
$html .= '</div>';
|
}
|
|
$html .= '</li>';
|
|
return $html;
|
}
|
}
|