1(function (Drupal) { 2 Drupal.behaviors.apbTabs = { 3 attach: function (context, settings) { 4 const { all } = APB.DOM; 5 6 // Search in context or document 7 const root = context instanceof HTMLElement ? context : document; 8 const tabsEls = all('.paragraph--type--apb-pb-tabs', root); 9 10 if (tabsEls.length) { 11 tabsEls.forEach(processTabsContainer); 12 } 13 }, 14 }; 15 16 // ===== GENERAL FUNCTIONS ===== 17 function processTabsContainer(mainParent) { 18 const { one, text, setAttr } = APB.DOM; 19 20 // Flag per Paragraph â prevents duplication 21 if (mainParent.dataset.apbTabs === 'initialized') { 22 return; 23 } 24 mainParent.dataset.apbTabs = 'initialized'; 25 26 // Get configuration from DOM 27 const config = getTabsConfig(mainParent); 28 // console.log('config:', config); 29 30 // Apply general settings 31 applyGeneralSettings(mainParent, config); 32 33 // Generate unique ID for this tabs container 34 const paragraphId = text(one('.paragraphs-item-id', mainParent)); 35 const containerId = `apb-tabs-container-${paragraphId}`; 36 setAttr(mainParent, 'id', containerId); 37 38 // Create tabs structure 39 createTabsStructure(mainParent, config, containerId, paragraphId); 40 41 // Setup scroll functionality for tab content 42 setupTabsScroll(mainParent); 43 44 // Inject CSS styles 45 injectTabsCSS(containerId, config); 46 47 // Initialize tabs functionality 48 initializeTabsFunctionality(mainParent, containerId); 49 } 50 51 function getTabsConfig(mainParent) { 52 const { one, text } = APB.DOM; 53 54 return { 55 sectionTitle: text( 56 one('.field--name-field-apb-section-title', mainParent) 57 ), 58 showSectionTitle: text( 59 one('.field--name-field-apb-show-section-title', mainParent) 60 ), 61 customClassGroup: text( 62 one('.field--name-field-apb-custom-class', mainParent) 63 ), 64 tbTitleMainColor: text( 65 one( 66 '.field--name-field-apb-color-style-02 .apb-tax-cs-title', 67 mainParent 68 ) 69 ), 70 tbMainColor: text( 71 one( 72 '.field--name-field-apb-color-style-02 .apb-tax-cs-main-color', 73 mainParent 74 ) 75 ), 76 tbTitleHover: text( 77 one( 78 '.field--name-field-apb-color-style-02 .apb-tax-cs-title-hover', 79 mainParent 80 ) 81 ), 82 tbHover: text( 83 one( 84 '.field--name-field-apb-color-style-02 .apb-tax-cs-hover', 85 mainParent 86 ) 87 ), 88 tbTitleActive: text( 89 one( 90 '.field--name-field-apb-color-style-02 .apb-tax-cs-title-active', 91 mainParent 92 ) 93 ), 94 tbActive: text( 95 one( 96 '.field--name-field-apb-color-style-02 .apb-tax-cs-active', 97 mainParent 98 ) 99 ), 100 tbHeight: text( 101 one( 102 '.field--name-field-apb-tabs-height-tx .term-machine-name', 103 mainParent 104 ) 105 ).replace('_', ''), 106 tbOrientation: text( 107 one('.field--name-field-apb-tabs-orientation', mainParent) 108 ).replace('_', ''), 109 contentWidth: text( 110 one( 111 '.field--name-field-apb-width-tx .term-machine-name', 112 mainParent 113 ) 114 ).replace('_', ''), 115 }; 116 } 117 118 function applyGeneralSettings(mainParent, config) { 119 const { one, addClass, removeClass, setAttr } = APB.DOM; 120 const { toBoolean } = APB.Data; 121 122 // SHOW SECTION TITLE 123 if (config.sectionTitle && toBoolean(config.showSectionTitle)) { 124 const titleEl = one( 125 '.field--name-field-apb-section-title', 126 mainParent 127 ); 128 if (titleEl) removeClass(titleEl, 'apb-setup'); 129 } 130 131 // CONTENT WIDTH 132 if (config.contentWidth !== '100') { 133 setAttr( 134 mainParent, 135 'style', 136 `max-width: ${config.contentWidth}%; margin-left: auto !important; margin-right: auto !important;` 137 ); 138 } 139 140 // CUSTOM CLASS GROUP 141 if (config.customClassGroup) { 142 addClass(mainParent, config.customClassGroup); 143 } 144 } 145 146 function createTabsStructure(mainParent, config, containerId, paragraphId) { 147 const { one, all, text, addClass, setAttr, createElement } = APB.DOM; 148 149 const viewsContainer = one('.views-element-container', mainParent); 150 const tabRows = all('.views-element-container .apb-tabs-row', mainParent); 151 152 // Create accordion wrapper 153 const accordionWrapper = createElement('section', 'apb-tabs-acc'); 154 // Add orientation class if defined 155 if (config.tbOrientation) { 156 addClass(accordionWrapper, `apb-tabs-acc__${config.tbOrientation}`); 157 } else { 158 addClass(accordionWrapper, 'apb-tabs-acc__horizontal'); 159 } 160 161 // Create tabs navigation (desktop) 162 const tabsNav = createElement( 163 'section', 164 'apb-tabs-acc-tabs apb-tabs-nav' 165 ); 166 setAttr(tabsNav, 'role', 'tablist'); 167 setAttr(tabsNav, 'aria-label', 'Tab navigation'); 168 169 // Create content wrapper 170 const contentWrapper = createElement( 171 'section', 172 'apb-tabs-acc-content apb-tabs-content' 173 ); 174 175 // Process each tab row 176 tabRows.forEach((row, index) => { 177 const tabTitle = text(one('.apb-tabs-title', row)).trim(); 178 const tabBody = one('.apb-tabs-body', row); 179 const tabCustomClass = text(one('.apb-tabs-custom-class', row)).trim(); 180 181 // Generate unique tab ID including paragraph ID 182 const baseTabId = tabTitle 183 .replace(/[^a-z0-9\s]/gi, '') 184 .replace(/[_\s]+/g, '-') 185 .toLowerCase(); 186 const tabId = `${paragraphId}-${baseTabId}-${index}`; 187 188 // Create tab button (desktop) 189 const tabButton = createElement('button', 'apb-tabs-acc-tab'); 190 if (index === 0) addClass(tabButton, 'apb-tabs-acc-active'); 191 setAttr(tabButton, 'role', 'tab'); 192 setAttr(tabButton, 'aria-selected', index === 0 ? 'true' : 'false'); 193 setAttr(tabButton, 'aria-controls', `apb-tabs-panel-${tabId}`); 194 setAttr(tabButton, 'id', `apb-tabs-tab-${tabId}`); 195 setAttr(tabButton, 'data-actab-group', containerId); 196 setAttr(tabButton, 'data-actab-id', index.toString()); 197 setAttr(tabButton, 'tabindex', index === 0 ? '0' : '-1'); 198 tabButton.textContent = tabTitle; 199 200 // Create tab panel (mobile accordion + desktop content) 201 const tabPanel = createElement('article', 'apb-tabs-acc-item'); 202 if (index === 0) addClass(tabPanel, 'apb-tabs-acc-active'); 203 setAttr(tabPanel, 'role', 'tabpanel'); 204 setAttr(tabPanel, 'id', `apb-tabs-panel-${tabId}`); 205 setAttr(tabPanel, 'aria-labelledby', `apb-tabs-tab-${tabId}`); 206 setAttr(tabPanel, 'data-actab-group', containerId); 207 setAttr(tabPanel, 'data-actab-id', index.toString()); 208 209 // Create accordion label (mobile) 210 const accordionLabel = createElement( 211 'div', 212 'apb-tabs-acc-item__label' 213 ); 214 setAttr(accordionLabel, 'tabindex', '0'); 215 accordionLabel.textContent = tabTitle; 216 217 // Create content container 218 const contentContainer = createElement( 219 'div', 220 'apb-tabs-acc-item__container' 221 ); 222 223 // Move original content to new structure 224 if (tabBody) { 225 contentContainer.appendChild(tabBody.cloneNode(true)); 226 } 227 228 // Apply custom class if exists 229 if (tabCustomClass) { 230 addClass(tabPanel, tabCustomClass); 231 } 232 233 // Assemble structures 234 tabPanel.appendChild(accordionLabel); 235 tabPanel.appendChild(contentContainer); 236 tabsNav.appendChild(tabButton); 237 contentWrapper.appendChild(tabPanel); 238 }); 239 240 // Assemble final structure 241 accordionWrapper.appendChild(tabsNav); 242 accordionWrapper.appendChild(contentWrapper); 243 244 // Replace original content 245 viewsContainer.innerHTML = ''; 246 viewsContainer.appendChild(accordionWrapper); 247 } 248 249 function setupTabsScroll(mainParent) { 250 const { all, addStyles } = APB.DOM; 251 252 // Get all content containers 253 const contentContainers = all('.apb-tabs-acc-content', mainParent); 254
255 contentContainers.forEach(container => { 256 // Add overflow styles 257 addStyles(container, { 258 'overflow-y': 'auto', 259 'overflow-x': 'hidden', 260 }); 261 262 // Initialize custom scrollbar using APB.DOM.scrollBox 263 if (typeof APB.DOM.scrollBox === 'function') { 264 APB.DOM.scrollBox(container, { 265 scrollbars: { 266 theme: 'os-theme-light', 267 visibility: 'visible', 268 autoHide: 'never', 269 dragScroll: true, 270 clickScroll: true, 271 touchSupport: true, 272 }, 273 overflow: { 274 x: 'hidden', 275 y: 'scroll', 276 }, 277 paddingAbsolute: true, 278 showNativeOverlaidScrollbars: false, 279 }); 280 } 281 }); 282 } 283 284 function injectTabsCSS(containerId, config) { 285 const { injectCSS } = APB.DOM; 286 287 const cssRules = ` 288 /* Desktop tabs styles */ 289 #${containerId} .apb-tabs-acc-tab { 290 background: ${config.tbMainColor} !important; 291 color: ${config.tbTitleMainColor} !important; 292 } 293 #${containerId} .apb-tabs-acc-tab:hover { 294 background: ${config.tbHover} !important; 295 color: ${config.tbTitleHover} !important; 296 } 297 #${containerId} .apb-tabs-acc-tab.apb-tabs-acc-active { 298 background: ${config.tbActive} !important; 299 color: ${config.tbTitleActive} !important; 300 } 301 302 /* Mobile accordion styles */ 303 #${containerId} .apb-tabs-acc-item__label { 304 background: ${config.tbMainColor} !important; 305 color: ${config.tbTitleMainColor} !important; 306 } 307 #${containerId} .apb-tabs-acc-item__label:hover { 308 background: ${config.tbHover} !important; 309 color: ${config.tbTitleHover} !important; 310 } 311 #${containerId} .apb-tabs-acc-item.apb-tabs-acc-active .apb-tabs-acc-item__label { 312 background: ${config.tbActive} !important; 313 color: ${config.tbTitleActive} !important; 314 } 315 316 @media screen and (min-width: 600px) { 317 /* Content height */ 318 #${containerId} .apb-tabs-acc-content { 319 height: ${config.tbHeight} !important; 320 } 321 } 322 `; 323 324 injectCSS(cssRules); 325 } 326 327 function initializeTabsFunctionality(mainParent, containerId) { 328 const { all } = APB.DOM; 329 330 const labels = all('.apb-tabs-acc-item__label', mainParent); 331 const tabs = all('.apb-tabs-acc-tab', mainParent); 332 let currentAnimation = null; 333 let isInitialized = false; 334 335 // Detects if we are in mobile mode (accordion) 336 function isMobileMode() { 337 return window.innerWidth < 600; 338 } 339 340 // Initializes the correct state of containers on mobile 341 function initializeMobileState() { 342 if (isMobileMode() && !isInitialized) { 343 labels.forEach(function (label) { 344 const tabItem = label.parentElement; 345 const container = tabItem.querySelector( 346 '.apb-tabs-acc-item__container' 347 ); 348 349 if (!tabItem.classList.contains('apb-tabs-acc-active')) { 350 // Ensures that closed items start with height 0 351 352 gsap.set(container, { 353 height: 0, 354 paddingTop: 0, 355 paddingBottom: 0, 356 opacity: 0, 357 overflow: 'hidden', 358 }); 359 } else { 360 // Ensures that the active item starts with auto height 361 gsap.set(container, { 362 height: 'auto', 363 paddingTop: '1.25rem', 364 paddingBottom: '1.25rem', 365 opacity: 1, 366 overflow: 'hidden', 367 }); 368 } 369 }); 370 isInitialized = true; 371 } 372 } 373 374 // Call initialization when DOM loads 375 initializeMobileState(); 376 377 function toggleShow() { 378 const target = this; 379 const item = target.classList.contains('apb-tabs-acc-tab') 380 ? target 381 : target.parentElement; 382 const group = item.dataset.actabGroup; 383 const id = item.dataset.actabId; 384 const isAlreadyActive = item.classList.contains('apb-tabs-acc-active'); 385 386 // Cancel previous animation if it exists 387 if (currentAnimation && typeof gsap !== 'undefined') { 388 currentAnimation.kill(); 389 } 390
391 tabs.forEach(function (tab) { 392 if (tab.dataset.actabGroup === group) { 393 if (tab.dataset.actabId === id) { 394 tab.classList.add('apb-tabs-acc-active'); 395 tab.setAttribute('aria-selected', 'true'); 396 tab.setAttribute('tabindex', '0'); 397 } else { 398 tab.classList.remove('apb-tabs-acc-active'); 399 tab.setAttribute('aria-selected', 'false'); 400 tab.setAttribute('tabindex', '-1'); 401 } 402 } 403 }); 404 405 labels.forEach(function (label) { 406 const tabItem = label.parentElement; 407 const container = tabItem.querySelector( 408 '.apb-tabs-acc-item__container' 409 ); 410 411 if (tabItem.dataset.actabGroup === group) { 412 if (tabItem.dataset.actabId === id) { 413 // If already active and we're on mobile, close 414 if (isAlreadyActive && isMobileMode()) { 415 tabItem.classList.remove('apb-tabs-acc-active'); 416 417 // Closing animation 418 419 // Kill any running animations on this container 420 gsap.killTweensOf(container); 421 currentAnimation = gsap.to(container, { 422 height: 0, 423 paddingTop: 0, 424 paddingBottom: 0, 425 opacity: 0, 426 duration: 0.8, 427 ease: 'power2.inOut', 428 }); 429 } else { 430 // Add active class 431 tabItem.classList.add('apb-tabs-acc-active'); 432 433 // Opening animation (mobile only) 434 if (isMobileMode()) { 435 // Kill any running animations on this container 436 gsap.killTweensOf(container); 437 438 // Calculate real content height WITH padding 439 gsap.set(container, { 440 height: 'auto', 441 opacity: 1, 442 paddingTop: '1.25rem', 443 paddingBottom: '1.25rem', 444 }); 445 const contentHeight = container.scrollHeight; 446 447 // Define altura inicial 448 gsap.set(container, { 449 height: 0, 450 paddingTop: 0, 451 paddingBottom: 0, 452 opacity: 0, 453 }); 454 455 // Anima para a altura real 456 currentAnimation = gsap.to(container, { 457 height: contentHeight, 458 paddingTop: '1.25rem', 459 paddingBottom: '1.25rem', 460 opacity: 1, 461 duration: 0.8, 462 ease: 'power2.out', 463 onComplete: function () { 464 gsap.set(container, { height: 'auto' }); 465 }, 466 }); 467 } 468 } 469 } else { 470 // Close other items only on mobile 471 if ( 472 isMobileMode() && 473 tabItem.classList.contains('apb-tabs-acc-active') 474 ) { 475 tabItem.classList.remove('apb-tabs-acc-active'); 476 477 // Kill any running animations on this container 478 gsap.killTweensOf(container); 479 gsap.to(container, { 480 height: 0, 481 paddingTop: 0, 482 paddingBottom: 0, 483 opacity: 0, 484 duration: 0.8, 485 ease: 'power2.inOut', 486 }); 487 } else { 488 tabItem.classList.remove('apb-tabs-acc-active'); 489 } 490 } 491 } 492 }); 493 } 494 495 // Keyboard navigation for tabs 496 function handleKeyboardNavigation(e) { 497 const currentTab = e.target; 498 const group = currentTab.dataset.actabGroup; 499 const groupTabs = Array.from(tabs).filter( 500 tab => tab.dataset.actabGroup === group 501 ); 502 const currentIndex = groupTabs.indexOf(currentTab); 503 504 let nextIndex; 505 506 switch (e.key) { 507 case 'ArrowRight': 508 e.preventDefault(); 509 nextIndex = (currentIndex + 1) % groupTabs.length; 510 groupTabs[nextIndex].focus(); 511 groupTabs[nextIndex].click(); 512 break; 513 514 case 'ArrowLeft': 515 e.preventDefault(); 516 nextIndex = 517 (currentIndex - 1 + groupTabs.length) % groupTabs.length; 518 groupTabs[nextIndex].focus(); 519 groupTabs[nextIndex].click(); 520 break; 521 522 case 'Home': 523 e.preventDefault(); 524 groupTabs[0].focus(); 525 groupTabs[0].click(); 526 break; 527 528 case 'End': 529 e.preventDefault(); 530 groupTabs[groupTabs.length - 1].focus(); 531 groupTabs[groupTabs.length - 1].click(); 532 break; 533 } 534 } 535 536 // Keyboard navigation for labels (mobile accordion) 537 function handleLabelKeyboard(e) { 538 if (e.key === 'Enter' || e.key === ' ') { 539 e.preventDefault(); 540 toggleShow.call(e.target); 541 } 542 } 543 544 // Add event listeners 545 labels.forEach(function (label) { 546 label.addEventListener('click', toggleShow); 547 label.addEventListener('keydown', handleLabelKeyboard); 548 }); 549
550 tabs.forEach(function (tab) { 551 tab.addEventListener('click', toggleShow); 552 tab.addEventListener('keydown', handleKeyboardNavigation); 553 }); 554 555 // Reset styles when changing from mobile to desktop 556 let lastMode = isMobileMode(); 557 window.addEventListener('resize', function () { 558 const currentMode = isMobileMode(); 559 if (lastMode !== currentMode) { 560 if (currentMode) { 561 // Entering mobile mode 562 isInitialized = false; 563 initializeMobileState(); 564 } else { 565 // Entering desktop mode: clear all inline styles 566 labels.forEach(function (label) { 567 const container = label.nextElementSibling; 568 gsap.set([label, container], { clearProps: 'all' }); 569 }); 570 } 571 lastMode = currentMode; 572 } 573 }); 574 } 575})(Drupal);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.