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