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