diff --git a/composer.lock b/composer.lock index c2569df..a645d55 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6db6b3cf2c87765466c72588c5b8b729", + "content-hash": "f31de0d7946d45b36413fb7c854d8cbd", "packages": [ { "name": "pear/console_getopt", @@ -1579,16 +1579,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.8.2", + "version": "6.9.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76" + "reference": "bd1bda2ebfc8bff418565941771ea8f03c557886" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/2c89ebb95ca9cedc9347f780333f7b25792dcb76", - "reference": "2c89ebb95ca9cedc9347f780333f7b25792dcb76", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/bd1bda2ebfc8bff418565941771ea8f03c557886", + "reference": "bd1bda2ebfc8bff418565941771ea8f03c557886", "shasum": "" }, "require": { @@ -1598,7 +1598,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "3.3.0", - "json-schema/json-schema-test-suite": "dev-main", + "json-schema/json-schema-test-suite": "^23.2", "marc-mabe/php-enum-phpstan": "^2.0", "phpspec/prophecy": "^1.19", "phpstan/phpstan": "^1.12", @@ -1648,9 +1648,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.9.0" }, - "time": "2026-05-05T05:39:01+00:00" + "time": "2026-06-05T14:05:24+00:00" }, { "name": "kubawerlos/php-cs-fixer-custom-fixers", diff --git a/src/Ease/TWB5/Navbar.php b/src/Ease/TWB5/Navbar.php index 3448520..96054ec 100644 --- a/src/Ease/TWB5/Navbar.php +++ b/src/Ease/TWB5/Navbar.php @@ -42,8 +42,20 @@ class Navbar extends NavTag * Brand link destination. */ public string $mainpage = '#'; + + /** + * Render the collapsible menu as an Offcanvas drawer instead of a + * Bootstrap collapse. Enable in a subclass before finalize(). + */ + public bool $offcanvas = false; + + /** + * Offcanvas drawer placement when $offcanvas is enabled. + */ + public string $offcanvasPlacement = 'end'; private string $navBarName = 'nav'; private \Ease\Html\DivTag $containerFluid; + private ButtonTag $toggler; /** * App Menu. @@ -70,7 +82,8 @@ public function __construct($brand = null, $name = 'navbar', $properties = []) $this->leftContent = new UlTag(null, ['class' => 'navbar-nav flex-nowrap mb-2 mb-lg-0', 'style' => '--bs-scroll-height: 100px;']); $this->rightContent = new UlTag(null, ['class' => 'navbar-nav ms-auto flex-nowrap mb-2 mb-lg-0']); - $this->containerFluid = $this->addItem(new \Ease\Html\DivTag([new ATag($this->mainpage, $brand, ['class' => 'navbar-brand']), $this->navBarToggler()], ['class' => 'container-fluid'])); + $this->toggler = $this->navBarToggler(); + $this->containerFluid = $this->addItem(new \Ease\Html\DivTag([new ATag($this->mainpage, $brand, ['class' => 'navbar-brand']), $this->toggler], ['class' => 'container-fluid'])); } /** @@ -136,12 +149,49 @@ public function navBarCollapse() return new \Ease\Html\DivTag($this->leftContent, ['class' => 'collapse navbar-collapse', 'id' => $this->navBarName]); } + /** + * Wrap the menu in an Offcanvas drawer (responsive offcanvas-in-navbar). + * + * @see https://getbootstrap.com/docs/5.3/components/navbar/#offcanvas + * + * @return \Ease\Html\DivTag Offcanvas drawer + */ + public function navBarOffcanvas() + { + $ocId = 'offcanvas'.$this->navBarName; + + $header = new \Ease\Html\DivTag([ + new \Ease\Html\H5Tag(_('Menu'), ['class' => 'offcanvas-title', 'id' => $ocId.'Label']), + new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => _('Close')]), + ], ['class' => 'offcanvas-header']); + + $body = new \Ease\Html\DivTag($this->leftContent, ['class' => 'offcanvas-body']); + + return new \Ease\Html\DivTag([$header, $body], [ + 'class' => 'offcanvas offcanvas-'.$this->offcanvasPlacement, + 'tabindex' => '-1', + 'id' => $ocId, + 'aria-labelledby' => $ocId.'Label', + ]); + } + /** * Finalize NavBar. */ public function finalize(): void { - $this->containerFluid->addItem($this->navbarCollapse()); + if ($this->offcanvas) { + $ocId = 'offcanvas'.$this->navBarName; + $this->toggler->setTagProperties([ + 'data-bs-toggle' => 'offcanvas', + 'data-bs-target' => '#'.$ocId, + 'aria-controls' => $ocId, + ]); + $this->containerFluid->addItem($this->navBarOffcanvas()); + } else { + $this->containerFluid->addItem($this->navbarCollapse()); + } + parent::finalize(); } diff --git a/src/Ease/TWB5/OffCanvas.php b/src/Ease/TWB5/OffCanvas.php index ee13cbf..dcdecb5 100644 --- a/src/Ease/TWB5/OffCanvas.php +++ b/src/Ease/TWB5/OffCanvas.php @@ -19,25 +19,65 @@ use Ease\Html\DivTag; use Ease\Html\H5Tag; +/** + * Bootstrap Offcanvas slide-in panel. + * + * @see https://getbootstrap.com/docs/5.3/components/offcanvas/ + * + * @author Vítězslav Dvořák + */ class OffCanvas extends DivTag { - public function __construct($id, $title, $bodyContent) - { - $header = new DivTag( - [ - new H5Tag($title, ['class' => 'offcanvas-title', 'id' => $id.'Label']), - new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => 'Close']), - ], - ['class' => 'offcanvas-header'], - ); - - $body = new DivTag($bodyContent, ['class' => 'offcanvas-body']); - - parent::__construct([$header, $body], [ - 'class' => 'offcanvas offcanvas-start show', + public DivTag $header; + public DivTag $body; + + /** + * Bootstrap Offcanvas. + * + * @param string $id Unique offcanvas ID + * @param mixed $title Header title + * @param mixed $bodyContent Offcanvas body content + * @param string $placement start|end|top|bottom + * @param array $properties Additional offcanvas div properties + */ + public function __construct( + string $id, + $title = null, + $bodyContent = null, + string $placement = 'start', + array $properties = [] + ) { + parent::__construct(null, array_merge([ 'tabindex' => '-1', - 'id' => $id, 'aria-labelledby' => $id.'Label', + ], $properties)); + $this->addTagClass('offcanvas offcanvas-'.$placement); + $this->setTagID($id); + + $this->header = new DivTag([ + new H5Tag($title, ['class' => 'offcanvas-title', 'id' => $id.'Label']), + new ButtonTag('', ['class' => 'btn-close', 'data-bs-dismiss' => 'offcanvas', 'aria-label' => 'Close']), + ], ['class' => 'offcanvas-header']); + + $this->body = new DivTag($bodyContent, ['class' => 'offcanvas-body']); + + $this->addItem($this->header); + $this->addItem($this->body); + } + + /** + * Returns a button that toggles this offcanvas. + * + * @param mixed $label Button label + * @param string $type primary|secondary|success|danger|warning|info|light|dark + */ + public function triggerButton($label, string $type = 'primary'): ButtonTag + { + return new ButtonTag($label, [ + 'class' => 'btn btn-'.$type, + 'data-bs-toggle' => 'offcanvas', + 'data-bs-target' => '#'.$this->getTagID(), + 'aria-controls' => $this->getTagID(), ]); } }