434 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			434 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nur\v\bs3;
 | |
| 
 | |
| use nur\A;
 | |
| use nur\b\UserException;
 | |
| use nur\b\ValueException;
 | |
| use nur\md;
 | |
| use nur\str;
 | |
| use nur\v\base\TagsManager;
 | |
| use nur\v\fo;
 | |
| use nur\v\model\IFormManager;
 | |
| use nur\v\model\INavbarManager;
 | |
| use nur\v\page;
 | |
| use nur\v\v;
 | |
| 
 | |
| class Bs3NavbarManager extends TagsManager implements INavbarManager {
 | |
|   const TYPE_FIXED = "fixed-top";
 | |
|   const TYPE_STATIC = "static-top";
 | |
|   const TYPE_NORMAL = "normal";
 | |
| 
 | |
|   static final function get_class_style(?string $navbar): array {
 | |
|     if ($navbar === null) $navbar = self::TYPE_NORMAL;
 | |
|     $class = false;
 | |
|     $style = false;
 | |
|     switch ($navbar) {
 | |
|     case "fixed-top":
 | |
|     case "fixed":
 | |
|     case "f":
 | |
|       $class = "navbar-fixed-top";
 | |
|       $style = '<style>body { margin-top: 70px; }</style>';
 | |
|       break;
 | |
|     case "static-top":
 | |
|     case "static":
 | |
|     case "s":
 | |
|       $class = "navbar-static-top";
 | |
|       break;
 | |
|     case "normal":
 | |
|     case "n":
 | |
|       break;
 | |
|     default:
 | |
|       throw new ValueException("$navbar: invalid navbar type");
 | |
|     }
 | |
|     return [$class, $style];
 | |
|   }
 | |
| 
 | |
|   private static function get_align_class($align): ?string {
 | |
|     if ($align === null) return null;
 | |
|     switch ($align) {
 | |
|     case "navbar-left":
 | |
|     case "align-left":
 | |
|     case "left":
 | |
|     case "l":
 | |
|       $align_class = "navbar-left";
 | |
|       break;
 | |
|     case "navbar-right":
 | |
|     case "align-right":
 | |
|     case "right":
 | |
|     case "r":
 | |
|       $align_class = "navbar-right";
 | |
|       break;
 | |
|     default:
 | |
|       throw new UserException("$align: invalid align");
 | |
|     }
 | |
|     return $align_class;
 | |
|   }
 | |
| 
 | |
|   const NAVBAR_OPTIONS_SCHEMA = [
 | |
|     "navbar" => ["?string", self::TYPE_NORMAL, "type de barre de navigation"],
 | |
|     "container" => ["?string", _container::CONTAINER_NORMAL, "type de container"],
 | |
|     "brand" => ["?string", null, "Marque"],
 | |
|     "show_brand" => [null, true, "Faut-il afficher la marque?"],
 | |
|     "class" => ["?string", null, "classe CSS"],
 | |
|   ];
 | |
| 
 | |
|   protected $formManager;
 | |
| 
 | |
|   function setFormManager(IFormManager $formManager): void {
 | |
|     $this->formManager = $formManager;
 | |
|   }
 | |
| 
 | |
|   function getFormManager(): IFormManager {
 | |
|     $formManager = $this->formManager;
 | |
|     if ($formManager !== null) return $formManager;
 | |
|     else return fo::manager();
 | |
|   }
 | |
| 
 | |
|   function start($options=null): array {
 | |
|     md::ensure_schema($options, self::NAVBAR_OPTIONS_SCHEMA);
 | |
| 
 | |
|     [$navbarClass, $navbarStyle] = self::get_class_style($options["navbar"]);
 | |
|     $class = $options["class"];
 | |
|     $containerClass = _container::get_class($options["container"]);
 | |
| 
 | |
|     $brand = $options["brand"];
 | |
|     $showBrand = $options["show_brand"];
 | |
|     if (!$brand) {
 | |
|       $brand = false;
 | |
|     } elseif ($showBrand && $showBrand !== "asis") {
 | |
|       $brand = v::span(["class" => "navbar-brand", $brand]);
 | |
|     }
 | |
| 
 | |
|     $start = [
 | |
|       $navbarStyle,
 | |
|       v::sdiv([
 | |
|         "class" => ["navbar navbar-default", $navbarClass, $class],
 | |
|         "role" => "navigation",
 | |
|       ]),
 | |
|       v::sdiv(["class" => $containerClass]),
 | |
|       v::div([
 | |
|         "class" => "navbar-header",
 | |
|         v::tag("button", [
 | |
|           "class" => "navbar-toggle",
 | |
|           "data-toggle" => "collapse",
 | |
|           "data-target" => ".navbar-collapse",
 | |
|           v::span(["class" => "sr-only", "Toggle navigation"]),
 | |
|           v::span(["class" => "icon-bar"]),
 | |
|           v::span(["class" => "icon-bar"]),
 | |
|           v::span(["class" => "icon-bar"]),
 | |
|         ]),
 | |
|         $brand,
 | |
|       ]),
 | |
|       v::sdiv(["class" => "navbar-collapse collapse"]),
 | |
|     ];
 | |
|     $end = [
 | |
|       v::ediv(),
 | |
|       v::ediv(),
 | |
|       v::ediv(),
 | |
|     ];
 | |
|     $content = $this->push("navbar", $start, $end);
 | |
|     #$this->getFormManager()->push(["type" => Bs3FormManager::TYPE_NAVBAR]);
 | |
|     return $content;
 | |
|   }
 | |
| 
 | |
|   function end(): array {
 | |
|     $content = $this->pop("navbar");
 | |
|     #$this->getFormManager()->pop();
 | |
|     return $content;
 | |
|   }
 | |
| 
 | |
|   function started(): bool {
 | |
|     return $this->haveMark("navbar");
 | |
|   }
 | |
|   
 | |
|   const NAV_OPTIONS_SCHEMA = [
 | |
|     "type" => ["?string", "nav", "type de section"],
 | |
|     "align" => ["?string", "left", "alignement de la section"],
 | |
|     "class" => ["?string", null, "classe CSS"],
 | |
|   ];
 | |
| 
 | |
|   function startNav($options=null): array {
 | |
|     md::ensure_schema($options, self::NAV_OPTIONS_SCHEMA);
 | |
|     $type = $options["type"];
 | |
|     switch ($type) {
 | |
|     case "nav":
 | |
|     case "n":
 | |
|       $typeClass = "navbar-nav";
 | |
|       break;
 | |
|     case "form":
 | |
|     case "f":
 | |
|       $typeClass = "navbar-form";
 | |
|       break;
 | |
|     default:
 | |
|       throw new UserException("$type: invalid type");
 | |
|     }
 | |
|     $alignClass = self::get_align_class($options["align"]);
 | |
|     $class = $options["class"];
 | |
|     $start = v::sul([
 | |
|       "class" => ["nav", $typeClass, $alignClass, $class],
 | |
|     ]);
 | |
|     $end = v::eul();
 | |
| 
 | |
|     $content = null;
 | |
|     A::append_nn($content, $this->pop("nav"));
 | |
|     A::append_nn($content, $this->push("nav", $start, $end));
 | |
|     return $content;
 | |
|   }
 | |
| 
 | |
|   const NAV_ITEM_SCHEMA = [
 | |
|     "value" => [null, null, "la valeur de l'élément"],
 | |
|     "item" => ["string", "text", "type d'élément"],
 | |
|   ];
 | |
| 
 | |
|   const ITEM_BRAND = "brand";
 | |
|   const ITEM_TEXT = "text";
 | |
|   const ITEM_LINK = "link";
 | |
|   const ITEM_BUTTON = "button";
 | |
|   const ITEM_MENU = "menu";
 | |
|   const ITEM_LITERAL = "literal";
 | |
| 
 | |
|   function navItem($item): array {
 | |
|     md::ensure_schema($item, self::NAV_ITEM_SCHEMA);
 | |
|     $type = $item["item"];
 | |
|     $inNav = $this->haveMark("nav");
 | |
|     switch ($type) {
 | |
|     case self::ITEM_BRAND:
 | |
|       return $this->navBrand($item, $inNav);
 | |
|     case self::ITEM_TEXT:
 | |
|     case "t":
 | |
|       #$item["type"] = self::ITEM_TEXT;
 | |
|       return $this->navText($item, $inNav);
 | |
|     case self::ITEM_LINK:
 | |
|     case "l":
 | |
|       $item["type"] = self::ITEM_LINK;
 | |
|       return $this->navLink($item);
 | |
|     case self::ITEM_BUTTON:
 | |
|     case "b":
 | |
|       $item["type"] = self::ITEM_BUTTON;
 | |
|       if (!$inNav) return $this->navLink($item);
 | |
|       break;
 | |
|     case self::ITEM_MENU:
 | |
|     case "m":
 | |
|       #$item["type"] = self::ITEM_MENU;
 | |
|       return $this->navMenu($item, $inNav);
 | |
|     case self::ITEM_LITERAL:
 | |
|     case "asis":
 | |
|       #$item["type"] = self::ITEM_LITERAL;
 | |
|       return $item["value"];
 | |
|     }
 | |
|     throw new ValueException("$type: invalid item type");
 | |
|   }
 | |
| 
 | |
|   const NAV_BRAND_SCHEMA = [
 | |
|     "value" => [null, null, "la marque à afficher"],
 | |
|     "class" => ["?array", null, "classe CSS de l'élément"],
 | |
|     # class: type ?array pour le moment, à remplacer par "?items"
 | |
|     "style" => ["?string", null, "style CSS de l'élément"],
 | |
|     "tag" => ["?string", null, "tag à utiliser pour l'élément"],
 | |
|     "attrs" => ["?array", null, "attributs HTML génériques de l'élément"],
 | |
|     "brand" => [null, null, "la marque à afficher"],
 | |
|     # hors nav
 | |
|     "align" => ["?string", null, "alignement de l'élément"],
 | |
|   ];
 | |
| 
 | |
|   function navBrand($brand, bool $inNav): array {
 | |
|     md::ensure_schema($brand, self::NAV_BRAND_SCHEMA);
 | |
|     A::replace_n_indirect($brand, "value", "brand");
 | |
|     $tag = $brand["tag"];
 | |
|     if ($tag === null) $tag = $inNav? "li": "span";
 | |
|     $alignClass = self::get_align_class($brand["align"]);
 | |
|     return v::tag($tag, [
 | |
|       "class" => ["navbar-brand", $alignClass, $brand["class"]],
 | |
|       "style" => $brand["style"],
 | |
|       $brand["attrs"],
 | |
|       q($brand["value"]),
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   const NAV_TEXT_SCHEMA = [
 | |
|     "value" => [null, null, "le texte à afficher"],
 | |
|     "class" => ["?array", null, "classe CSS de l'élément"],
 | |
|     # class: type ?array pour le moment, à remplacer par "?items"
 | |
|     "style" => ["?string", null, "style CSS de l'élément"],
 | |
|     "tag" => ["?string", null, "tag à utiliser pour l'élément"],
 | |
|     "attrs" => ["?array", null, "attributs HTML génériques de l'élément"],
 | |
|     "text" => [null, null, "le texte à afficher"],
 | |
|     # hors nav
 | |
|     "align" => ["?string", null, "alignement de l'élément"],
 | |
|   ];
 | |
| 
 | |
|   function navText($text, bool $inNav): array {
 | |
|     md::ensure_schema($text, self::NAV_TEXT_SCHEMA);
 | |
|     A::replace_n_indirect($text, "value", "text");
 | |
|     $tag = $text["tag"];
 | |
|     if ($tag === null) $tag = $inNav? "li": "p";
 | |
|     $alignClass = self::get_align_class($text["align"]);
 | |
|     return v::tag($tag, [
 | |
|       "class" => ["navbar-text", $alignClass, $text["class"]],
 | |
|       "style" => $text["style"],
 | |
|       $text["attrs"],
 | |
|       q($text["value"]),
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   const NAV_LINK_SCHEMA = [
 | |
|     "url" => ["?string", null, "l'url du lien, à adapter avec page::bu()"],
 | |
|     "value" => [null, null, "le texte à afficher"],
 | |
|     "active" => ["?bool", false, "le lien est-il actif?"],
 | |
|     "show_active" => ["?bool", false, "faut-il afficher le lien comme actif (même s'il n'est pas actif)?"],
 | |
|     "accesskey" => ["?string", null, "touche d'accès rapide"],
 | |
|     "target" => ["?string", null, "destination du lien"],
 | |
|     "params" => ["?array", null, "des paramètres à ajouter à l'url le cas échéant"],
 | |
|     "class" => ["?array", null, "classe CSS du lien"],
 | |
|     # class: type ?array pour le moment, à remplacer par "?items"
 | |
|     "style" => ["?string", null, "style CSS du lien"],
 | |
|     "attrs" => ["?array", null, "attributs HTML génériques du lien"],
 | |
|     "text" => [null, null, "le texte à afficher"],
 | |
|     "literal_url" => ["?string", null, "l'url du lien, à utiliser sans modification"],
 | |
|     # hors nav, i.e type button
 | |
|     "align" => ["?string", null, "alignement de l'élément"],
 | |
|   ];
 | |
| 
 | |
|   private static function ensure_link(array &$link): void {
 | |
|     A::replace_n_indirect($link, "value", "text");
 | |
|     A::replace_n_indirect($link, "value", "url");
 | |
|   }
 | |
|   private static function is_sep(array $link): bool {
 | |
|     $value = $link["value"];
 | |
|     $url = $link["url"];
 | |
|     if ($url !== null) $sep = $url;
 | |
|     elseif ($value !== null) $sep = $value;
 | |
|     else $sep = null;
 | |
|     if (is_array($sep)) $sep = str::join("", $sep);
 | |
|     $literal_url = $link["literal_url"];
 | |
|     return $sep !== null && $literal_url == null && preg_match('/^-{3,}/', $sep);
 | |
|   }
 | |
|   private function _navSep(array $link): array {
 | |
|     return v::li([
 | |
|       "role" => "separator",
 | |
|       "class" => "divider",
 | |
|       $link["attrs"],
 | |
|     ]);
 | |
|   }
 | |
|   private function _navLinkShared(array $link): array {
 | |
|     $url = $link["url"];
 | |
|     $params = $link["params"];
 | |
|     $literal_url = $link["literal_url"];
 | |
|     if ($literal_url !== null) $url = $literal_url;
 | |
|     elseif ($url === null) $url = "#";
 | |
|     else $url = page::bu($url, $params);
 | |
| 
 | |
|     return v::a([
 | |
|       "href" => $url,
 | |
|       "accesskey" => $link["accesskey"],
 | |
|       "target" => $link["target"],
 | |
|       "class" => $link["class"],
 | |
|       "style" => $link["style"],
 | |
|       $link["attrs"],
 | |
|       q($link["value"]),
 | |
|     ]);
 | |
|   }
 | |
|   private function _navLink(array $link): array {
 | |
|     return v::li([
 | |
|       "class" => ["active" => $link["active"] || $link["show_active"]],
 | |
|       $this->_navLinkShared($link),
 | |
|     ]);
 | |
|   }
 | |
|   private function _navButton(array $button): array {
 | |
|     $a = $this->_navLinkShared($button);
 | |
|     return $a;
 | |
|   }
 | |
| 
 | |
|   function navLink($link): array {
 | |
|     md::ensure_schema($link, self::NAV_LINK_SCHEMA);
 | |
|     self::ensure_link($link);
 | |
|     switch ($link["type"]) {
 | |
|     case self::ITEM_LINK:
 | |
|       if (self::is_sep($link)) return $this->_navSep($link);
 | |
|       else return $this->_navLink($link);
 | |
|     case self::ITEM_BUTTON:
 | |
|       $alignClass = self::get_align_class($link["align"]);
 | |
|       $active = $link["active"];
 | |
|       $class = $link["class"];
 | |
|       if ($active !== null) {
 | |
|         $activeClass = $active ? "btn-primary" : "btn-default";
 | |
|       } elseif ($class === null) {
 | |
|         $activeClass = "btn-default";
 | |
|       } else {
 | |
|         $activeClass = null;
 | |
|       }
 | |
|       $link["class"] = ["navbar-btn btn", $activeClass, $alignClass, $class];
 | |
|       return $this->_navButton($link);
 | |
|     default:
 | |
|       return [];
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const NAV_MENU_SCHEMA = [
 | |
|     "value" => [null, null, "le texte à afficher"],
 | |
|     "links" => ["?array[]", null, "les liens à afficher"],
 | |
|     "active" => ["?bool", null, "le lien est-il actif?"],
 | |
|     "class" => ["?array", null, "classe CSS du lien"],
 | |
|     # class: type ?array pour le moment, à remplacer par "?items"
 | |
|     "style" => ["?string", null, "style CSS du lien"],
 | |
|     "attrs" => ["?array", null, "attributs HTML génériques du lien"],
 | |
|     "text" => [null, null, "le texte à afficher"],
 | |
|     # hors nav
 | |
|     "align" => ["?string", null, "alignement de l'élément"],
 | |
|   ];
 | |
| 
 | |
|   function navMenu($menu): array {
 | |
|     md::ensure_schema($menu, self::NAV_MENU_SCHEMA);
 | |
|     A::replace_n_indirect($menu, "value", "text");
 | |
|     $active = $menu["active"];
 | |
|     $navLinks = [];
 | |
|     foreach ($menu["links"] as $link) {
 | |
|       $itemType = A::get($link, "item");
 | |
|       if ($itemType == self::ITEM_TEXT) {
 | |
|         # support minimal du type texte
 | |
|         $text = $link;
 | |
|         md::ensure_schema($text, self::NAV_TEXT_SCHEMA);
 | |
|         $navLinks[] = v::p([
 | |
|           "class" => ["navbar-text", $text["class"]],
 | |
|           "style" => $text["style"],
 | |
|           $text["attrs"],
 | |
|           q($text["value"]),
 | |
|         ]);
 | |
|       } elseif ($itemType == self::ITEM_LITERAL) {
 | |
|         # support minimal du type literal
 | |
|         $literal = $link;
 | |
|         md::ensure_schema($literal, self::NAV_ITEM_SCHEMA);
 | |
|         $navLinks[] = $literal["value"];
 | |
|       } else {
 | |
|         md::ensure_schema($link, self::NAV_LINK_SCHEMA);
 | |
|         self::ensure_link($link);
 | |
|         if (self::is_sep($link)) {
 | |
|           $navLinks[] = $this->_navSep($link);
 | |
|         } else {
 | |
|           $navLinks[] = $this->_navLink($link);
 | |
|         }
 | |
|         if ($active === null && $link["active"]) {
 | |
|           $active = true;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     return v::li([
 | |
|       "class" => ["dropdown", $active? "active": null],
 | |
|       v::a([
 | |
|         "href" => "#", "role" => "button",
 | |
|         "class" => "dropdown-toggle", "data-toggle" => "dropdown",
 | |
|         "aria-haspopup" => "true", "aria-expanded" => "false",
 | |
|         q($menu["value"]),
 | |
|         " <span class=\"caret\"></span>",
 | |
|       ]),
 | |
|       v::ul(["class" => "dropdown-menu", $navLinks]),
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   function endNav(): array {
 | |
|     $endnav = $this->pop("nav");
 | |
|     if ($endnav === null) $endnav = [];
 | |
|     return $endnav;
 | |
|   }
 | |
| }
 |