1*04fd306cSNickeau<?php 2*04fd306cSNickeau 3*04fd306cSNickeaunamespace ComboStrap; 4*04fd306cSNickeau 5*04fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute; 6*04fd306cSNickeauuse dokuwiki\Menu\PageMenu; 7*04fd306cSNickeauuse dokuwiki\Menu\SiteMenu; 8*04fd306cSNickeauuse dokuwiki\Menu\UserMenu; 9*04fd306cSNickeau 10*04fd306cSNickeau/** 11*04fd306cSNickeau * A fetcher for a menu rail bar 12*04fd306cSNickeau * https://material.io/components/navigation-rail 13*04fd306cSNickeau * 14*04fd306cSNickeau * 15*04fd306cSNickeau * Note: this class is a fetcher but it does not still work to call it via a javascript function included in the page. 16*04fd306cSNickeau * Why ? The problem is that plugins that add a item would expect 17*04fd306cSNickeau * to be loaded with the page and the related javascript is generally wrapped in a listener waiting for the page load event. 18*04fd306cSNickeau * It means that it would never be triggered. 19*04fd306cSNickeau * 20*04fd306cSNickeau */ 21*04fd306cSNickeauclass FetcherRailBar extends IFetcherAbs implements IFetcherString 22*04fd306cSNickeau{ 23*04fd306cSNickeau 24*04fd306cSNickeau use FetcherTraitWikiPath; 25*04fd306cSNickeau 26*04fd306cSNickeau const CANONICAL = self::NAME; 27*04fd306cSNickeau const NAME = "railbar"; 28*04fd306cSNickeau const FIXED_LAYOUT = "fixed"; 29*04fd306cSNickeau const OFFCANVAS_LAYOUT = "off-canvas"; 30*04fd306cSNickeau const VIEWPORT_WIDTH = "viewport"; 31*04fd306cSNickeau const LAYOUT_ATTRIBUTE = "layout"; 32*04fd306cSNickeau /** 33*04fd306cSNickeau * Do we show the rail bar for anonymous user 34*04fd306cSNickeau */ 35*04fd306cSNickeau public const CONF_PRIVATE_RAIL_BAR = "privateRailbar"; 36*04fd306cSNickeau /** 37*04fd306cSNickeau * When do we toggle from offcanvas to fixed railbar 38*04fd306cSNickeau */ 39*04fd306cSNickeau public const CONF_BREAKPOINT_RAIL_BAR = "breakpointRailbar"; 40*04fd306cSNickeau const BOTH_LAYOUT = "all"; 41*04fd306cSNickeau const KNOWN_LAYOUT = [self::FIXED_LAYOUT, self::OFFCANVAS_LAYOUT, self::BOTH_LAYOUT]; 42*04fd306cSNickeau 43*04fd306cSNickeau 44*04fd306cSNickeau private int $requestedViewPort; 45*04fd306cSNickeau private string $requestedLayout; 46*04fd306cSNickeau 47*04fd306cSNickeau 48*04fd306cSNickeau public static function createRailBar(): FetcherRailBar 49*04fd306cSNickeau { 50*04fd306cSNickeau return new FetcherRailBar(); 51*04fd306cSNickeau } 52*04fd306cSNickeau 53*04fd306cSNickeau private static function getComponentClass(): string 54*04fd306cSNickeau { 55*04fd306cSNickeau return StyleAttribute::addComboStrapSuffix(self::CANONICAL); 56*04fd306cSNickeau } 57*04fd306cSNickeau 58*04fd306cSNickeau /** 59*04fd306cSNickeau * @throws ExceptionBadArgument 60*04fd306cSNickeau * @throws ExceptionBadSyntax 61*04fd306cSNickeau * @throws ExceptionNotExists 62*04fd306cSNickeau * @throws ExceptionNotFound 63*04fd306cSNickeau */ 64*04fd306cSNickeau public function buildFromTagAttributes(TagAttributes $tagAttributes): IFetcher 65*04fd306cSNickeau { 66*04fd306cSNickeau /** 67*04fd306cSNickeau * Capture the id 68*04fd306cSNickeau */ 69*04fd306cSNickeau $this->buildOriginalPathFromTagAttributes($tagAttributes); 70*04fd306cSNickeau /** 71*04fd306cSNickeau * Capture the view port 72*04fd306cSNickeau */ 73*04fd306cSNickeau $viewPortWidth = $tagAttributes->getValueAndRemoveIfPresent(self::VIEWPORT_WIDTH); 74*04fd306cSNickeau if ($viewPortWidth !== null) { 75*04fd306cSNickeau try { 76*04fd306cSNickeau $this->setRequestedViewPort(DataType::toInteger($viewPortWidth)); 77*04fd306cSNickeau } catch (ExceptionBadArgument $e) { 78*04fd306cSNickeau throw new ExceptionBadArgument("The viewport width is not a valid integer. Error:{$e->getMessage()}", self::CANONICAL); 79*04fd306cSNickeau } 80*04fd306cSNickeau } 81*04fd306cSNickeau /** 82*04fd306cSNickeau * Capture the layout 83*04fd306cSNickeau */ 84*04fd306cSNickeau $layout = $tagAttributes->getValueAndRemoveIfPresent(self::LAYOUT_ATTRIBUTE); 85*04fd306cSNickeau if ($layout !== null) { 86*04fd306cSNickeau try { 87*04fd306cSNickeau $this->setRequestedLayout($layout); 88*04fd306cSNickeau } catch (ExceptionBadArgument $e) { 89*04fd306cSNickeau throw new ExceptionBadArgument("The layout is not a valid. Error:{$e->getMessage()}", self::CANONICAL); 90*04fd306cSNickeau } 91*04fd306cSNickeau } 92*04fd306cSNickeau return parent::buildFromTagAttributes($tagAttributes); 93*04fd306cSNickeau } 94*04fd306cSNickeau 95*04fd306cSNickeau 96*04fd306cSNickeau function getFetchPath(): Path 97*04fd306cSNickeau { 98*04fd306cSNickeau throw new ExceptionRuntimeInternal("No fetch path: Railbar is not a file but a dynamic HTML document"); 99*04fd306cSNickeau } 100*04fd306cSNickeau 101*04fd306cSNickeau function getFetchString(): string 102*04fd306cSNickeau { 103*04fd306cSNickeau 104*04fd306cSNickeau if (!$this->shouldBePrinted()) { 105*04fd306cSNickeau return ""; 106*04fd306cSNickeau } 107*04fd306cSNickeau 108*04fd306cSNickeau $localWikiRequest = null; 109*04fd306cSNickeau $localWikiId = null; 110*04fd306cSNickeau try { 111*04fd306cSNickeau ExecutionContext::getExecutionContext(); 112*04fd306cSNickeau } catch (ExceptionNotFound $e) { 113*04fd306cSNickeau 114*04fd306cSNickeau /** 115*04fd306cSNickeau * No actual request (called via ajax) 116*04fd306cSNickeau */ 117*04fd306cSNickeau $localWikiId = $this->getSourcePath()->getWikiId(); 118*04fd306cSNickeau $localWikiRequest = ExecutionContext::getOrCreateFromRequestedWikiId($localWikiId); 119*04fd306cSNickeau 120*04fd306cSNickeau /** 121*04fd306cSNickeau * page info is needed and used by all other plugins 122*04fd306cSNickeau * in all hooks (should be first) 123*04fd306cSNickeau */ 124*04fd306cSNickeau global $INFO; 125*04fd306cSNickeau $INFO = pageinfo(); 126*04fd306cSNickeau 127*04fd306cSNickeau /** 128*04fd306cSNickeau * Uses by {@link action_plugin_move_rename} to set 129*04fd306cSNickeau * if it will add the button 130*04fd306cSNickeau */ 131*04fd306cSNickeau $tmp = array(); 132*04fd306cSNickeau \dokuwiki\Extension\Event::createAndTrigger('DOKUWIKI_STARTED', $tmp); 133*04fd306cSNickeau 134*04fd306cSNickeau } 135*04fd306cSNickeau 136*04fd306cSNickeau 137*04fd306cSNickeau try { 138*04fd306cSNickeau 139*04fd306cSNickeau $snippetManager = SnippetSystem::getFromContext(); 140*04fd306cSNickeau $railBarHtmlListItems = $this->getRailBarHtmlListItems(); 141*04fd306cSNickeau $railBarLayout = $this->getLayoutTypeToApply(); 142*04fd306cSNickeau switch ($railBarLayout) { 143*04fd306cSNickeau case self::FIXED_LAYOUT: 144*04fd306cSNickeau $railBar = $this->toFixedLayout($railBarHtmlListItems); 145*04fd306cSNickeau $snippetManager->attachCssInternalStylesheet("railbar-$railBarLayout"); 146*04fd306cSNickeau break; 147*04fd306cSNickeau case self::OFFCANVAS_LAYOUT: 148*04fd306cSNickeau $railBar = $this->toOffCanvasLayout($railBarHtmlListItems); 149*04fd306cSNickeau $snippetManager->attachCssInternalStylesheet("railbar-$railBarLayout"); 150*04fd306cSNickeau break; 151*04fd306cSNickeau case self::BOTH_LAYOUT: 152*04fd306cSNickeau default: 153*04fd306cSNickeau $snippetManager->attachCssInternalStylesheet("railbar-" . self::FIXED_LAYOUT); 154*04fd306cSNickeau $snippetManager->attachCssInternalStylesheet("railbar-" . self::OFFCANVAS_LAYOUT); 155*04fd306cSNickeau $breakpoint = $this->getBreakPointConfiguration(); 156*04fd306cSNickeau $railBar = $this->toFixedLayout($railBarHtmlListItems, $breakpoint) 157*04fd306cSNickeau . $this->toOffCanvasLayout($railBarHtmlListItems, $breakpoint); 158*04fd306cSNickeau break; 159*04fd306cSNickeau } 160*04fd306cSNickeau 161*04fd306cSNickeau 162*04fd306cSNickeau $snippetManager->attachCssInternalStylesheet("railbar"); 163*04fd306cSNickeau 164*04fd306cSNickeau if ($localWikiRequest !== null) { 165*04fd306cSNickeau $snippets = $snippetManager->toHtmlForAllSnippets(); 166*04fd306cSNickeau $snippetClass = self::getSnippetClass(); 167*04fd306cSNickeau /** 168*04fd306cSNickeau * Snippets should be after the html because they works 169*04fd306cSNickeau * on the added HTML 170*04fd306cSNickeau */ 171*04fd306cSNickeau $railBar = <<<EOF 172*04fd306cSNickeau$railBar 173*04fd306cSNickeau<div id="$snippetClass" class="$snippetClass"> 174*04fd306cSNickeau$snippets 175*04fd306cSNickeau</div> 176*04fd306cSNickeauEOF; 177*04fd306cSNickeau } 178*04fd306cSNickeau 179*04fd306cSNickeau return $railBar; 180*04fd306cSNickeau 181*04fd306cSNickeau 182*04fd306cSNickeau } finally { 183*04fd306cSNickeau if ($localWikiRequest !== null) { 184*04fd306cSNickeau $localWikiRequest->close($localWikiId); 185*04fd306cSNickeau } 186*04fd306cSNickeau } 187*04fd306cSNickeau 188*04fd306cSNickeau } 189*04fd306cSNickeau 190*04fd306cSNickeau function getBuster(): string 191*04fd306cSNickeau { 192*04fd306cSNickeau return ""; 193*04fd306cSNickeau } 194*04fd306cSNickeau 195*04fd306cSNickeau public function getMime(): Mime 196*04fd306cSNickeau { 197*04fd306cSNickeau return Mime::getHtml(); 198*04fd306cSNickeau } 199*04fd306cSNickeau 200*04fd306cSNickeau public function getFetcherName(): string 201*04fd306cSNickeau { 202*04fd306cSNickeau return self::NAME; 203*04fd306cSNickeau } 204*04fd306cSNickeau 205*04fd306cSNickeau public function setRequestedPageWikiId(string $wikiId): FetcherRailBar 206*04fd306cSNickeau { 207*04fd306cSNickeau $path = WikiPath::createMarkupPathFromId($wikiId); 208*04fd306cSNickeau return $this->setRequestedPath($path); 209*04fd306cSNickeau } 210*04fd306cSNickeau 211*04fd306cSNickeau public static function getSnippetClass(): string 212*04fd306cSNickeau { 213*04fd306cSNickeau return Snippet::getClassFromComponentId(self::CANONICAL); 214*04fd306cSNickeau } 215*04fd306cSNickeau 216*04fd306cSNickeau private function getRailBarHtmlListItems(): string 217*04fd306cSNickeau { 218*04fd306cSNickeau $liUserTools = (new UserMenu())->getListItems('action'); 219*04fd306cSNickeau $pageMenu = new PageMenu(); 220*04fd306cSNickeau $liPageTools = $pageMenu->getListItems(); 221*04fd306cSNickeau $liSiteTools = (new SiteMenu())->getListItems('action'); 222*04fd306cSNickeau // FYI: The below code outputs all menu in mobile (in another HTML layout) 223*04fd306cSNickeau // echo (new \dokuwiki\Menu\MobileMenu())->getDropdown($lang['tools']); 224*04fd306cSNickeau $componentClass = self::getComponentClass(); 225*04fd306cSNickeau return <<<EOF 226*04fd306cSNickeau<ul class="$componentClass"> 227*04fd306cSNickeau <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> 228*04fd306cSNickeau $liUserTools 229*04fd306cSNickeau <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> 230*04fd306cSNickeau $liPageTools 231*04fd306cSNickeau <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> 232*04fd306cSNickeau $liSiteTools 233*04fd306cSNickeau</ul> 234*04fd306cSNickeauEOF; 235*04fd306cSNickeau 236*04fd306cSNickeau } 237*04fd306cSNickeau 238*04fd306cSNickeau private function toOffCanvasLayout(string $railBarHtmlListItems, Breakpoint $hideFromBreakpoint = null): string 239*04fd306cSNickeau { 240*04fd306cSNickeau $breakpointHiding = ""; 241*04fd306cSNickeau if ($hideFromBreakpoint !== null) { 242*04fd306cSNickeau $breakpointHiding = "d-{$hideFromBreakpoint->getShortName()}-none"; 243*04fd306cSNickeau } 244*04fd306cSNickeau $railBarOffCanvasPrefix = "railbar-offcanvas"; 245*04fd306cSNickeau $railBarClass = StyleAttribute::addComboStrapSuffix(self::NAME); 246*04fd306cSNickeau $railBarOffCanvasClassAndId = StyleAttribute::addComboStrapSuffix($railBarOffCanvasPrefix); 247*04fd306cSNickeau $railBarOffCanvasWrapperId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-wrapper"); 248*04fd306cSNickeau $railBarOffCanvasLabelId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-label"); 249*04fd306cSNickeau $railBarOffcanvasBodyId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-body"); 250*04fd306cSNickeau $railBarOffCanvasCloseId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-close"); 251*04fd306cSNickeau $railBarOffCanvasOpenId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-open"); 252*04fd306cSNickeau return <<<EOF 253*04fd306cSNickeau<div id="$railBarOffCanvasWrapperId" class="$railBarClass $railBarOffCanvasClassAndId $breakpointHiding"> 254*04fd306cSNickeau <button id="$railBarOffCanvasOpenId" class="btn" type="button" data-bs-toggle="offcanvas" 255*04fd306cSNickeau data-bs-target="#$railBarOffCanvasClassAndId" aria-controls="railbar-offcanvas"> 256*04fd306cSNickeau </button> 257*04fd306cSNickeau 258*04fd306cSNickeau <div id="$railBarOffCanvasClassAndId" class="offcanvas offcanvas-end" aria-labelledby="$railBarOffCanvasLabelId" 259*04fd306cSNickeau style="visibility: hidden;" aria-hidden="true"> 260*04fd306cSNickeau <h5 class="d-none" id="$railBarOffCanvasLabelId">Railbar</h5> 261*04fd306cSNickeau <!-- Pseudo relative element https://stackoverflow.com/questions/6040005/relatively-position-an-element-without-it-taking-up-space-in-document-flow --> 262*04fd306cSNickeau <div style="position: relative; width: 0; height: 0"> 263*04fd306cSNickeau <button id="$railBarOffCanvasCloseId" class="btn" type="button" data-bs-dismiss="offcanvas" aria-label="Close"></button> 264*04fd306cSNickeau </div> 265*04fd306cSNickeau <div id="$railBarOffcanvasBodyId" class="offcanvas-body" style="align-items: center;display: flex;"> 266*04fd306cSNickeau $railBarHtmlListItems 267*04fd306cSNickeau </div> 268*04fd306cSNickeau </div> 269*04fd306cSNickeau</div> 270*04fd306cSNickeauEOF; 271*04fd306cSNickeau 272*04fd306cSNickeau } 273*04fd306cSNickeau 274*04fd306cSNickeau public function getLayoutTypeToApply(): string 275*04fd306cSNickeau { 276*04fd306cSNickeau 277*04fd306cSNickeau if (isset($this->requestedLayout)) { 278*04fd306cSNickeau return $this->requestedLayout; 279*04fd306cSNickeau } 280*04fd306cSNickeau $bootstrapVersion = Bootstrap::getBootStrapMajorVersion(); 281*04fd306cSNickeau if ($bootstrapVersion === Bootstrap::BootStrapFourMajorVersion) { 282*04fd306cSNickeau return self::FIXED_LAYOUT; 283*04fd306cSNickeau } 284*04fd306cSNickeau try { 285*04fd306cSNickeau $breakPointConfigurationInPixel = $this->getBreakPointConfiguration()->getWidth(); 286*04fd306cSNickeau } catch (ExceptionInfinite $e) { 287*04fd306cSNickeau // no breakpoint 288*04fd306cSNickeau return self::OFFCANVAS_LAYOUT; 289*04fd306cSNickeau } 290*04fd306cSNickeau 291*04fd306cSNickeau try { 292*04fd306cSNickeau if ($this->getRequestedViewPort() > $breakPointConfigurationInPixel) { 293*04fd306cSNickeau return self::FIXED_LAYOUT; 294*04fd306cSNickeau } else { 295*04fd306cSNickeau return self::OFFCANVAS_LAYOUT; 296*04fd306cSNickeau } 297*04fd306cSNickeau } catch (ExceptionNotFound $e) { 298*04fd306cSNickeau // no known target view port 299*04fd306cSNickeau // we send them both then 300*04fd306cSNickeau return self::BOTH_LAYOUT; 301*04fd306cSNickeau } 302*04fd306cSNickeau 303*04fd306cSNickeau } 304*04fd306cSNickeau 305*04fd306cSNickeau public function setRequestedViewPort(int $viewPort): FetcherRailBar 306*04fd306cSNickeau { 307*04fd306cSNickeau $this->requestedViewPort = $viewPort; 308*04fd306cSNickeau return $this; 309*04fd306cSNickeau } 310*04fd306cSNickeau 311*04fd306cSNickeau /** 312*04fd306cSNickeau * The call may indicate the view port that the railbar will be used for 313*04fd306cSNickeau * (ie breakpoint) 314*04fd306cSNickeau * @return int 315*04fd306cSNickeau * @throws ExceptionNotFound 316*04fd306cSNickeau */ 317*04fd306cSNickeau public function getRequestedViewPort(): int 318*04fd306cSNickeau { 319*04fd306cSNickeau if (!isset($this->requestedViewPort)) { 320*04fd306cSNickeau throw new ExceptionNotFound("No requested view port"); 321*04fd306cSNickeau } 322*04fd306cSNickeau return $this->requestedViewPort; 323*04fd306cSNickeau } 324*04fd306cSNickeau 325*04fd306cSNickeau private function shouldBePrinted(): bool 326*04fd306cSNickeau { 327*04fd306cSNickeau 328*04fd306cSNickeau if ( 329*04fd306cSNickeau SiteConfig::getConfValue(self::CONF_PRIVATE_RAIL_BAR, 0) === 1 330*04fd306cSNickeau && !Identity::isLoggedIn() 331*04fd306cSNickeau ) { 332*04fd306cSNickeau return false; 333*04fd306cSNickeau } 334*04fd306cSNickeau return true; 335*04fd306cSNickeau 336*04fd306cSNickeau } 337*04fd306cSNickeau 338*04fd306cSNickeau private function getBreakPointConfiguration(): Breakpoint 339*04fd306cSNickeau { 340*04fd306cSNickeau $name = SiteConfig::getConfValue(self::CONF_BREAKPOINT_RAIL_BAR, Breakpoint::BREAKPOINT_LARGE_NAME); 341*04fd306cSNickeau return Breakpoint::createFromLongName($name); 342*04fd306cSNickeau } 343*04fd306cSNickeau 344*04fd306cSNickeau 345*04fd306cSNickeau /** 346*04fd306cSNickeau * @param string $railBarHtmlListItems 347*04fd306cSNickeau * @param Breakpoint|null $showFromBreakpoint 348*04fd306cSNickeau * @return string 349*04fd306cSNickeau */ 350*04fd306cSNickeau private function toFixedLayout(string $railBarHtmlListItems, Breakpoint $showFromBreakpoint = null): string 351*04fd306cSNickeau { 352*04fd306cSNickeau $showFromBreakpointClasses = ""; 353*04fd306cSNickeau if ($showFromBreakpoint !== null) { 354*04fd306cSNickeau $showFromBreakpointClasses = "d-none d-{$showFromBreakpoint->getShortName()}-flex"; 355*04fd306cSNickeau } 356*04fd306cSNickeau $railBarClass = StyleAttribute::addComboStrapSuffix(self::NAME); 357*04fd306cSNickeau $railBarFixedClassOrId = StyleAttribute::addComboStrapSuffix(self::NAME . "-fixed"); 358*04fd306cSNickeau $zIndexRailbar = 1000; // A navigation bar (below the drop down because we use it in the search box for auto-completion) 359*04fd306cSNickeau return <<<EOF 360*04fd306cSNickeau<div id="$railBarFixedClassOrId" class="$railBarClass $railBarFixedClassOrId d-flex $showFromBreakpointClasses" style="z-index: $zIndexRailbar;"> 361*04fd306cSNickeau <div> 362*04fd306cSNickeau $railBarHtmlListItems 363*04fd306cSNickeau </div> 364*04fd306cSNickeau</div> 365*04fd306cSNickeauEOF; 366*04fd306cSNickeau 367*04fd306cSNickeau } 368*04fd306cSNickeau 369*04fd306cSNickeau /** 370*04fd306cSNickeau * The layout may be requested (example in a landing page where you don't want to see it) 371*04fd306cSNickeau * @param string $layout 372*04fd306cSNickeau * @return FetcherRailBar 373*04fd306cSNickeau * @throws ExceptionBadArgument 374*04fd306cSNickeau */ 375*04fd306cSNickeau public function setRequestedLayout(string $layout): FetcherRailBar 376*04fd306cSNickeau { 377*04fd306cSNickeau if (!in_array($layout, self::KNOWN_LAYOUT)) { 378*04fd306cSNickeau throw new ExceptionBadArgument("The layout ($layout) is not valid. The known-layout are : ".ArrayUtility::formatAsString(self::KNOWN_LAYOUT)); 379*04fd306cSNickeau } 380*04fd306cSNickeau $this->requestedLayout = $layout; 381*04fd306cSNickeau return $this; 382*04fd306cSNickeau } 383*04fd306cSNickeau 384*04fd306cSNickeau public function setRequestedPath(WikiPath $requestedPath): FetcherRailBar 385*04fd306cSNickeau { 386*04fd306cSNickeau $this->setSourcePath($requestedPath); 387*04fd306cSNickeau return $this; 388*04fd306cSNickeau } 389*04fd306cSNickeau 390*04fd306cSNickeau 391*04fd306cSNickeau public function getLabel(): string 392*04fd306cSNickeau { 393*04fd306cSNickeau return self::NAME; 394*04fd306cSNickeau } 395*04fd306cSNickeau 396*04fd306cSNickeau} 397