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