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