xref: /plugin/combo/ComboStrap/TabsTag.php (revision 1e6623d9be7c26643a021b887e027c9b902710d7)
104fd306cSNickeau<?php
204fd306cSNickeau
304fd306cSNickeaunamespace ComboStrap;
404fd306cSNickeau
504fd306cSNickeau
604fd306cSNickeauuse syntax_plugin_combo_label;
704fd306cSNickeauuse syntax_plugin_combo_tab;
804fd306cSNickeau
904fd306cSNickeau/**
1004fd306cSNickeau * The tabs component is a little bit a nasty one
1104fd306cSNickeau * because it's used in three cases:
1204fd306cSNickeau *   * the new syntax to enclose the panels
1304fd306cSNickeau *   * the new syntax to create the tabs
1404fd306cSNickeau *   * the old syntax to create the tabs
1504fd306cSNickeau * The code is using the context to manage this cases
1604fd306cSNickeau *
1704fd306cSNickeau * Full example can be found
1804fd306cSNickeau * in the Javascript section of tabs and navs
1904fd306cSNickeau * https://getbootstrap.com/docs/5.0/components/navs-tabs/#javascript-behavior
2004fd306cSNickeau *
2104fd306cSNickeau * Vertical Pills
2204fd306cSNickeau * https://getbootstrap.com/docs/4.0/components/navs/#vertical
23*1e6623d9Sgerardnico *
24*1e6623d9Sgerardnico * React: https://react.dev/reference/react/Children#calling-a-render-prop-to-customize-rendering
2504fd306cSNickeau */
2604fd306cSNickeauclass TabsTag
2704fd306cSNickeau{
2804fd306cSNickeau
2904fd306cSNickeau
3004fd306cSNickeau    /**
3104fd306cSNickeau     * A tabs with this context will render the `ul` HTML tags
3204fd306cSNickeau     * (ie tabs enclose the navigation partition)
3304fd306cSNickeau     */
3404fd306cSNickeau    public const NAVIGATION_CONTEXT = "navigation-context";
3504fd306cSNickeau    public const PILLS_TYPE = "pills";
3604fd306cSNickeau    public const TABS_SKIN = "tabs";
3704fd306cSNickeau    public const ENCLOSED_TABS_TYPE = "enclosed-tabs";
3804fd306cSNickeau    public const ENCLOSED_PILLS_TYPE = "enclosed-pills";
3904fd306cSNickeau    public const LABEL = 'label';
4004fd306cSNickeau    public const SELECTED_ATTRIBUTE = "selected";
4104fd306cSNickeau    public const TAG = 'tabs';
4204fd306cSNickeau    /**
4304fd306cSNickeau     * A key attributes to set on in the instructions the attributes
4404fd306cSNickeau     * of panel
4504fd306cSNickeau     */
4604fd306cSNickeau    public const KEY_PANEL_ATTRIBUTES = "panels";
4704fd306cSNickeau    /**
4804fd306cSNickeau     * Type tabs
4904fd306cSNickeau     */
5004fd306cSNickeau    public const TABS_TYPE = "tabs";
5104fd306cSNickeau    public const PILLS_SKIN = "pills";
5204fd306cSNickeau
5304fd306cSNickeau    public static function closeNavigationalHeaderComponent($type)
5404fd306cSNickeau    {
5504fd306cSNickeau        $html = "</ul>" . DOKU_LF;
5604fd306cSNickeau        switch ($type) {
5704fd306cSNickeau            case self::ENCLOSED_PILLS_TYPE:
5804fd306cSNickeau            case self::ENCLOSED_TABS_TYPE:
5904fd306cSNickeau                $html .= "</div>" . DOKU_LF;
6004fd306cSNickeau        }
6104fd306cSNickeau        return $html;
6204fd306cSNickeau
6304fd306cSNickeau    }
6404fd306cSNickeau
6504fd306cSNickeau    /**
6604fd306cSNickeau     * @param $tagAttributes
6704fd306cSNickeau     * @return string - the opening HTML code of the tab navigational header
6804fd306cSNickeau     */
6904fd306cSNickeau    public static function openNavigationalTabsElement(TagAttributes $tagAttributes): string
7004fd306cSNickeau    {
7104fd306cSNickeau
7204fd306cSNickeau        /**
7304fd306cSNickeau         * Unset non-html attributes
7404fd306cSNickeau         */
7504fd306cSNickeau        $tagAttributes->removeComponentAttributeIfPresent(self::KEY_PANEL_ATTRIBUTES);
7604fd306cSNickeau
7704fd306cSNickeau        /**
7804fd306cSNickeau         * Type (Skin determination)
7904fd306cSNickeau         */
8004fd306cSNickeau        $type = self::getComponentType($tagAttributes);
8104fd306cSNickeau
8204fd306cSNickeau        /**
8304fd306cSNickeau         * $skin (tabs or pills)
8404fd306cSNickeau         */
8504fd306cSNickeau        $skin = self::TABS_TYPE;
8604fd306cSNickeau        switch ($type) {
8704fd306cSNickeau            case self::TABS_TYPE:
8804fd306cSNickeau            case self::ENCLOSED_TABS_TYPE:
8904fd306cSNickeau                $skin = self::TABS_SKIN;
9004fd306cSNickeau                break;
9104fd306cSNickeau            case self::PILLS_TYPE:
9204fd306cSNickeau            case self::ENCLOSED_PILLS_TYPE:
9304fd306cSNickeau                $skin = self::PILLS_SKIN;
9404fd306cSNickeau                break;
9504fd306cSNickeau            default:
9604fd306cSNickeau                LogUtility::warning("The tabs type ($type) has an unknown skin", self::TAG);
9704fd306cSNickeau        }
9804fd306cSNickeau
9904fd306cSNickeau        /**
10004fd306cSNickeau         * Creates the panel wrapper element
10104fd306cSNickeau         */
10204fd306cSNickeau        $html = "";
10304fd306cSNickeau        switch ($type) {
10404fd306cSNickeau            case self::TABS_TYPE:
10504fd306cSNickeau            case self::PILLS_TYPE:
10604fd306cSNickeau                if (!$tagAttributes->hasAttribute(Spacing::SPACING_ATTRIBUTE)) {
10704fd306cSNickeau                    $tagAttributes->addComponentAttributeValue(Spacing::SPACING_ATTRIBUTE, "mb-3");
10804fd306cSNickeau                }
10904fd306cSNickeau                $tagAttributes->addClassName("nav")
11004fd306cSNickeau                    ->addClassName("nav-$skin");
11104fd306cSNickeau                $tagAttributes->addOutputAttributeValue('role', 'tablist');
11204fd306cSNickeau                $html = $tagAttributes->toHtmlEnterTag("ul");
11304fd306cSNickeau                break;
11404fd306cSNickeau            case self::ENCLOSED_TABS_TYPE:
11504fd306cSNickeau            case self::ENCLOSED_PILLS_TYPE:
11604fd306cSNickeau                /**
11704fd306cSNickeau                 * The HTML opening for cards
11804fd306cSNickeau                 */
11904fd306cSNickeau                $tagAttributes->addClassName("card");
12004fd306cSNickeau                $html = $tagAttributes->toHtmlEnterTag("div") .
12104fd306cSNickeau                    "<div class=\"card-header\">";
12204fd306cSNickeau                /**
12304fd306cSNickeau                 * The HTML opening for the menu (UL)
12404fd306cSNickeau                 */
12504fd306cSNickeau                $html .= TagAttributes::createEmpty()
12604fd306cSNickeau                    ->addClassName("nav")
12704fd306cSNickeau                    ->addClassName("nav-$skin")
12804fd306cSNickeau                    ->addClassName("card-header-$skin")
12904fd306cSNickeau                    ->toHtmlEnterTag("ul");
13004fd306cSNickeau                break;
13104fd306cSNickeau            default:
13204fd306cSNickeau                LogUtility::error("The tabs type ($type) is unknown", self::TAG);
13304fd306cSNickeau        }
13404fd306cSNickeau        return $html;
13504fd306cSNickeau
13604fd306cSNickeau    }
13704fd306cSNickeau
13804fd306cSNickeau    /**
13904fd306cSNickeau     * @param array $attributes
14004fd306cSNickeau     * @return string
14104fd306cSNickeau     */
14204fd306cSNickeau    public static function openNavigationalTabElement(array $attributes): string
14304fd306cSNickeau    {
14404fd306cSNickeau        $liTagAttributes = TagAttributes::createFromCallStackArray($attributes);
14504fd306cSNickeau
14604fd306cSNickeau        /**
14704fd306cSNickeau         * Check all attributes for the link (not the li)
14804fd306cSNickeau         * and delete them
14904fd306cSNickeau         */
15004fd306cSNickeau        $active = PanelTag::getSelectedValue($liTagAttributes);
15104fd306cSNickeau        $panel = "";
15204fd306cSNickeau
15304fd306cSNickeau
15404fd306cSNickeau        $panel = $liTagAttributes->getValueAndRemoveIfPresent("panel");
15504fd306cSNickeau        if ($panel === null && $liTagAttributes->hasComponentAttribute("id")) {
15604fd306cSNickeau            $panel = $liTagAttributes->getValueAndRemoveIfPresent("id");
15704fd306cSNickeau        }
15804fd306cSNickeau        if ($panel === null) {
15904fd306cSNickeau            LogUtility::msg("A id attribute is missing on a panel tag", LogUtility::LVL_MSG_ERROR, TabsTag::TAG);
16004fd306cSNickeau        }
16104fd306cSNickeau
16204fd306cSNickeau
16304fd306cSNickeau        /**
16404fd306cSNickeau         * Creating the li element
16504fd306cSNickeau         */
16604fd306cSNickeau        $html = $liTagAttributes->addClassName("nav-item")
16704fd306cSNickeau            ->addOutputAttributeValue("role", "presentation")
16804fd306cSNickeau            ->toHtmlEnterTag("li");
16904fd306cSNickeau
17004fd306cSNickeau        /**
17104fd306cSNickeau         * Creating the a element
17204fd306cSNickeau         */
17304fd306cSNickeau        $namespace = Bootstrap::getDataNamespace();
17404fd306cSNickeau        $htmlAttributes = TagAttributes::createEmpty();
17504fd306cSNickeau        if ($active === true) {
17604fd306cSNickeau            $htmlAttributes->addClassName("active");
17704fd306cSNickeau            $htmlAttributes->addOutputAttributeValue("aria-selected", "true");
17804fd306cSNickeau        }
17904fd306cSNickeau        $html .= $htmlAttributes
18004fd306cSNickeau            ->addClassName("nav-link")
18104fd306cSNickeau            ->addOutputAttributeValue('id', $panel . "-tab")
18204fd306cSNickeau            ->addOutputAttributeValue("data{$namespace}-toggle", "tab")
18304fd306cSNickeau            ->addOutputAttributeValue('aria-controls', $panel)
18404fd306cSNickeau            ->addOutputAttributeValue("role", "tab")
18504fd306cSNickeau            ->addOutputAttributeValue('href', "#$panel")
18604fd306cSNickeau            ->toHtmlEnterTag("a");
18704fd306cSNickeau
18804fd306cSNickeau        return $html;
18904fd306cSNickeau    }
19004fd306cSNickeau
19104fd306cSNickeau    public static function closeNavigationalTabElement(): string
19204fd306cSNickeau    {
19304fd306cSNickeau        return "</a></li>";
19404fd306cSNickeau    }
19504fd306cSNickeau
19604fd306cSNickeau    /**
19704fd306cSNickeau     * @param TagAttributes $tagAttributes
19804fd306cSNickeau     * @return string - return the HTML open tags of the panels (not the navigation)
19904fd306cSNickeau     */
20004fd306cSNickeau    public static function openTabPanelsElement(TagAttributes $tagAttributes): string
20104fd306cSNickeau    {
20204fd306cSNickeau
20304fd306cSNickeau        $tagAttributes->addClassName("tab-content");
20404fd306cSNickeau
20504fd306cSNickeau        /**
20604fd306cSNickeau         * In preview with only one panel
20704fd306cSNickeau         */
20804fd306cSNickeau        global $ACT;
20904fd306cSNickeau        if ($ACT === "preview" && $tagAttributes->hasComponentAttribute(self::SELECTED_ATTRIBUTE)) {
21004fd306cSNickeau            $tagAttributes->removeComponentAttribute(self::SELECTED_ATTRIBUTE);
21104fd306cSNickeau        }
21204fd306cSNickeau
21304fd306cSNickeau        $html = $tagAttributes->toHtmlEnterTag("div");
21404fd306cSNickeau        $type = self::getComponentType($tagAttributes);
21504fd306cSNickeau        switch ($type) {
21604fd306cSNickeau            case self::ENCLOSED_TABS_TYPE:
21704fd306cSNickeau            case self::ENCLOSED_PILLS_TYPE:
21804fd306cSNickeau                $html = "<div class=\"card-body\">" . $html;
21904fd306cSNickeau                break;
22004fd306cSNickeau        }
22104fd306cSNickeau        return $html;
22204fd306cSNickeau
22304fd306cSNickeau    }
22404fd306cSNickeau
22504fd306cSNickeau    public static function getComponentType(TagAttributes $tagAttributes)
22604fd306cSNickeau    {
22704fd306cSNickeau
22804fd306cSNickeau        $skin = $tagAttributes->getValueAndRemoveIfPresent("skin");
22904fd306cSNickeau        if ($skin !== null) {
23004fd306cSNickeau            return $skin;
23104fd306cSNickeau        }
23204fd306cSNickeau
23304fd306cSNickeau        $type = $tagAttributes->getType();
23404fd306cSNickeau        if ($type !== null) {
23504fd306cSNickeau            return $type;
23604fd306cSNickeau        }
23704fd306cSNickeau        return self::TABS_TYPE;
23804fd306cSNickeau    }
23904fd306cSNickeau
24004fd306cSNickeau    public static function closeTabPanelsElement(TagAttributes $tagAttributes): string
24104fd306cSNickeau    {
24204fd306cSNickeau        $html = "</div>";
24304fd306cSNickeau        $type = self::getComponentType($tagAttributes);
24404fd306cSNickeau        switch ($type) {
24504fd306cSNickeau            case self::ENCLOSED_TABS_TYPE:
24604fd306cSNickeau            case self::ENCLOSED_PILLS_TYPE:
24704fd306cSNickeau                $html .= "</div>";
24804fd306cSNickeau                $html .= "</div>";
24904fd306cSNickeau                break;
25004fd306cSNickeau        }
25104fd306cSNickeau        return $html;
25204fd306cSNickeau    }
25304fd306cSNickeau
25404fd306cSNickeau    public static function handleExit($handler): array
25504fd306cSNickeau    {
25604fd306cSNickeau        $callStack = CallStack::createFromHandler($handler);
25704fd306cSNickeau        $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
25804fd306cSNickeau        if ($openingTag === false) {
25904fd306cSNickeau            LogUtility::error("A tabs tag had no opening tag and was discarded");
26004fd306cSNickeau            return array(
26104fd306cSNickeau                PluginUtility::CONTEXT => "root",
26204fd306cSNickeau                PluginUtility::ATTRIBUTES => []
26304fd306cSNickeau            );
26404fd306cSNickeau        }
26504fd306cSNickeau        $previousOpeningTag = $callStack->previous();
26604fd306cSNickeau        $callStack->next();
26704fd306cSNickeau        $firstChild = $callStack->moveToFirstChildTag();
26804fd306cSNickeau        $context = null;
26904fd306cSNickeau        if ($firstChild !== false) {
27004fd306cSNickeau            /**
27104fd306cSNickeau             * Add the context to the opening and ending tag
27204fd306cSNickeau             */
27304fd306cSNickeau            $context = $firstChild->getTagName();
27404fd306cSNickeau            $openingTag->setContext($context);
27504fd306cSNickeau            /**
27604fd306cSNickeau             * Does tabs enclosed Panel (new syntax)
27704fd306cSNickeau             */
27804fd306cSNickeau            if ($context == PanelTag::PANEL_LOGICAL_MARKUP) {
27904fd306cSNickeau
28004fd306cSNickeau                /**
28104fd306cSNickeau                 * We scan the tabs and derived:
28204fd306cSNickeau                 * * the navigation tabs element (ie ul/li nav-tab)
28304fd306cSNickeau                 * * the tab pane element
28404fd306cSNickeau                 */
28504fd306cSNickeau
28604fd306cSNickeau                /**
28704fd306cSNickeau                 * This call will create the ul
28804fd306cSNickeau                 * <ul class="nav nav-tabs mb-3" role="tablist">
28904fd306cSNickeau                 * thanks to the {@link TabsTag::NAVIGATION_CONTEXT}
29004fd306cSNickeau                 */
29104fd306cSNickeau                $navigationalCalls[] = Call::createComboCall(
29204fd306cSNickeau                    TabsTag::TAG,
29304fd306cSNickeau                    DOKU_LEXER_ENTER,
29404fd306cSNickeau                    $openingTag->getAttributes(),
29504fd306cSNickeau                    TabsTag::NAVIGATION_CONTEXT,
29604fd306cSNickeau                    null,
29704fd306cSNickeau                    null,
29804fd306cSNickeau                    null,
29904fd306cSNickeau                    \syntax_plugin_combo_xmlblocktag::TAG
30004fd306cSNickeau                );
30104fd306cSNickeau
30204fd306cSNickeau                /**
30304fd306cSNickeau                 * The tab pane elements
30404fd306cSNickeau                 */
30504fd306cSNickeau                $tabPaneCalls = [$openingTag, $firstChild];
30604fd306cSNickeau
30704fd306cSNickeau                /**
30804fd306cSNickeau                 * Copy the stack
30904fd306cSNickeau                 */
31004fd306cSNickeau                $labelState = "label";
31104fd306cSNickeau                $nonLabelState = "non-label";
31204fd306cSNickeau                $scanningState = $nonLabelState;
31304fd306cSNickeau                while ($actual = $callStack->next()) {
31404fd306cSNickeau
31504fd306cSNickeau                    if (
31604fd306cSNickeau                        $actual->getTagName() == syntax_plugin_combo_label::TAG
31704fd306cSNickeau                        &&
31804fd306cSNickeau                        $actual->getState() == DOKU_LEXER_ENTER
31904fd306cSNickeau                    ) {
32004fd306cSNickeau                        $scanningState = $labelState;
32104fd306cSNickeau                    }
32204fd306cSNickeau
32304fd306cSNickeau                    if ($labelState === $scanningState) {
32404fd306cSNickeau                        $navigationalCalls[] = $actual;
32504fd306cSNickeau                    } else {
32604fd306cSNickeau                        $tabPaneCalls[] = $actual;
32704fd306cSNickeau                    }
32804fd306cSNickeau
32904fd306cSNickeau                    if (
33004fd306cSNickeau                        $actual->getTagName() == syntax_plugin_combo_label::TAG
33104fd306cSNickeau                        &&
33204fd306cSNickeau                        $actual->getState() == DOKU_LEXER_EXIT
33304fd306cSNickeau                    ) {
33404fd306cSNickeau                        $scanningState = $nonLabelState;
33504fd306cSNickeau                    }
33604fd306cSNickeau
33704fd306cSNickeau
33804fd306cSNickeau                }
33904fd306cSNickeau
34004fd306cSNickeau                /**
34104fd306cSNickeau                 * End navigational tabs
34204fd306cSNickeau                 */
34304fd306cSNickeau                $navigationalCalls[] = Call::createComboCall(
34404fd306cSNickeau                    TabsTag::TAG,
34504fd306cSNickeau                    DOKU_LEXER_EXIT,
34604fd306cSNickeau                    $openingTag->getAttributes(),
34704fd306cSNickeau                    TabsTag::NAVIGATION_CONTEXT,
34804fd306cSNickeau                    null,
34904fd306cSNickeau                    null,
35004fd306cSNickeau                    null,
35104fd306cSNickeau                    \syntax_plugin_combo_xmlblocktag::TAG
35204fd306cSNickeau                );
35304fd306cSNickeau
35404fd306cSNickeau                /**
35504fd306cSNickeau                 * Rebuild
35604fd306cSNickeau                 */
35704fd306cSNickeau                $callStack->deleteAllCallsAfter($previousOpeningTag);
35804fd306cSNickeau                $callStack->appendCallsAtTheEnd($navigationalCalls);
35904fd306cSNickeau                $callStack->appendCallsAtTheEnd($tabPaneCalls);
36004fd306cSNickeau
36104fd306cSNickeau            }
36204fd306cSNickeau        }
36304fd306cSNickeau
36404fd306cSNickeau        return array(
36504fd306cSNickeau            PluginUtility::CONTEXT => $context,
36604fd306cSNickeau            PluginUtility::ATTRIBUTES => $openingTag->getAttributes()
36704fd306cSNickeau        );
36804fd306cSNickeau    }
36904fd306cSNickeau
37004fd306cSNickeau    public static function renderEnterXhtml(TagAttributes $tagAttributes, array $data): string
37104fd306cSNickeau    {
37204fd306cSNickeau        $context = $data[PluginUtility::CONTEXT];
37304fd306cSNickeau        switch ($context) {
37404fd306cSNickeau            /**
37504fd306cSNickeau             * When the tag tabs enclosed the panels
37604fd306cSNickeau             */
37704fd306cSNickeau            case PanelTag::PANEL_LOGICAL_MARKUP:
37804fd306cSNickeau                return TabsTag::openTabPanelsElement($tagAttributes);
37904fd306cSNickeau            /**
38004fd306cSNickeau             * When the tag tabs are derived (new syntax)
38104fd306cSNickeau             */
38204fd306cSNickeau            case TabsTag::NAVIGATION_CONTEXT:
38304fd306cSNickeau                /**
38404fd306cSNickeau                 * Old syntax, when the tag had to be added specifically
38504fd306cSNickeau                 */
38604fd306cSNickeau            case syntax_plugin_combo_tab::TAG:
38704fd306cSNickeau                return TabsTag::openNavigationalTabsElement($tagAttributes);
38804fd306cSNickeau            default:
38904fd306cSNickeau                LogUtility::internalError("The context ($context) is unknown in enter", TabsTag::TAG);
39004fd306cSNickeau                return "";
39104fd306cSNickeau
39204fd306cSNickeau        }
39304fd306cSNickeau    }
39404fd306cSNickeau
39504fd306cSNickeau    public static function renderExitXhtml(TagAttributes $tagAttributes, array $data): string
39604fd306cSNickeau    {
39704fd306cSNickeau        $context = $data[PluginUtility::CONTEXT];
39804fd306cSNickeau        switch ($context) {
39904fd306cSNickeau            /**
40004fd306cSNickeau             * New syntax (tabpanel enclosing)
40104fd306cSNickeau             */
40204fd306cSNickeau            case PanelTag::PANEL_LOGICAL_MARKUP:
40304fd306cSNickeau                return TabsTag::closeTabPanelsElement($tagAttributes);
40404fd306cSNickeau            /**
40504fd306cSNickeau             * Old syntax
40604fd306cSNickeau             */
40704fd306cSNickeau            case syntax_plugin_combo_tab::TAG:
40804fd306cSNickeau                /**
40904fd306cSNickeau                 * New syntax (Derived)
41004fd306cSNickeau                 */
41104fd306cSNickeau            case TabsTag::NAVIGATION_CONTEXT:
41204fd306cSNickeau                $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
41304fd306cSNickeau                $type = TabsTag::getComponentType($tagAttributes);
41404fd306cSNickeau                return TabsTag::closeNavigationalHeaderComponent($type);
41504fd306cSNickeau            default:
41604fd306cSNickeau                LogUtility::log2FrontEnd("The context $context is unknown in exit", LogUtility::LVL_MSG_ERROR, TabsTag::TAG);
41704fd306cSNickeau                return "";
41804fd306cSNickeau        }
41904fd306cSNickeau    }
42004fd306cSNickeau}
42104fd306cSNickeau
422