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