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