Jake Vanderwerf
2026-05-31 97e7c319d656a5f05489ca996e249e7359303d4d
inc/ui/Navigation.php
@@ -23,13 +23,15 @@
 */
class Navigation {
   private string $id;
   private array $items = [];
   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();
@@ -124,11 +126,19 @@
      if ($this->isNav) {
         $html = '<nav id="' . esc_attr($this->id).'"' . $classStr.'>';
         if ($this->hasToggle) {
         // Drawer toggle or regular toggle
         if ($this->isDrawer) {
            $html .= '<button class="toggle main" type="button"
                aria-expanded="' . ($this->drawerCollapsed ? 'false' : 'true') . '"
                aria-controls="' . esc_attr($this->id) . '-list">
                ' . jvbIcon('caret-left') . '
                <span class="screen-reader-text">Toggle Menu</span>
            </button>';
         } elseif ($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>';
                ' . jvbIcon('list') . '
                <span class="screen-reader-text">Toggle Menu</span>
            </button>';
         }
      }
      if (!$this->isNav) {
@@ -154,6 +164,96 @@
      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']);
         }
      }
   }
}
/**
@@ -276,6 +376,9 @@
    * @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';
@@ -324,3 +427,66 @@
      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 = '<li' . $classStr . '>';
      $html .= '<span class="section-title">' . esc_html($this->title) . '</span>';
      $html .= '<ul class="section-items">';
      foreach ($this->items as $item) {
         $html .= $item->render();
      }
      $html .= '</ul></li>';
      return $html;
   }
}