xref: /template/strap/ComboStrap/FetcherRailBar.php (revision 8b8569b7384a60bf4f631e3aa3bd7f9aab461558)
104fd306cSNickeau<?php
204fd306cSNickeau
304fd306cSNickeaunamespace ComboStrap;
404fd306cSNickeau
504fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute;
604fd306cSNickeauuse dokuwiki\Menu\PageMenu;
704fd306cSNickeauuse dokuwiki\Menu\SiteMenu;
804fd306cSNickeauuse dokuwiki\Menu\UserMenu;
904fd306cSNickeau
1004fd306cSNickeau/**
1104fd306cSNickeau * A fetcher for a menu rail bar
1204fd306cSNickeau * https://material.io/components/navigation-rail
1304fd306cSNickeau *
1404fd306cSNickeau *
1504fd306cSNickeau * Note: this class is a fetcher but it does not still work to call it via a javascript function included in the page.
1604fd306cSNickeau * Why ? The problem is that plugins that add a item would expect
1704fd306cSNickeau * to be loaded with the page and the related javascript is generally wrapped in a listener waiting for the page load event.
1804fd306cSNickeau * It means that it would never be triggered.
1904fd306cSNickeau *
2004fd306cSNickeau */
2104fd306cSNickeauclass FetcherRailBar extends IFetcherAbs implements IFetcherString
2204fd306cSNickeau{
2304fd306cSNickeau
2404fd306cSNickeau    use FetcherTraitWikiPath;
2504fd306cSNickeau
2604fd306cSNickeau    const CANONICAL = self::NAME;
2704fd306cSNickeau    const NAME = "railbar";
2804fd306cSNickeau    const FIXED_LAYOUT = "fixed";
2904fd306cSNickeau    const OFFCANVAS_LAYOUT = "off-canvas";
3004fd306cSNickeau    const VIEWPORT_WIDTH = "viewport";
3104fd306cSNickeau    const LAYOUT_ATTRIBUTE = "layout";
3204fd306cSNickeau    /**
3304fd306cSNickeau     * Do we show the rail bar for anonymous user
3404fd306cSNickeau     */
3504fd306cSNickeau    public const CONF_PRIVATE_RAIL_BAR = "privateRailbar";
3604fd306cSNickeau    /**
3704fd306cSNickeau     * When do we toggle from offcanvas to fixed railbar
3804fd306cSNickeau     */
3904fd306cSNickeau    public const CONF_BREAKPOINT_RAIL_BAR = "breakpointRailbar";
4004fd306cSNickeau    const BOTH_LAYOUT = "all";
4104fd306cSNickeau    const KNOWN_LAYOUT = [self::FIXED_LAYOUT, self::OFFCANVAS_LAYOUT, self::BOTH_LAYOUT];
4204fd306cSNickeau
4304fd306cSNickeau
4404fd306cSNickeau    private int $requestedViewPort;
4504fd306cSNickeau    private string $requestedLayout;
4604fd306cSNickeau
4704fd306cSNickeau
4804fd306cSNickeau    public static function createRailBar(): FetcherRailBar
4904fd306cSNickeau    {
5004fd306cSNickeau        return new FetcherRailBar();
5104fd306cSNickeau    }
5204fd306cSNickeau
5304fd306cSNickeau    private static function getComponentClass(): string
5404fd306cSNickeau    {
5504fd306cSNickeau        return StyleAttribute::addComboStrapSuffix(self::CANONICAL);
5604fd306cSNickeau    }
5704fd306cSNickeau
5804fd306cSNickeau    /**
5904fd306cSNickeau     * @throws ExceptionBadArgument
6004fd306cSNickeau     * @throws ExceptionBadSyntax
6104fd306cSNickeau     * @throws ExceptionNotExists
6204fd306cSNickeau     * @throws ExceptionNotFound
6304fd306cSNickeau     */
6404fd306cSNickeau    public function buildFromTagAttributes(TagAttributes $tagAttributes): IFetcher
6504fd306cSNickeau    {
6604fd306cSNickeau        /**
6704fd306cSNickeau         * Capture the id
6804fd306cSNickeau         */
6904fd306cSNickeau        $this->buildOriginalPathFromTagAttributes($tagAttributes);
7004fd306cSNickeau        /**
7104fd306cSNickeau         * Capture the view port
7204fd306cSNickeau         */
7304fd306cSNickeau        $viewPortWidth = $tagAttributes->getValueAndRemoveIfPresent(self::VIEWPORT_WIDTH);
7404fd306cSNickeau        if ($viewPortWidth !== null) {
7504fd306cSNickeau            try {
7604fd306cSNickeau                $this->setRequestedViewPort(DataType::toInteger($viewPortWidth));
7704fd306cSNickeau            } catch (ExceptionBadArgument $e) {
7804fd306cSNickeau                throw new ExceptionBadArgument("The viewport width is not a valid integer. Error:{$e->getMessage()}", self::CANONICAL);
7904fd306cSNickeau            }
8004fd306cSNickeau        }
8104fd306cSNickeau        /**
8204fd306cSNickeau         * Capture the layout
8304fd306cSNickeau         */
8404fd306cSNickeau        $layout = $tagAttributes->getValueAndRemoveIfPresent(self::LAYOUT_ATTRIBUTE);
8504fd306cSNickeau        if ($layout !== null) {
8604fd306cSNickeau            try {
8704fd306cSNickeau                $this->setRequestedLayout($layout);
8804fd306cSNickeau            } catch (ExceptionBadArgument $e) {
8904fd306cSNickeau                throw new ExceptionBadArgument("The layout is not a valid. Error:{$e->getMessage()}", self::CANONICAL);
9004fd306cSNickeau            }
9104fd306cSNickeau        }
9204fd306cSNickeau        return parent::buildFromTagAttributes($tagAttributes);
9304fd306cSNickeau    }
9404fd306cSNickeau
9504fd306cSNickeau
9604fd306cSNickeau    function getFetchPath(): Path
9704fd306cSNickeau    {
9804fd306cSNickeau        throw new ExceptionRuntimeInternal("No fetch path: Railbar is not a file but a dynamic HTML document");
9904fd306cSNickeau    }
10004fd306cSNickeau
10104fd306cSNickeau    function getFetchString(): string
10204fd306cSNickeau    {
10304fd306cSNickeau
10404fd306cSNickeau        if (!$this->shouldBePrinted()) {
10504fd306cSNickeau            return "";
10604fd306cSNickeau        }
10704fd306cSNickeau
10804fd306cSNickeau        $localWikiRequest = null;
10904fd306cSNickeau        $localWikiId = null;
11004fd306cSNickeau        try {
11104fd306cSNickeau            ExecutionContext::getExecutionContext();
11204fd306cSNickeau        } catch (ExceptionNotFound $e) {
11304fd306cSNickeau
11404fd306cSNickeau            /**
11504fd306cSNickeau             * No actual request (called via ajax)
11604fd306cSNickeau             */
11704fd306cSNickeau            $localWikiId = $this->getSourcePath()->getWikiId();
11804fd306cSNickeau            $localWikiRequest = ExecutionContext::getOrCreateFromRequestedWikiId($localWikiId);
11904fd306cSNickeau
12004fd306cSNickeau            /**
12104fd306cSNickeau             * page info is needed and used by all other plugins
12204fd306cSNickeau             * in all hooks (should be first)
12304fd306cSNickeau             */
12404fd306cSNickeau            global $INFO;
12504fd306cSNickeau            $INFO = pageinfo();
12604fd306cSNickeau
12704fd306cSNickeau            /**
12804fd306cSNickeau             * Uses by {@link action_plugin_move_rename} to set
12904fd306cSNickeau             * if it will add the button
13004fd306cSNickeau             */
13104fd306cSNickeau            $tmp = array();
13204fd306cSNickeau            \dokuwiki\Extension\Event::createAndTrigger('DOKUWIKI_STARTED', $tmp);
13304fd306cSNickeau
13404fd306cSNickeau        }
13504fd306cSNickeau
13604fd306cSNickeau
13704fd306cSNickeau        try {
13804fd306cSNickeau
13904fd306cSNickeau            $snippetManager = SnippetSystem::getFromContext();
14004fd306cSNickeau            $railBarHtmlListItems = $this->getRailBarHtmlListItems();
14104fd306cSNickeau            $railBarLayout = $this->getLayoutTypeToApply();
14204fd306cSNickeau            switch ($railBarLayout) {
14304fd306cSNickeau                case self::FIXED_LAYOUT:
14404fd306cSNickeau                    $railBar = $this->toFixedLayout($railBarHtmlListItems);
14504fd306cSNickeau                    $snippetManager->attachCssInternalStylesheet("railbar-$railBarLayout");
14604fd306cSNickeau                    break;
14704fd306cSNickeau                case self::OFFCANVAS_LAYOUT:
14804fd306cSNickeau                    $railBar = $this->toOffCanvasLayout($railBarHtmlListItems);
14904fd306cSNickeau                    $snippetManager->attachCssInternalStylesheet("railbar-$railBarLayout");
15004fd306cSNickeau                    break;
15104fd306cSNickeau                case self::BOTH_LAYOUT:
15204fd306cSNickeau                default:
15304fd306cSNickeau                    $snippetManager->attachCssInternalStylesheet("railbar-" . self::FIXED_LAYOUT);
15404fd306cSNickeau                    $snippetManager->attachCssInternalStylesheet("railbar-" . self::OFFCANVAS_LAYOUT);
15504fd306cSNickeau                    $breakpoint = $this->getBreakPointConfiguration();
15604fd306cSNickeau                    $railBar = $this->toFixedLayout($railBarHtmlListItems, $breakpoint)
15704fd306cSNickeau                        . $this->toOffCanvasLayout($railBarHtmlListItems, $breakpoint);
15804fd306cSNickeau                    break;
15904fd306cSNickeau            }
16004fd306cSNickeau
16104fd306cSNickeau
16204fd306cSNickeau            $snippetManager->attachCssInternalStylesheet("railbar");
16304fd306cSNickeau
16404fd306cSNickeau            if ($localWikiRequest !== null) {
16504fd306cSNickeau                $snippets = $snippetManager->toHtmlForAllSnippets();
16604fd306cSNickeau                $snippetClass = self::getSnippetClass();
16704fd306cSNickeau                /**
16804fd306cSNickeau                 * Snippets should be after the html because they works
16904fd306cSNickeau                 * on the added HTML
17004fd306cSNickeau                 */
17104fd306cSNickeau                $railBar = <<<EOF
17204fd306cSNickeau$railBar
17304fd306cSNickeau<div id="$snippetClass" class="$snippetClass">
17404fd306cSNickeau$snippets
17504fd306cSNickeau</div>
17604fd306cSNickeauEOF;
17704fd306cSNickeau            }
17804fd306cSNickeau
17904fd306cSNickeau            return $railBar;
18004fd306cSNickeau
18104fd306cSNickeau
18204fd306cSNickeau        } finally {
18304fd306cSNickeau            if ($localWikiRequest !== null) {
18404fd306cSNickeau                $localWikiRequest->close($localWikiId);
18504fd306cSNickeau            }
18604fd306cSNickeau        }
18704fd306cSNickeau
18804fd306cSNickeau    }
18904fd306cSNickeau
19004fd306cSNickeau    function getBuster(): string
19104fd306cSNickeau    {
19204fd306cSNickeau        return "";
19304fd306cSNickeau    }
19404fd306cSNickeau
19504fd306cSNickeau    public function getMime(): Mime
19604fd306cSNickeau    {
19704fd306cSNickeau        return Mime::getHtml();
19804fd306cSNickeau    }
19904fd306cSNickeau
20004fd306cSNickeau    public function getFetcherName(): string
20104fd306cSNickeau    {
20204fd306cSNickeau        return self::NAME;
20304fd306cSNickeau    }
20404fd306cSNickeau
20504fd306cSNickeau    public function setRequestedPageWikiId(string $wikiId): FetcherRailBar
20604fd306cSNickeau    {
20704fd306cSNickeau        $path = WikiPath::createMarkupPathFromId($wikiId);
20804fd306cSNickeau        return $this->setRequestedPath($path);
20904fd306cSNickeau    }
21004fd306cSNickeau
21104fd306cSNickeau    public static function getSnippetClass(): string
21204fd306cSNickeau    {
21304fd306cSNickeau        return Snippet::getClassFromComponentId(self::CANONICAL);
21404fd306cSNickeau    }
21504fd306cSNickeau
21604fd306cSNickeau    private function getRailBarHtmlListItems(): string
21704fd306cSNickeau    {
21804fd306cSNickeau        $liUserTools = (new UserMenu())->getListItems('action');
21904fd306cSNickeau        $pageMenu = new PageMenu();
22004fd306cSNickeau        $liPageTools = $pageMenu->getListItems();
22104fd306cSNickeau        $liSiteTools = (new SiteMenu())->getListItems('action');
22204fd306cSNickeau        // FYI: The below code outputs all menu in mobile (in another HTML layout)
22304fd306cSNickeau        // echo (new \dokuwiki\Menu\MobileMenu())->getDropdown($lang['tools']);
22404fd306cSNickeau        $componentClass = self::getComponentClass();
22504fd306cSNickeau        return <<<EOF
22604fd306cSNickeau<ul class="$componentClass">
22704fd306cSNickeau    <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>
22804fd306cSNickeau    $liUserTools
22904fd306cSNickeau    <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>
23004fd306cSNickeau    $liPageTools
23104fd306cSNickeau    <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>
23204fd306cSNickeau    $liSiteTools
23304fd306cSNickeau</ul>
23404fd306cSNickeauEOF;
23504fd306cSNickeau
23604fd306cSNickeau    }
23704fd306cSNickeau
23804fd306cSNickeau    private function toOffCanvasLayout(string $railBarHtmlListItems, Breakpoint $hideFromBreakpoint = null): string
23904fd306cSNickeau    {
24004fd306cSNickeau        $breakpointHiding = "";
24104fd306cSNickeau        if ($hideFromBreakpoint !== null) {
24204fd306cSNickeau            $breakpointHiding = "d-{$hideFromBreakpoint->getShortName()}-none";
24304fd306cSNickeau        }
24404fd306cSNickeau        $railBarOffCanvasPrefix = "railbar-offcanvas";
24504fd306cSNickeau        $railBarClass = StyleAttribute::addComboStrapSuffix(self::NAME);
24604fd306cSNickeau        $railBarOffCanvasClassAndId = StyleAttribute::addComboStrapSuffix($railBarOffCanvasPrefix);
24704fd306cSNickeau        $railBarOffCanvasWrapperId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-wrapper");
24804fd306cSNickeau        $railBarOffCanvasLabelId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-label");
24904fd306cSNickeau        $railBarOffcanvasBodyId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-body");
25004fd306cSNickeau        $railBarOffCanvasCloseId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-close");
25104fd306cSNickeau        $railBarOffCanvasOpenId = StyleAttribute::addComboStrapSuffix("{$railBarOffCanvasPrefix}-open");
25204fd306cSNickeau        return <<<EOF
25304fd306cSNickeau<div id="$railBarOffCanvasWrapperId" class="$railBarClass $railBarOffCanvasClassAndId $breakpointHiding">
254*8b8569b7Sgerardnico    <button id="$railBarOffCanvasOpenId" class="btn" type="button" aria-label="Open the railbar" data-bs-toggle="offcanvas"
25504fd306cSNickeau            data-bs-target="#$railBarOffCanvasClassAndId" aria-controls="railbar-offcanvas">
25604fd306cSNickeau    </button>
25704fd306cSNickeau
25804fd306cSNickeau    <div id="$railBarOffCanvasClassAndId" class="offcanvas offcanvas-end" aria-labelledby="$railBarOffCanvasLabelId"
25904fd306cSNickeau         style="visibility: hidden;" aria-hidden="true">
26004fd306cSNickeau         <h5 class="d-none" id="$railBarOffCanvasLabelId">Railbar</h5>
26104fd306cSNickeau        <!-- Pseudo relative element  https://stackoverflow.com/questions/6040005/relatively-position-an-element-without-it-taking-up-space-in-document-flow -->
26204fd306cSNickeau        <div style="position: relative; width: 0; height: 0">
26304fd306cSNickeau            <button id="$railBarOffCanvasCloseId" class="btn" type="button" data-bs-dismiss="offcanvas" aria-label="Close"></button>
26404fd306cSNickeau        </div>
26504fd306cSNickeau        <div id="$railBarOffcanvasBodyId" class="offcanvas-body" style="align-items: center;display: flex;">
26604fd306cSNickeau            $railBarHtmlListItems
26704fd306cSNickeau        </div>
26804fd306cSNickeau    </div>
26904fd306cSNickeau</div>
27004fd306cSNickeauEOF;
27104fd306cSNickeau
27204fd306cSNickeau    }
27304fd306cSNickeau
27404fd306cSNickeau    public function getLayoutTypeToApply(): string
27504fd306cSNickeau    {
27604fd306cSNickeau
27704fd306cSNickeau        if (isset($this->requestedLayout)) {
27804fd306cSNickeau            return $this->requestedLayout;
27904fd306cSNickeau        }
28004fd306cSNickeau        $bootstrapVersion = Bootstrap::getBootStrapMajorVersion();
28104fd306cSNickeau        if ($bootstrapVersion === Bootstrap::BootStrapFourMajorVersion) {
28204fd306cSNickeau            return self::FIXED_LAYOUT;
28304fd306cSNickeau        }
28404fd306cSNickeau        try {
28504fd306cSNickeau            $breakPointConfigurationInPixel = $this->getBreakPointConfiguration()->getWidth();
28604fd306cSNickeau        } catch (ExceptionInfinite $e) {
28704fd306cSNickeau            // no breakpoint
28804fd306cSNickeau            return self::OFFCANVAS_LAYOUT;
28904fd306cSNickeau        }
29004fd306cSNickeau
29104fd306cSNickeau        try {
29204fd306cSNickeau            if ($this->getRequestedViewPort() > $breakPointConfigurationInPixel) {
29304fd306cSNickeau                return self::FIXED_LAYOUT;
29404fd306cSNickeau            } else {
29504fd306cSNickeau                return self::OFFCANVAS_LAYOUT;
29604fd306cSNickeau            }
29704fd306cSNickeau        } catch (ExceptionNotFound $e) {
29804fd306cSNickeau            // no known target view port
29904fd306cSNickeau            // we send them both then
30004fd306cSNickeau            return self::BOTH_LAYOUT;
30104fd306cSNickeau        }
30204fd306cSNickeau
30304fd306cSNickeau    }
30404fd306cSNickeau
30504fd306cSNickeau    public function setRequestedViewPort(int $viewPort): FetcherRailBar
30604fd306cSNickeau    {
30704fd306cSNickeau        $this->requestedViewPort = $viewPort;
30804fd306cSNickeau        return $this;
30904fd306cSNickeau    }
31004fd306cSNickeau
31104fd306cSNickeau    /**
31204fd306cSNickeau     * The call may indicate the view port that the railbar will be used for
31304fd306cSNickeau     * (ie breakpoint)
31404fd306cSNickeau     * @return int
31504fd306cSNickeau     * @throws ExceptionNotFound
31604fd306cSNickeau     */
31704fd306cSNickeau    public function getRequestedViewPort(): int
31804fd306cSNickeau    {
31904fd306cSNickeau        if (!isset($this->requestedViewPort)) {
32004fd306cSNickeau            throw new ExceptionNotFound("No requested view port");
32104fd306cSNickeau        }
32204fd306cSNickeau        return $this->requestedViewPort;
32304fd306cSNickeau    }
32404fd306cSNickeau
32504fd306cSNickeau    private function shouldBePrinted(): bool
32604fd306cSNickeau    {
32704fd306cSNickeau
32804fd306cSNickeau        if (
32904fd306cSNickeau            SiteConfig::getConfValue(self::CONF_PRIVATE_RAIL_BAR, 0) === 1
33004fd306cSNickeau            && !Identity::isLoggedIn()
33104fd306cSNickeau        ) {
33204fd306cSNickeau            return false;
33304fd306cSNickeau        }
33404fd306cSNickeau        return true;
33504fd306cSNickeau
33604fd306cSNickeau    }
33704fd306cSNickeau
33804fd306cSNickeau    private function getBreakPointConfiguration(): Breakpoint
33904fd306cSNickeau    {
34004fd306cSNickeau        $name = SiteConfig::getConfValue(self::CONF_BREAKPOINT_RAIL_BAR, Breakpoint::BREAKPOINT_LARGE_NAME);
34104fd306cSNickeau        return Breakpoint::createFromLongName($name);
34204fd306cSNickeau    }
34304fd306cSNickeau
34404fd306cSNickeau
34504fd306cSNickeau    /**
34604fd306cSNickeau     * @param string $railBarHtmlListItems
34704fd306cSNickeau     * @param Breakpoint|null $showFromBreakpoint
34804fd306cSNickeau     * @return string
34904fd306cSNickeau     */
35004fd306cSNickeau    private function toFixedLayout(string $railBarHtmlListItems, Breakpoint $showFromBreakpoint = null): string
35104fd306cSNickeau    {
35204fd306cSNickeau        $showFromBreakpointClasses = "";
35304fd306cSNickeau        if ($showFromBreakpoint !== null) {
35404fd306cSNickeau            $showFromBreakpointClasses = "d-none d-{$showFromBreakpoint->getShortName()}-flex";
35504fd306cSNickeau        }
35604fd306cSNickeau        $railBarClass = StyleAttribute::addComboStrapSuffix(self::NAME);
35704fd306cSNickeau        $railBarFixedClassOrId = StyleAttribute::addComboStrapSuffix(self::NAME . "-fixed");
35804fd306cSNickeau        $zIndexRailbar = 1000; // A navigation bar (below the drop down because we use it in the search box for auto-completion)
35904fd306cSNickeau        return <<<EOF
36004fd306cSNickeau<div id="$railBarFixedClassOrId" class="$railBarClass $railBarFixedClassOrId d-flex $showFromBreakpointClasses" style="z-index: $zIndexRailbar;">
36104fd306cSNickeau    <div>
36204fd306cSNickeau        $railBarHtmlListItems
36304fd306cSNickeau    </div>
36404fd306cSNickeau</div>
36504fd306cSNickeauEOF;
36604fd306cSNickeau
36704fd306cSNickeau    }
36804fd306cSNickeau
36904fd306cSNickeau    /**
37004fd306cSNickeau     * The layout may be requested (example in a landing page where you don't want to see it)
37104fd306cSNickeau     * @param string $layout
37204fd306cSNickeau     * @return FetcherRailBar
37304fd306cSNickeau     * @throws ExceptionBadArgument
37404fd306cSNickeau     */
37504fd306cSNickeau    public function setRequestedLayout(string $layout): FetcherRailBar
37604fd306cSNickeau    {
37704fd306cSNickeau        if (!in_array($layout, self::KNOWN_LAYOUT)) {
37804fd306cSNickeau            throw new ExceptionBadArgument("The layout ($layout) is not valid. The known-layout are : ".ArrayUtility::formatAsString(self::KNOWN_LAYOUT));
37904fd306cSNickeau        }
38004fd306cSNickeau        $this->requestedLayout = $layout;
38104fd306cSNickeau        return $this;
38204fd306cSNickeau    }
38304fd306cSNickeau
38404fd306cSNickeau    public function setRequestedPath(WikiPath $requestedPath): FetcherRailBar
38504fd306cSNickeau    {
38604fd306cSNickeau        $this->setSourcePath($requestedPath);
38704fd306cSNickeau        return $this;
38804fd306cSNickeau    }
38904fd306cSNickeau
39004fd306cSNickeau
39104fd306cSNickeau    public function getLabel(): string
39204fd306cSNickeau    {
39304fd306cSNickeau        return self::NAME;
39404fd306cSNickeau    }
39504fd306cSNickeau
39604fd306cSNickeau}
397