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; public array $items = []; private array $classes = []; protected array $defaultMenuClasses = []; private bool $isNav = true; private bool $hasToggle = false; protected array $defaultItemClasses = []; private int $counter = 0; private bool $isDrawer = false; private bool $drawerCollapsed = true; 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 = ''; } if ($return) { return $html; } echo $html; return $html; } /** * Configure as a drawer-style menu * * @param bool $collapsed Initial state * @return self */ public function asDrawer(bool $collapsed = true): self { $this->isDrawer = true; $this->drawerCollapsed = $collapsed; $this->addClass('drawer'); if (!$collapsed) { $this->addClass('open'); } return $this; } public function isDrawer(): bool { return $this->isDrawer; } /** * Add a section header * * @param string $title * @return MenuSection */ public function addSection(string $title): MenuSection { $section = new MenuSection($title, ++$this->counter); $this->items[] = $section; if (!empty($this->defaultItemClasses)) { foreach ($this->defaultItemClasses as $class) { $section->addItemClass($class); } } return $section; } /** * Populate menu from array structure * * @param array $items Array of menu items * @return self */ public function populateFromArray(array $items): self { foreach ($items as $item) { // Handle sections if (!empty($item['section'])) { $section = $this->addSection($item['section']); if (!empty($item['items'])) { $this->populateSection($section, $item['items']); } continue; } // Handle regular items $menuItem = $this->addItem($item['text'] ?? '', $item['icon'] ?? ''); if (!empty($item['class'])) { $menuItem->addClass($item['class']); } if (!empty($item['url'])) { $menuItem->url($item['url']); } if (!empty($item['submenu'])) { $submenu = $menuItem->submenu(); $submenu->populateFromArray($item['submenu']); } } return $this; } private function populateSection(MenuSection $section, array $items): void { foreach ($items as $item) { $menuItem = $section->addItem($item['text'] ?? '', $item['icon'] ?? null); if (!empty($item['url'])) { $menuItem->url($item['url']); } if (!empty($item['submenu'])) { $submenu = $menuItem->submenu(); $submenu->populateFromArray($item['submenu']); } } } } /** * 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, '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 { if (!$this->url && (!$this->submenu || empty($this->submenu->items))) { return ''; } $classes = $this->classes; if ($this->submenu) { $classes[] = 'has-submenu'; } $classStr = $this->renderClasses($classes); $html = ''; $html .= '
'; // Render link or button if ($this->url) { $attrs = ''; foreach ($this->attributes as $key => $value) { $attrs .= ' ' . esc_attr($key) . '="' . esc_attr($value) . '"'; } $html .= ''; } else { $html .= ''; } if ($this->icon) { $html .= $this->icon; } $html .= ''.esc_html($this->text) . ''; $html .= ($this->url) ? '' : ''; // Render submenu if exists if ($this->submenu) { $html .= ''; $html .= '
'; $html .= $this->submenu->render(); }else { $html .= ''; } $html .= ''; return $html; } } /** * Menu section with header and items */ class MenuSection { private int $id; private string $title; private array $items = []; private array $classes = []; private array $defaultItemClasses = []; private int $counter = 0; public function __construct(string $title, int $id) { $this->title = $title; $this->id = $id; } 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; } public function addClass(string $class): self { $this->classes[] = $class; return $this; } public function addItemClass(string $class): self { $this->defaultItemClasses[] = $class; return $this; } public function render(): string { if (empty($this->items)) { return ''; } $classStr = !empty($this->classes) ? ' class="menu-section ' . esc_attr(implode(' ', $this->classes)) . '"' : ' class="menu-section"'; $html = ''; $html .= '' . esc_html($this->title) . ''; $html .= '
    '; foreach ($this->items as $item) { $html .= $item->render(); } $html .= '
'; return $html; } }