104fd306cSNickeau<?php 204fd306cSNickeau 304fd306cSNickeaunamespace ComboStrap; 404fd306cSNickeau 504fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute; 604fd306cSNickeauuse dokuwiki\Menu\PageMenu; 704fd306cSNickeauuse dokuwiki\Menu\SiteMenu; 804fd306cSNickeauuse dokuwiki\Menu\UserMenu; 904fd306cSNickeau 1004fd306cSNickeau/** 1104fd306cSNickeau * A fetcher for a menu rail bar 1204fd306cSNickeau * https://material.io/components/navigation-rail 1304fd306cSNickeau * 1404fd306cSNickeau * 1504fd306cSNickeau * Note: this class is a fetcher but it does not still work to call it via a javascript function included in the page. 1604fd306cSNickeau * Why ? The problem is that plugins that add a item would expect 1704fd306cSNickeau * to be loaded with the page and the related javascript is generally wrapped in a listener waiting for the page load event. 1804fd306cSNickeau * It means that it would never be triggered. 1904fd306cSNickeau * 2004fd306cSNickeau */ 2104fd306cSNickeauclass FetcherRailBar extends IFetcherAbs implements IFetcherString 2204fd306cSNickeau{ 2304fd306cSNickeau 2404fd306cSNickeau use FetcherTraitWikiPath; 2504fd306cSNickeau 2604fd306cSNickeau const CANONICAL = self::NAME; 2704fd306cSNickeau const NAME = "railbar"; 2804fd306cSNickeau const FIXED_LAYOUT = "fixed"; 2904fd306cSNickeau const OFFCANVAS_LAYOUT = "off-canvas"; 3004fd306cSNickeau const VIEWPORT_WIDTH = "viewport"; 3104fd306cSNickeau const LAYOUT_ATTRIBUTE = "layout"; 3204fd306cSNickeau /** 3304fd306cSNickeau * Do we show the rail bar for anonymous user 3404fd306cSNickeau */ 3504fd306cSNickeau public const CONF_PRIVATE_RAIL_BAR = "privateRailbar"; 3604fd306cSNickeau /** 3704fd306cSNickeau * When do we toggle from offcanvas to fixed railbar 3804fd306cSNickeau */ 3904fd306cSNickeau public const CONF_BREAKPOINT_RAIL_BAR = "breakpointRailbar"; 4004fd306cSNickeau const BOTH_LAYOUT = "all"; 4104fd306cSNickeau const KNOWN_LAYOUT = [self::FIXED_LAYOUT, self::OFFCANVAS_LAYOUT, self::BOTH_LAYOUT]; 4204fd306cSNickeau 4304fd306cSNickeau 4404fd306cSNickeau private int $requestedViewPort; 4504fd306cSNickeau private string $requestedLayout; 4604fd306cSNickeau 4704fd306cSNickeau 4804fd306cSNickeau public static function createRailBar(): FetcherRailBar 4904fd306cSNickeau { 5004fd306cSNickeau return new FetcherRailBar(); 5104fd306cSNickeau } 5204fd306cSNickeau 5304fd306cSNickeau private static function getComponentClass(): string 5404fd306cSNickeau { 5504fd306cSNickeau return StyleAttribute::addComboStrapSuffix(self::CANONICAL); 5604fd306cSNickeau } 5704fd306cSNickeau 5804fd306cSNickeau /** 5904fd306cSNickeau * @throws ExceptionBadArgument 6004fd306cSNickeau * @throws ExceptionBadSyntax 6104fd306cSNickeau * @throws ExceptionNotExists 6204fd306cSNickeau * @throws ExceptionNotFound 6304fd306cSNickeau */ 6404fd306cSNickeau public function buildFromTagAttributes(TagAttributes $tagAttributes): IFetcher 6504fd306cSNickeau { 6604fd306cSNickeau /** 6704fd306cSNickeau * Capture the id 6804fd306cSNickeau */ 6904fd306cSNickeau $this->buildOriginalPathFromTagAttributes($tagAttributes); 7004fd306cSNickeau /** 7104fd306cSNickeau * Capture the view port 7204fd306cSNickeau */ 7304fd306cSNickeau $viewPortWidth = $tagAttributes->getValueAndRemoveIfPresent(self::VIEWPORT_WIDTH); 7404fd306cSNickeau if ($viewPortWidth !== null) { 7504fd306cSNickeau try { 7604fd306cSNickeau $this->setRequestedViewPort(DataType::toInteger($viewPortWidth)); 7704fd306cSNickeau } catch (ExceptionBadArgument $e) { 7804fd306cSNickeau throw new ExceptionBadArgument("The viewport width is not a valid integer. Error:{$e->getMessage()}", self::CANONICAL); 7904fd306cSNickeau } 8004fd306cSNickeau } 8104fd306cSNickeau /** 8204fd306cSNickeau * Capture the layout 8304fd306cSNickeau */ 8404fd306cSNickeau $layout = $tagAttributes->getValueAndRemoveIfPresent(self::LAYOUT_ATTRIBUTE); 8504fd306cSNickeau if ($layout !== null) { 8604fd306cSNickeau try { 8704fd306cSNickeau $this->setRequestedLayout($layout); 8804fd306cSNickeau } catch (ExceptionBadArgument $e) { 8904fd306cSNickeau throw new ExceptionBadArgument("The layout is not a valid. Error:{$e->getMessage()}", self::CANONICAL); 9004fd306cSNickeau } 9104fd306cSNickeau } 9204fd306cSNickeau return parent::buildFromTagAttributes($tagAttributes); 9304fd306cSNickeau } 9404fd306cSNickeau 9504fd306cSNickeau 9604fd306cSNickeau function getFetchPath(): Path 9704fd306cSNickeau { 9804fd306cSNickeau throw new ExceptionRuntimeInternal("No fetch path: Railbar is not a file but a dynamic HTML document"); 9904fd306cSNickeau } 10004fd306cSNickeau 10104fd306cSNickeau function getFetchString(): string 10204fd306cSNickeau { 10304fd306cSNickeau 10404fd306cSNickeau if (!$this->shouldBePrinted()) { 10504fd306cSNickeau return ""; 10604fd306cSNickeau } 10704fd306cSNickeau 10804fd306cSNickeau $localWikiRequest = null; 10904fd306cSNickeau $localWikiId = null; 11004fd306cSNickeau try { 11104fd306cSNickeau ExecutionContext::getExecutionContext(); 11204fd306cSNickeau } catch (ExceptionNotFound $e) { 11304fd306cSNickeau 11404fd306cSNickeau /** 11504fd306cSNickeau * No actual request (called via ajax) 11604fd306cSNickeau */ 11704fd306cSNickeau $localWikiId = $this->getSourcePath()->getWikiId(); 11804fd306cSNickeau $localWikiRequest = ExecutionContext::getOrCreateFromRequestedWikiId($localWikiId); 11904fd306cSNickeau 12004fd306cSNickeau /** 12104fd306cSNickeau * page info is needed and used by all other plugins 12204fd306cSNickeau * in all hooks (should be first) 12304fd306cSNickeau */ 12404fd306cSNickeau global $INFO; 12504fd306cSNickeau $INFO = pageinfo(); 12604fd306cSNickeau 12704fd306cSNickeau /** 12804fd306cSNickeau * Uses by {@link action_plugin_move_rename} to set 12904fd306cSNickeau * if it will add the button 13004fd306cSNickeau */ 13104fd306cSNickeau $tmp = array(); 13204fd306cSNickeau \dokuwiki\Extension\Event::createAndTrigger('DOKUWIKI_STARTED', $tmp); 13304fd306cSNickeau 13404fd306cSNickeau } 13504fd306cSNickeau 13604fd306cSNickeau 13704fd306cSNickeau try { 13804fd306cSNickeau 13904fd306cSNickeau $snippetManager = SnippetSystem::getFromContext(); 14004fd306cSNickeau $railBarHtmlListItems = $this->getRailBarHtmlListItems(); 14104fd306cSNickeau $railBarLayout = $this->getLayoutTypeToApply(); 14204fd306cSNickeau switch ($railBarLayout) { 14304fd306cSNickeau case self::FIXED_LAYOUT: 14404fd306cSNickeau $railBar = $this->toFixedLayout($railBarHtmlListItems); 14504fd306cSNickeau $snippetManager->attachCssInternalStylesheet("railbar-$railBarLayout"); 14604fd306cSNickeau break; 14704fd306cSNickeau case self::OFFCANVAS_LAYOUT: 14804fd306cSNickeau $railBar = $this->toOffCanvasLayout($railBarHtmlListItems); 14904fd306cSNickeau $snippetManager->attachCssInternalStylesheet("railbar-$railBarLayout"); 15004fd306cSNickeau break; 15104fd306cSNickeau case self::BOTH_LAYOUT: 15204fd306cSNickeau default: 15304fd306cSNickeau $snippetManager->attachCssInternalStylesheet("railbar-" . self::FIXED_LAYOUT); 15404fd306cSNickeau $snippetManager->attachCssInternalStylesheet("railbar-" . self::OFFCANVAS_LAYOUT); 15504fd306cSNickeau $breakpoint = $this->getBreakPointConfiguration(); 15604fd306cSNickeau $railBar = $this->toFixedLayout($railBarHtmlListItems, $breakpoint) 15704fd306cSNickeau . $this->toOffCanvasLayout($railBarHtmlListItems, $breakpoint); 15804fd306cSNickeau break; 15904fd306cSNickeau } 16004fd306cSNickeau 16104fd306cSNickeau 16204fd306cSNickeau $snippetManager->attachCssInternalStylesheet("railbar"); 16304fd306cSNickeau 16404fd306cSNickeau if ($localWikiRequest !== null) { 16504fd306cSNickeau $snippets = $snippetManager->toHtmlForAllSnippets(); 16604fd306cSNickeau $snippetClass = self::getSnippetClass(); 16704fd306cSNickeau /** 16804fd306cSNickeau * Snippets should be after the html because they works 16904fd306cSNickeau * on the added HTML 17004fd306cSNickeau */ 17104fd306cSNickeau $railBar = <<<EOF 17204fd306cSNickeau$railBar 17304fd306cSNickeau<div id="$snippetClass" class="$snippetClass"> 17404fd306cSNickeau$snippets 17504fd306cSNickeau</div> 17604fd306cSNickeauEOF; 17704fd306cSNickeau } 17804fd306cSNickeau 17904fd306cSNickeau return $railBar; 18004fd306cSNickeau 18104fd306cSNickeau 18204fd306cSNickeau } finally { 18304fd306cSNickeau if ($localWikiRequest !== null) { 18404fd306cSNickeau $localWikiRequest->close($localWikiId); 18504fd306cSNickeau } 18604fd306cSNickeau } 18704fd306cSNickeau 18804fd306cSNickeau } 18904fd306cSNickeau 19004fd306cSNickeau function getBuster(): string 19104fd306cSNickeau { 19204fd306cSNickeau return ""; 19304fd306cSNickeau } 19404fd306cSNickeau 19504fd306cSNickeau public function getMime(): Mime 19604fd306cSNickeau { 19704fd306cSNickeau return Mime::getHtml(); 19804fd306cSNickeau } 19904fd306cSNickeau 20004fd306cSNickeau public function getFetcherName(): string 20104fd306cSNickeau { 20204fd306cSNickeau return self::NAME; 20304fd306cSNickeau } 20404fd306cSNickeau 20504fd306cSNickeau public function setRequestedPageWikiId(string $wikiId): FetcherRailBar 20604fd306cSNickeau { 20704fd306cSNickeau $path = WikiPath::createMarkupPathFromId($wikiId); 20804fd306cSNickeau return $this->setRequestedPath($path); 20904fd306cSNickeau } 21004fd306cSNickeau 21104fd306cSNickeau public static function getSnippetClass(): string 21204fd306cSNickeau { 21304fd306cSNickeau return Snippet::getClassFromComponentId(self::CANONICAL); 21404fd306cSNickeau } 21504fd306cSNickeau 21604fd306cSNickeau private function getRailBarHtmlListItems(): string 21704fd306cSNickeau { 21804fd306cSNickeau $liUserTools = (new UserMenu())->getListItems('action'); 21904fd306cSNickeau $pageMenu = new PageMenu(); 22004fd306cSNickeau $liPageTools = $pageMenu->getListItems(); 22104fd306cSNickeau $liSiteTools = (new SiteMenu())->getListItems('action'); 22204fd306cSNickeau // FYI: The below code outputs all menu in mobile (in another HTML layout) 22304fd306cSNickeau // echo (new \dokuwiki\Menu\MobileMenu())->getDropdown($lang['tools']); 22404fd306cSNickeau $componentClass = self::getComponentClass(); 22504fd306cSNickeau return <<<EOF 22604fd306cSNickeau<ul class="$componentClass"> 22704fd306cSNickeau <li><a href="#" style="height: 19px;line-height: 17px;text-align: left;font-weight:bold"><span>User</span><svg style="height:19px"></svg></a></li> 22804fd306cSNickeau $liUserTools 22904fd306cSNickeau <li><a href="#" style="height: 19px;line-height: 17px;text-align: left;font-weight:bold"><span>Page</span><svg style="height:19px"></svg></a></li> 23004fd306cSNickeau $liPageTools 23104fd306cSNickeau <li><a href="#" style="height: 19px;line-height: 17px;text-align: left;font-weight:bold"><span>Website</span><svg style="height:19px"></svg></a></li> 23204fd306cSNickeau $liSiteTools 23304fd306cSNickeau</ul> 23404fd306cSNickeauEOF; 23504fd306cSNickeau 23604fd306cSNickeau } 23704fd306cSNickeau 23804fd306cSNickeau private function toOffCanvasLayout(string $railBarHtmlListItems, Breakpoint $hideFromBreakpoint = null): string 23904fd306cSNickeau { 24004fd306cSNickeau $breakpointHiding = ""; 24104fd306cSNickeau if ($hideFromBreakpoint !== null) { 24204fd306cSNickeau $breakpointHiding = "d-{$hideFromBreakpoint->getShortName()}-none"; 24304fd306cSNickeau } 24404fd306cSNickeau $railBarOffCanvasPrefix = "railbar-offcanvas"; 24504fd306cSNickeau $railBarClass = StyleAttribute::addComboStrapSuffix(self::NAME); 24604fd306cSNickeau $railBarOffCanvasClassAndId = StyleAttribute::addComboStrapSuffix($railBarOffCanvasPrefix); 24704fd306cSNickeau $railBarOffCanvasWrapperId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-wrapper"); 24804fd306cSNickeau $railBarOffCanvasLabelId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-label"); 24904fd306cSNickeau $railBarOffcanvasBodyId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-body"); 25004fd306cSNickeau $railBarOffCanvasCloseId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-close"); 25104fd306cSNickeau $railBarOffCanvasOpenId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-open"); 25204fd306cSNickeau return <<<EOF 25304fd306cSNickeau<div id="$railBarOffCanvasWrapperId" class="$railBarClass $railBarOffCanvasClassAndId $breakpointHiding"> 254*8b8569b7Sgerardnico <button id="$railBarOffCanvasOpenId" class="btn" type="button" aria-label="Open the railbar" data-bs-toggle="offcanvas" 25504fd306cSNickeau data-bs-target="#$railBarOffCanvasClassAndId" aria-controls="railbar-offcanvas"> 25604fd306cSNickeau </button> 25704fd306cSNickeau 25804fd306cSNickeau <div id="$railBarOffCanvasClassAndId" class="offcanvas offcanvas-end" aria-labelledby="$railBarOffCanvasLabelId" 25904fd306cSNickeau style="visibility: hidden;" aria-hidden="true"> 26004fd306cSNickeau <h5 class="d-none" id="$railBarOffCanvasLabelId">Railbar</h5> 26104fd306cSNickeau <!-- Pseudo relative element https://stackoverflow.com/questions/6040005/relatively-position-an-element-without-it-taking-up-space-in-document-flow --> 26204fd306cSNickeau <div style="position: relative; width: 0; height: 0"> 26304fd306cSNickeau <button id="$railBarOffCanvasCloseId" class="btn" type="button" data-bs-dismiss="offcanvas" aria-label="Close"></button> 26404fd306cSNickeau </div> 26504fd306cSNickeau <div id="$railBarOffcanvasBodyId" class="offcanvas-body" style="align-items: center;display: flex;"> 26604fd306cSNickeau $railBarHtmlListItems 26704fd306cSNickeau </div> 26804fd306cSNickeau </div> 26904fd306cSNickeau</div> 27004fd306cSNickeauEOF; 27104fd306cSNickeau 27204fd306cSNickeau } 27304fd306cSNickeau 27404fd306cSNickeau public function getLayoutTypeToApply(): string 27504fd306cSNickeau { 27604fd306cSNickeau 27704fd306cSNickeau if (isset($this->requestedLayout)) { 27804fd306cSNickeau return $this->requestedLayout; 27904fd306cSNickeau } 28004fd306cSNickeau $bootstrapVersion = Bootstrap::getBootStrapMajorVersion(); 28104fd306cSNickeau if ($bootstrapVersion === Bootstrap::BootStrapFourMajorVersion) { 28204fd306cSNickeau return self::FIXED_LAYOUT; 28304fd306cSNickeau } 28404fd306cSNickeau try { 28504fd306cSNickeau $breakPointConfigurationInPixel = $this->getBreakPointConfiguration()->getWidth(); 28604fd306cSNickeau } catch (ExceptionInfinite $e) { 28704fd306cSNickeau // no breakpoint 28804fd306cSNickeau return self::OFFCANVAS_LAYOUT; 28904fd306cSNickeau } 29004fd306cSNickeau 29104fd306cSNickeau try { 29204fd306cSNickeau if ($this->getRequestedViewPort() > $breakPointConfigurationInPixel) { 29304fd306cSNickeau return self::FIXED_LAYOUT; 29404fd306cSNickeau } else { 29504fd306cSNickeau return self::OFFCANVAS_LAYOUT; 29604fd306cSNickeau } 29704fd306cSNickeau } catch (ExceptionNotFound $e) { 29804fd306cSNickeau // no known target view port 29904fd306cSNickeau // we send them both then 30004fd306cSNickeau return self::BOTH_LAYOUT; 30104fd306cSNickeau } 30204fd306cSNickeau 30304fd306cSNickeau } 30404fd306cSNickeau 30504fd306cSNickeau public function setRequestedViewPort(int $viewPort): FetcherRailBar 30604fd306cSNickeau { 30704fd306cSNickeau $this->requestedViewPort = $viewPort; 30804fd306cSNickeau return $this; 30904fd306cSNickeau } 31004fd306cSNickeau 31104fd306cSNickeau /** 31204fd306cSNickeau * The call may indicate the view port that the railbar will be used for 31304fd306cSNickeau * (ie breakpoint) 31404fd306cSNickeau * @return int 31504fd306cSNickeau * @throws ExceptionNotFound 31604fd306cSNickeau */ 31704fd306cSNickeau public function getRequestedViewPort(): int 31804fd306cSNickeau { 31904fd306cSNickeau if (!isset($this->requestedViewPort)) { 32004fd306cSNickeau throw new ExceptionNotFound("No requested view port"); 32104fd306cSNickeau } 32204fd306cSNickeau return $this->requestedViewPort; 32304fd306cSNickeau } 32404fd306cSNickeau 32504fd306cSNickeau private function shouldBePrinted(): bool 32604fd306cSNickeau { 32704fd306cSNickeau 32804fd306cSNickeau if ( 32904fd306cSNickeau SiteConfig::getConfValue(self::CONF_PRIVATE_RAIL_BAR, 0) === 1 33004fd306cSNickeau && !Identity::isLoggedIn() 33104fd306cSNickeau ) { 33204fd306cSNickeau return false; 33304fd306cSNickeau } 33404fd306cSNickeau return true; 33504fd306cSNickeau 33604fd306cSNickeau } 33704fd306cSNickeau 33804fd306cSNickeau private function getBreakPointConfiguration(): Breakpoint 33904fd306cSNickeau { 34004fd306cSNickeau $name = SiteConfig::getConfValue(self::CONF_BREAKPOINT_RAIL_BAR, Breakpoint::BREAKPOINT_LARGE_NAME); 34104fd306cSNickeau return Breakpoint::createFromLongName($name); 34204fd306cSNickeau } 34304fd306cSNickeau 34404fd306cSNickeau 34504fd306cSNickeau /** 34604fd306cSNickeau * @param string $railBarHtmlListItems 34704fd306cSNickeau * @param Breakpoint|null $showFromBreakpoint 34804fd306cSNickeau * @return string 34904fd306cSNickeau */ 35004fd306cSNickeau private function toFixedLayout(string $railBarHtmlListItems, Breakpoint $showFromBreakpoint = null): string 35104fd306cSNickeau { 35204fd306cSNickeau $showFromBreakpointClasses = ""; 35304fd306cSNickeau if ($showFromBreakpoint !== null) { 35404fd306cSNickeau $showFromBreakpointClasses = "d-none d-{$showFromBreakpoint->getShortName()}-flex"; 35504fd306cSNickeau } 35604fd306cSNickeau $railBarClass = StyleAttribute::addComboStrapSuffix(self::NAME); 35704fd306cSNickeau $railBarFixedClassOrId = StyleAttribute::addComboStrapSuffix(self::NAME . "-fixed"); 35804fd306cSNickeau $zIndexRailbar = 1000; // A navigation bar (below the drop down because we use it in the search box for auto-completion) 35904fd306cSNickeau return <<<EOF 36004fd306cSNickeau<div id="$railBarFixedClassOrId" class="$railBarClass $railBarFixedClassOrId d-flex $showFromBreakpointClasses" style="z-index: $zIndexRailbar;"> 36104fd306cSNickeau <div> 36204fd306cSNickeau $railBarHtmlListItems 36304fd306cSNickeau </div> 36404fd306cSNickeau</div> 36504fd306cSNickeauEOF; 36604fd306cSNickeau 36704fd306cSNickeau } 36804fd306cSNickeau 36904fd306cSNickeau /** 37004fd306cSNickeau * The layout may be requested (example in a landing page where you don't want to see it) 37104fd306cSNickeau * @param string $layout 37204fd306cSNickeau * @return FetcherRailBar 37304fd306cSNickeau * @throws ExceptionBadArgument 37404fd306cSNickeau */ 37504fd306cSNickeau public function setRequestedLayout(string $layout): FetcherRailBar 37604fd306cSNickeau { 37704fd306cSNickeau if (!in_array($layout, self::KNOWN_LAYOUT)) { 37804fd306cSNickeau throw new ExceptionBadArgument("The layout ($layout) is not valid. The known-layout are : ".ArrayUtility::formatAsString(self::KNOWN_LAYOUT)); 37904fd306cSNickeau } 38004fd306cSNickeau $this->requestedLayout = $layout; 38104fd306cSNickeau return $this; 38204fd306cSNickeau } 38304fd306cSNickeau 38404fd306cSNickeau public function setRequestedPath(WikiPath $requestedPath): FetcherRailBar 38504fd306cSNickeau { 38604fd306cSNickeau $this->setSourcePath($requestedPath); 38704fd306cSNickeau return $this; 38804fd306cSNickeau } 38904fd306cSNickeau 39004fd306cSNickeau 39104fd306cSNickeau public function getLabel(): string 39204fd306cSNickeau { 39304fd306cSNickeau return self::NAME; 39404fd306cSNickeau } 39504fd306cSNickeau 39604fd306cSNickeau} 397