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