xref: /plugin/combo/ComboStrap/FetcherRailBar.php (revision 3e9dc6bf218c8fa40e4f2d8a21ce99a93d1207cc)
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" aria-label="Open the railbar" 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