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