xref: /plugin/combo/ComboStrap/TabsTag.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1<?php
2
3namespace ComboStrap;
4
5
6use syntax_plugin_combo_label;
7use syntax_plugin_combo_tab;
8
9/**
10 * The tabs component is a little bit a nasty one
11 * because it's used in three cases:
12 *   * the new syntax to enclose the panels
13 *   * the new syntax to create the tabs
14 *   * the old syntax to create the tabs
15 * The code is using the context to manage this cases
16 *
17 * Full example can be found
18 * in the Javascript section of tabs and navs
19 * https://getbootstrap.com/docs/5.0/components/navs-tabs/#javascript-behavior
20 *
21 * Vertical Pills
22 * https://getbootstrap.com/docs/4.0/components/navs/#vertical
23 */
24class TabsTag
25{
26
27
28    /**
29     * A tabs with this context will render the `ul` HTML tags
30     * (ie tabs enclose the navigation partition)
31     */
32    public const NAVIGATION_CONTEXT = "navigation-context";
33    public const PILLS_TYPE = "pills";
34    public const TABS_SKIN = "tabs";
35    public const ENCLOSED_TABS_TYPE = "enclosed-tabs";
36    public const ENCLOSED_PILLS_TYPE = "enclosed-pills";
37    public const LABEL = 'label';
38    public const SELECTED_ATTRIBUTE = "selected";
39    public const TAG = 'tabs';
40    /**
41     * A key attributes to set on in the instructions the attributes
42     * of panel
43     */
44    public const KEY_PANEL_ATTRIBUTES = "panels";
45    /**
46     * Type tabs
47     */
48    public const TABS_TYPE = "tabs";
49    public const PILLS_SKIN = "pills";
50
51    public static function closeNavigationalHeaderComponent($type)
52    {
53        $html = "</ul>" . DOKU_LF;
54        switch ($type) {
55            case self::ENCLOSED_PILLS_TYPE:
56            case self::ENCLOSED_TABS_TYPE:
57                $html .= "</div>" . DOKU_LF;
58        }
59        return $html;
60
61    }
62
63    /**
64     * @param $tagAttributes
65     * @return string - the opening HTML code of the tab navigational header
66     */
67    public static function openNavigationalTabsElement(TagAttributes $tagAttributes): string
68    {
69
70        /**
71         * Unset non-html attributes
72         */
73        $tagAttributes->removeComponentAttributeIfPresent(self::KEY_PANEL_ATTRIBUTES);
74
75        /**
76         * Type (Skin determination)
77         */
78        $type = self::getComponentType($tagAttributes);
79
80        /**
81         * $skin (tabs or pills)
82         */
83        $skin = self::TABS_TYPE;
84        switch ($type) {
85            case self::TABS_TYPE:
86            case self::ENCLOSED_TABS_TYPE:
87                $skin = self::TABS_SKIN;
88                break;
89            case self::PILLS_TYPE:
90            case self::ENCLOSED_PILLS_TYPE:
91                $skin = self::PILLS_SKIN;
92                break;
93            default:
94                LogUtility::warning("The tabs type ($type) has an unknown skin", self::TAG);
95        }
96
97        /**
98         * Creates the panel wrapper element
99         */
100        $html = "";
101        switch ($type) {
102            case self::TABS_TYPE:
103            case self::PILLS_TYPE:
104                if (!$tagAttributes->hasAttribute(Spacing::SPACING_ATTRIBUTE)) {
105                    $tagAttributes->addComponentAttributeValue(Spacing::SPACING_ATTRIBUTE, "mb-3");
106                }
107                $tagAttributes->addClassName("nav")
108                    ->addClassName("nav-$skin");
109                $tagAttributes->addOutputAttributeValue('role', 'tablist');
110                $html = $tagAttributes->toHtmlEnterTag("ul");
111                break;
112            case self::ENCLOSED_TABS_TYPE:
113            case self::ENCLOSED_PILLS_TYPE:
114                /**
115                 * The HTML opening for cards
116                 */
117                $tagAttributes->addClassName("card");
118                $html = $tagAttributes->toHtmlEnterTag("div") .
119                    "<div class=\"card-header\">";
120                /**
121                 * The HTML opening for the menu (UL)
122                 */
123                $html .= TagAttributes::createEmpty()
124                    ->addClassName("nav")
125                    ->addClassName("nav-$skin")
126                    ->addClassName("card-header-$skin")
127                    ->toHtmlEnterTag("ul");
128                break;
129            default:
130                LogUtility::error("The tabs type ($type) is unknown", self::TAG);
131        }
132        return $html;
133
134    }
135
136    /**
137     * @param array $attributes
138     * @return string
139     */
140    public static function openNavigationalTabElement(array $attributes): string
141    {
142        $liTagAttributes = TagAttributes::createFromCallStackArray($attributes);
143
144        /**
145         * Check all attributes for the link (not the li)
146         * and delete them
147         */
148        $active = PanelTag::getSelectedValue($liTagAttributes);
149        $panel = "";
150
151
152        $panel = $liTagAttributes->getValueAndRemoveIfPresent("panel");
153        if ($panel === null && $liTagAttributes->hasComponentAttribute("id")) {
154            $panel = $liTagAttributes->getValueAndRemoveIfPresent("id");
155        }
156        if ($panel === null) {
157            LogUtility::msg("A id attribute is missing on a panel tag", LogUtility::LVL_MSG_ERROR, TabsTag::TAG);
158        }
159
160
161        /**
162         * Creating the li element
163         */
164        $html = $liTagAttributes->addClassName("nav-item")
165            ->addOutputAttributeValue("role", "presentation")
166            ->toHtmlEnterTag("li");
167
168        /**
169         * Creating the a element
170         */
171        $namespace = Bootstrap::getDataNamespace();
172        $htmlAttributes = TagAttributes::createEmpty();
173        if ($active === true) {
174            $htmlAttributes->addClassName("active");
175            $htmlAttributes->addOutputAttributeValue("aria-selected", "true");
176        }
177        $html .= $htmlAttributes
178            ->addClassName("nav-link")
179            ->addOutputAttributeValue('id', $panel . "-tab")
180            ->addOutputAttributeValue("data{$namespace}-toggle", "tab")
181            ->addOutputAttributeValue('aria-controls', $panel)
182            ->addOutputAttributeValue("role", "tab")
183            ->addOutputAttributeValue('href', "#$panel")
184            ->toHtmlEnterTag("a");
185
186        return $html;
187    }
188
189    public static function closeNavigationalTabElement(): string
190    {
191        return "</a></li>";
192    }
193
194    /**
195     * @param TagAttributes $tagAttributes
196     * @return string - return the HTML open tags of the panels (not the navigation)
197     */
198    public static function openTabPanelsElement(TagAttributes $tagAttributes): string
199    {
200
201        $tagAttributes->addClassName("tab-content");
202
203        /**
204         * In preview with only one panel
205         */
206        global $ACT;
207        if ($ACT === "preview" && $tagAttributes->hasComponentAttribute(self::SELECTED_ATTRIBUTE)) {
208            $tagAttributes->removeComponentAttribute(self::SELECTED_ATTRIBUTE);
209        }
210
211        $html = $tagAttributes->toHtmlEnterTag("div");
212        $type = self::getComponentType($tagAttributes);
213        switch ($type) {
214            case self::ENCLOSED_TABS_TYPE:
215            case self::ENCLOSED_PILLS_TYPE:
216                $html = "<div class=\"card-body\">" . $html;
217                break;
218        }
219        return $html;
220
221    }
222
223    public static function getComponentType(TagAttributes $tagAttributes)
224    {
225
226        $skin = $tagAttributes->getValueAndRemoveIfPresent("skin");
227        if ($skin !== null) {
228            return $skin;
229        }
230
231        $type = $tagAttributes->getType();
232        if ($type !== null) {
233            return $type;
234        }
235        return self::TABS_TYPE;
236    }
237
238    public static function closeTabPanelsElement(TagAttributes $tagAttributes): string
239    {
240        $html = "</div>";
241        $type = self::getComponentType($tagAttributes);
242        switch ($type) {
243            case self::ENCLOSED_TABS_TYPE:
244            case self::ENCLOSED_PILLS_TYPE:
245                $html .= "</div>";
246                $html .= "</div>";
247                break;
248        }
249        return $html;
250    }
251
252    public static function handleExit($handler): array
253    {
254        $callStack = CallStack::createFromHandler($handler);
255        $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
256        if ($openingTag === false) {
257            LogUtility::error("A tabs tag had no opening tag and was discarded");
258            return array(
259                PluginUtility::CONTEXT => "root",
260                PluginUtility::ATTRIBUTES => []
261            );
262        }
263        $previousOpeningTag = $callStack->previous();
264        $callStack->next();
265        $firstChild = $callStack->moveToFirstChildTag();
266        $context = null;
267        if ($firstChild !== false) {
268            /**
269             * Add the context to the opening and ending tag
270             */
271            $context = $firstChild->getTagName();
272            $openingTag->setContext($context);
273            /**
274             * Does tabs enclosed Panel (new syntax)
275             */
276            if ($context == PanelTag::PANEL_LOGICAL_MARKUP) {
277
278                /**
279                 * We scan the tabs and derived:
280                 * * the navigation tabs element (ie ul/li nav-tab)
281                 * * the tab pane element
282                 */
283
284                /**
285                 * This call will create the ul
286                 * <ul class="nav nav-tabs mb-3" role="tablist">
287                 * thanks to the {@link TabsTag::NAVIGATION_CONTEXT}
288                 */
289                $navigationalCalls[] = Call::createComboCall(
290                    TabsTag::TAG,
291                    DOKU_LEXER_ENTER,
292                    $openingTag->getAttributes(),
293                    TabsTag::NAVIGATION_CONTEXT,
294                    null,
295                    null,
296                    null,
297                    \syntax_plugin_combo_xmlblocktag::TAG
298                );
299
300                /**
301                 * The tab pane elements
302                 */
303                $tabPaneCalls = [$openingTag, $firstChild];
304
305                /**
306                 * Copy the stack
307                 */
308                $labelState = "label";
309                $nonLabelState = "non-label";
310                $scanningState = $nonLabelState;
311                while ($actual = $callStack->next()) {
312
313                    if (
314                        $actual->getTagName() == syntax_plugin_combo_label::TAG
315                        &&
316                        $actual->getState() == DOKU_LEXER_ENTER
317                    ) {
318                        $scanningState = $labelState;
319                    }
320
321                    if ($labelState === $scanningState) {
322                        $navigationalCalls[] = $actual;
323                    } else {
324                        $tabPaneCalls[] = $actual;
325                    }
326
327                    if (
328                        $actual->getTagName() == syntax_plugin_combo_label::TAG
329                        &&
330                        $actual->getState() == DOKU_LEXER_EXIT
331                    ) {
332                        $scanningState = $nonLabelState;
333                    }
334
335
336                }
337
338                /**
339                 * End navigational tabs
340                 */
341                $navigationalCalls[] = Call::createComboCall(
342                    TabsTag::TAG,
343                    DOKU_LEXER_EXIT,
344                    $openingTag->getAttributes(),
345                    TabsTag::NAVIGATION_CONTEXT,
346                    null,
347                    null,
348                    null,
349                    \syntax_plugin_combo_xmlblocktag::TAG
350                );
351
352                /**
353                 * Rebuild
354                 */
355                $callStack->deleteAllCallsAfter($previousOpeningTag);
356                $callStack->appendCallsAtTheEnd($navigationalCalls);
357                $callStack->appendCallsAtTheEnd($tabPaneCalls);
358
359            }
360        }
361
362        return array(
363            PluginUtility::CONTEXT => $context,
364            PluginUtility::ATTRIBUTES => $openingTag->getAttributes()
365        );
366    }
367
368    public static function renderEnterXhtml(TagAttributes $tagAttributes, array $data): string
369    {
370        $context = $data[PluginUtility::CONTEXT];
371        switch ($context) {
372            /**
373             * When the tag tabs enclosed the panels
374             */
375            case PanelTag::PANEL_LOGICAL_MARKUP:
376                return TabsTag::openTabPanelsElement($tagAttributes);
377            /**
378             * When the tag tabs are derived (new syntax)
379             */
380            case TabsTag::NAVIGATION_CONTEXT:
381                /**
382                 * Old syntax, when the tag had to be added specifically
383                 */
384            case syntax_plugin_combo_tab::TAG:
385                return TabsTag::openNavigationalTabsElement($tagAttributes);
386            default:
387                LogUtility::internalError("The context ($context) is unknown in enter", TabsTag::TAG);
388                return "";
389
390        }
391    }
392
393    public static function renderExitXhtml(TagAttributes $tagAttributes, array $data): string
394    {
395        $context = $data[PluginUtility::CONTEXT];
396        switch ($context) {
397            /**
398             * New syntax (tabpanel enclosing)
399             */
400            case PanelTag::PANEL_LOGICAL_MARKUP:
401                return TabsTag::closeTabPanelsElement($tagAttributes);
402            /**
403             * Old syntax
404             */
405            case syntax_plugin_combo_tab::TAG:
406                /**
407                 * New syntax (Derived)
408                 */
409            case TabsTag::NAVIGATION_CONTEXT:
410                $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
411                $type = TabsTag::getComponentType($tagAttributes);
412                return TabsTag::closeNavigationalHeaderComponent($type);
413            default:
414                LogUtility::log2FrontEnd("The context $context is unknown in exit", LogUtility::LVL_MSG_ERROR, TabsTag::TAG);
415                return "";
416        }
417    }
418}
419
420