nur-sery/nur_src/v/bs3/Bs3NavbarManager.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;
}
}