1/** 2 * TabNav Module 3 * Handles responsive tab navigation with dynamic dropdown functionality 4 * and ARIA-compliant tab interactions 5 */ 6class TabNav { 7 constructor(container) { 8 this.container = typeof container === 'string' ? document.querySelector(container) : container; 9 10 this.tabs = []; 11 this.dropdown = null; 12 this.dropdownToggle = null; 13 this.dropdownContent = null; 14 this.isInitialized = false; 15 16 this.hasEvo = document.body.classList.contains('evo'); 17 18 this.boundTabClick = this.handleTabClick.bind(this); 19 this.boundTabKeydown = this.handleTabKeydown.bind(this); 20 this.boundDropdownToggle = this.handleDropdownToggle.bind(this); 21 this.boundDropdownToggleKeydown = this.handleDropdownToggleKeydown.bind(this); 22 this.boundDocumentClick = this.handleOutsideClick.bind(this); 23 this.boundDropdownContentClick = this.handleDropdownContentClick.bind(this); 24 this.boundDocumentKeydown = this.handleDocumentKeydown.bind(this); 25 26 this.init(); 27 } 28 29 init() { 30 if (!this.container) { 31 console.warn('TabNav: Container not found'); 32 return; 33 } 34 35 this.container._tabnavInstance = this; 36 37 this.setupElements(); 38 this.bindEvents(); 39 40 this.isInitialized = true; 41 this.adjustLayout(); 42 this.ensureTabSelected(); 43 44 setTimeout(() => { 45 this.container.classList.remove('overflow-hidden'); 46 }, 100); 47 48 window.addEventListener('resize', this.debounce(() => { 49 if (!this.container.classList.contains('bypass')) { 50 this.container.offsetHeight; 51 this.adjustLayout(); 52 } 53 }, 50)); 54 } 55 56 setupElements() { 57 this.tabs = Array.from(this.container.querySelectorAll('[role="tab"]')); 58 59 if (this.container.classList.contains('bypass')) return; 60 61 this.dropdown = this.container.querySelector('.tabs_more_link'); 62 63 if (!this.dropdown) { 64 this.createDropdown(); 65 } else { 66 this.dropdownContent = this.dropdown.querySelector('.dropDown'); 67 this.dropdownToggle = this.dropdown.querySelector('.dropDownHolder > button'); 68 } 69 } 70 71 createDropdown() { 72 this.dropdown = document.createElement('div'); 73 this.dropdown.className = 'tabs_more_link tabnav__tab tabnav__dropdown_tab'; 74 this.dropdown.style.display = 'none'; 75 76 const uniqueId = 'tabnav-dropdown-' + Date.now(); 77 this.dropdown.setAttribute('data-tabnav-dropdown', uniqueId); 78 79 this.dropdown.innerHTML = ` 80 <div class="dropDownHolder"> 81 <button aria-label="Toggle dropdown menu" class="tabnav__toggle" role="tab"> 82 <i class="ellipsis_vertical"></i> 83 </button> 84 <div class="dropDown" data-dropdown-content="${uniqueId}"></div> 85 </div> 86 `; 87 88 this.container.appendChild(this.dropdown); 89 90 this.dropdownContent = this.dropdown.querySelector('.dropDown'); 91 this.dropdownToggle = this.dropdown.querySelector('.dropDownHolder > button'); 92 93 if (this.dropdownToggle) { 94 this.dropdownToggle.addEventListener('click', this.boundDropdownToggle); 95 } 96 if (this.dropdownContent) { 97 this.dropdownContent.addEventListener('click', this.boundDropdownContentClick); 98 } 99 } 100 101 bindEvents() {
102 this.tabs.forEach(tab => { 103 tab.addEventListener('click', this.boundTabClick); 104 tab.addEventListener('keydown', this.boundTabKeydown); 105 106 // Save focus state before navigation 107 tab.addEventListener('click', () => { 108 TabNav.saveFocusState(tab); 109 }); 110 }); 111 112 if (!this.container.classList.contains('bypass')) { 113 document.addEventListener('click', this.boundDocumentClick); 114 document.addEventListener('keydown', this.boundDocumentKeydown); 115 116 if (this.dropdownToggle) { 117 this.dropdownToggle.addEventListener('click', this.boundDropdownToggle); 118 this.dropdownToggle.addEventListener('keydown', this.boundDropdownToggleKeydown); 119 } 120 if (this.dropdownContent) { 121 this.dropdownContent.addEventListener('click', this.boundDropdownContentClick); 122 } 123 } 124 } 125 126 handleTabClick(event) { 127 event.preventDefault(); 128 this.activateTab(event.currentTarget); 129 } 130 131 handleTabKeydown(event) { 132 const tab = event.currentTarget; 133 const tablist = tab.closest('[role="tablist"]'); 134 const allTabs = Array.from(tablist.querySelectorAll('[role="tab"]')); 135 const isInDropdown = this.dropdownContent?.contains(tab); 136 137 const tabs = isInDropdown 138 ? Array.from(this.dropdownContent.querySelectorAll('[role="tab"]')) 139 : allTabs.filter(t => this.isTabVisible(t)); 140 141 const currentIndex = tabs.indexOf(tab); 142 let targetTab; 143 144 switch (event.key) { 145 case 'ArrowUp': 146 case 'ArrowLeft': 147 event.preventDefault(); 148 targetTab = currentIndex > 0 ? tabs[currentIndex - 1] : tabs[tabs.length - 1]; 149 targetTab.focus(); 150 break; 151 case 'ArrowDown': 152 case 'ArrowRight': 153 event.preventDefault(); 154 targetTab = currentIndex < tabs.length - 1 ? tabs[currentIndex + 1] : tabs[0]; 155 targetTab.focus(); 156 break; 157 case 'Home': 158 event.preventDefault(); 159 tabs[0].focus(); 160 break; 161 case 'End': 162 event.preventDefault(); 163 tabs[tabs.length - 1].focus(); 164 break; 165 case 'Enter': 166 case ' ': 167 event.preventDefault(); 168 this.activateTab(tab); 169 TabNav.saveFocusState(tab); 170 tab.click(); 171 if (isInDropdown) { 172 this.closeDropdown(); 173 } 174 break; 175 case 'Escape': 176 if (isInDropdown) { 177 event.preventDefault(); 178 this.closeDropdown(); 179 if (this.dropdownToggle) { 180 this.dropdownToggle.focus(); 181 } 182 } 183 break; 184 } 185 } 186 187 activateTab(tabElement) { 188 const $tab = $(tabElement); 189 const tablist = $tab.closest('[role="tablist"]'); 190 const $tabs = tablist.find('[role="tab"]'); 191 const panelId = $tab.attr('aria-controls'); 192 const $panel = panelId ? $('#' + panelId) : null; 193 194 $tabs.attr({ 'aria-selected': 'false', tabindex: '-1' }); 195 $tab.attr({ 'aria-selected': 'true', tabindex: '0' }); 196 197 $tabs.removeClass('selected'); 198 $tab.addClass('selected'); 199 200 if ($panel && $panel.length) { 201 $('[role="tabpanel"]').attr({ 'aria-hidden': 'true', tabindex: '-1' }); 202 $panel.attr({ 'aria-hidden': 'false', tabindex: '0' }); 203 } 204 } 205 206 handleDropdownToggle(event) { 207 event.preventDefault(); 208 event.stopPropagation(); 209 210 if (!this.dropdown) return; 211 212 const wasActive = this.dropdown.classList.contains('active'); 213 const isActive = !wasActive; 214 const isKeyboardEvent = event.key || event.keyCode; 215 216 this.dropdown.classList.toggle('active', isActive); 217 if (this.dropdownToggle) { 218 this.dropdownToggle.classList.toggle('highlight', isActive); 219 } 220 if (this.dropdownContent) { 221 this.dropdownContent.classList.toggle('dDownShow', isActive); 222 } 223 if (this.container) { 224 this.container.classList.toggle('vis', isActive); 225 } 226 227 if (isActive && isKeyboardEvent) { 228 const firstDropdownTab = this.dropdownContent.querySelector('[role="tab"]'); 229 if (firstDropdownTab) { 230 // Need to wait for CSS visibility change to take effect 231 requestAnimationFrame(() => { 232 requestAnimationFrame(() => { 233 firstDropdownTab.setAttribute('tabindex', '0'); 234 if (this.dropdownToggle) { 235 this.dropdownToggle.blur(); 236 } 237 firstDropdownTab.focus(); 238 if (document.activeElement !== firstDropdownTab) { 239 requestAnimationFrame(() => { 240 firstDropdownTab.focus(); 241 }); 242 } 243 }); 244 }); 245 } 246 } 247 } 248 249 handleDropdownToggleKeydown(event) { 250 if (event.key === 'Enter' || event.key === ' ' || event.keyCode === 13 || event.keyCode === 32) { 251 event.preventDefault(); 252 event.stopPropagation(); 253 this.handleDropdownToggle(event); 254 } else if (event.key === 'Escape' || event.keyCode === 27) { 255 event.preventDefault(); 256 this.closeDropdown(); 257 } 258 } 259 260 handleDocumentKeydown(event) { 261 if ((event.key === 'Escape' || event.keyCode === 27) && this.dropdown?.classList.contains('active')) { 262 event.preventDefault(); 263 this.closeDropdown(); 264 if (this.dropdownToggle) { 265 this.dropdownToggle.focus(); 266 } 267 } 268 } 269 270 handleOutsideClick(event) { 271 if (this.dropdown && !this.dropdown.contains(event.target)) { 272 this.closeDropdown(); 273 } 274 } 275 276 handleDropdownContentClick(event) { 277 const tab = event.target.closest('[role="tab"]'); 278 if (tab) { 279 this.activateTab(tab); 280 TabNav.saveFocusState(tab); 281 tab.click(); 282 this.closeDropdown(); 283 } 284 } 285 286 closeDropdown() { 287 if (this.dropdown) { 288 this.dropdown.classList.remove('active'); 289 } 290 if (this.dropdownToggle) { 291 this.dropdownToggle.classList.remove('highlight'); 292 } 293 if (this.dropdownContent) { 294 this.dropdownContent.classList.remove('dDownShow'); 295 296 const dropdownTabs = this.dropdownContent.querySelectorAll('[role="tab"]');
297 dropdownTabs.forEach(tab => { 298 if (tab.getAttribute('aria-selected') !== 'true') { 299 tab.setAttribute('tabindex', '-1'); 300 } 301 }); 302 } 303 if (this.container) { 304 this.container.classList.remove('vis'); 305 } 306 } 307 308 adjustLayout() { 309 if (this.container.classList.contains('bypass')) return; 310 311 if (!this.dropdown) { 312 this.createDropdown(); 313 } 314 if (!this.dropdownContent) return; 315 this.container.offsetHeight; 316 317 if (this.hasEvo) { 318 this.adjustLayoutNewUI(); 319 } else { 320 this.adjustLayoutOldUI(); 321 } 322 323 this.updateDropdownVisibility(); 324 } 325 326 adjustLayoutOldUI() { 327 const wrapper = this.container; 328 const constraintWidth = this.container.offsetWidth; 329 const buffer = 40; 330 const maxIterations = 30; 331 let iterations = 0; 332 333 // First, check if the selected tab is in the dropdown and move it to visible area 334 const selectedTabInDropdown = this.dropdownContent.querySelector('[role="tab"].selected'); 335 if (selectedTabInDropdown) { 336 // Move selected tab to the end of visible tabs (before dropdown) 337 wrapper.insertBefore(selectedTabInDropdown, this.dropdown); 338 } 339 340 while (iterations < maxIterations) { 341 const visibleTabs = Array.from(wrapper.children).filter(child => 342 child.getAttribute('role') === 'tab' 343 ); 344 let totalTabsWidth = 0; 345 visibleTabs.forEach(tab => { 346 totalTabsWidth += tab.offsetWidth + this.getFlexGap(); 347 }); 348 349 if (constraintWidth >= (totalTabsWidth + buffer)) { 350 break; 351 } 352 353 const nextTabToMove = this.getLastVisibleTab(wrapper); 354 if (!nextTabToMove) { 355 break; 356 } 357 358 if (nextTabToMove.classList.contains('selected')) { 359 // If the selected tab is too large and is the last one, move other tabs to dropdown 360 // Find the second-to-last tab to move instead 361 const visibleTabsList = Array.from(wrapper.children).filter(child => 362 child.getAttribute('role') === 'tab' 363 ); 364 if (visibleTabsList.length > 1) { 365 const secondToLast = visibleTabsList[visibleTabsList.length - 2]; 366 this.moveTabToDropdown(secondToLast); 367 } else { 368 // Only one tab and it's selected, keep it 369 break; 370 } 371 iterations++; 372 continue; 373 } else { 374 this.moveTabToDropdown(nextTabToMove); 375 } 376 377 iterations++; 378 } 379 380 let backIterations = 0; 381 const maxBackIterations = 30; 382 383 while (this.dropdownContent.children.length > 0 && backIterations < maxBackIterations) { 384 const visibleTabs = Array.from(wrapper.children).filter(child => 385 child.getAttribute('role') === 'tab' 386 ); 387 let totalTabsWidth = 0; 388 visibleTabs.forEach(tab => { 389 totalTabsWidth += tab.offsetWidth + this.getFlexGap(); 390 }); 391 392 const lastDropdownTab = this.dropdownContent.lastElementChild; 393 if (!lastDropdownTab) break; 394 395 const tabWidth = lastDropdownTab.offsetWidth + this.getFlexGap(); 396 if (constraintWidth >= (totalTabsWidth + tabWidth + buffer)) { 397 this.moveTabFromDropdown(lastDropdownTab, wrapper); 398 } else { 399 break; 400 } 401 backIterations++; 402 } 403 } 404 405 adjustLayoutNewUI() { 406 let wrapper = this.container.querySelector('.width-wrap'); 407 408 // If no .width-wrap found, create it and wrap all children 409 if (!wrapper) { 410 wrapper = this.createWidthWrapForNewUI(); 411 } 412 413 const constraintWidth = this.container.offsetWidth; 414 const buffer = 70; 415 const maxIterations = 30; 416 let iterations = 0; 417 418 // First, check if the selected tab is in the dropdown and move it to visible area 419 const selectedTabInDropdown = this.dropdownContent.querySelector('[role="tab"].selected'); 420 if (selectedTabInDropdown) { 421 // Move selected tab to the end of the wrapper (last visible position) 422 wrapper.appendChild(selectedTabInDropdown); 423 } 424 425 while (iterations < maxIterations) { 426 wrapper.offsetHeight; 427 428 if (constraintWidth >= (wrapper.scrollWidth + buffer)) { 429 break; 430 } 431 432 const nextTabToMove = this.getLastVisibleTab(wrapper); 433 434 if (!nextTabToMove) { 435 break; 436 } 437 438 if (nextTabToMove.classList.contains('selected')) { 439 // If the selected tab is too large and is the last one, move other tabs to dropdown 440 // Find the second-to-last tab to move instead 441 const visibleTabsList = Array.from(wrapper.children).filter(child => 442 child.getAttribute('role') === 'tab' 443 ); 444 if (visibleTabsList.length > 1) { 445 const secondToLast = visibleTabsList[visibleTabsList.length - 2]; 446 this.moveTabToDropdown(secondToLast); 447 } else { 448 // Only one tab and it's selected, keep it 449 break; 450 } 451 wrapper.offsetHeight; 452 iterations++; 453 continue; 454 } else { 455 this.moveTabToDropdown(nextTabToMove); 456 } 457 458 iterations++; 459 } 460 461 let backIterations = 0; 462 const maxBackIterations = 30; 463 464 while (this.dropdownContent.children.length > 0 && backIterations < maxBackIterations) { 465 wrapper.offsetHeight; 466 467 if (constraintWidth >= (wrapper.scrollWidth + 220)) { 468 const lastDropdownTab = this.dropdownContent.lastElementChild; 469 if (lastDropdownTab) { 470 this.moveTabFromDropdown(lastDropdownTab, wrapper); 471 } else { 472 break; 473 } 474 } else { 475 break; 476 } 477 backIterations++; 478 } 479 } 480 481 createWidthWrapForNewUI() { 482 const wrapper = document.createElement('div'); 483 wrapper.className = 'width-wrap'; 484 // FRE-2604 â ARIA-transparent so role="tab" children are semantic children of role="tablist" 485 wrapper.setAttribute('role', 'presentation'); 486 487 // Move all direct children (except dropdown) to the wrapper 488 const children = Array.from(this.container.children);
489 children.forEach(child => { 490 if (!child.classList.contains('tabs_more_link')) { 491 wrapper.appendChild(child); 492 } 493 }); 494 495 // Insert wrapper before the dropdown (if it exists) 496 const dropdown = this.container.querySelector('.tabs_more_link'); 497 if (dropdown) { 498 this.container.insertBefore(wrapper, dropdown); 499 } else { 500 this.container.appendChild(wrapper); 501 } 502 503 return wrapper; 504 } 505 506 getLastVisibleTab(wrapper) { 507 const visibleTabs = Array.from(wrapper.children).filter(child => 508 child.getAttribute('role') === 'tab' 509 ); 510 511 return visibleTabs.length > 0 ? visibleTabs[visibleTabs.length - 1] : null; 512 } 513 514 moveTabToDropdown(tab) { 515 this.dropdownContent.appendChild(tab); 516 } 517 518 moveTabFromDropdown(tab, wrapper) { 519 if (this.hasEvo) { 520 wrapper.appendChild(tab); 521 } else { 522 wrapper.insertBefore(tab, this.dropdown); 523 } 524 } 525 526 updateDropdownVisibility() { 527 if (!this.dropdown || !this.dropdownContent) return; 528 529 const hasTabsInDropdown = this.dropdownContent.children.length > 0; 530 531 if (hasTabsInDropdown) { 532 this.dropdown.style.display = ''; 533 this.dropdown.classList.remove('active'); 534 } else { 535 this.dropdown.style.display = 'none'; 536 } 537 } 538 539 resetTabs() { 540 if (this.container.classList.contains('bypass') || !this.dropdownContent) return; 541 542 const tabsInDropdown = Array.from(this.dropdownContent.children).filter(child => 543 child.getAttribute('role') === 'tab' 544 ); 545 const target = this.container.querySelector('.width-wrap') || this.container; 546 tabsInDropdown.forEach(tab => target.appendChild(tab)); 547 548 if (this.dropdown.parentNode) { 549 this.dropdown.remove(); 550 this.dropdown = null; 551 this.dropdownContent = null; 552 this.dropdownToggle = null; 553 } 554 } 555 556 refresh() { 557 this.adjustLayout(); 558 } 559 560 destroy() {
561 this.tabs.forEach(tab => { 562 tab.removeEventListener('click', this.boundTabClick); 563 tab.removeEventListener('keydown', this.boundTabKeydown); 564 }); 565 566 document.removeEventListener('click', this.boundDocumentClick); 567 document.removeEventListener('keydown', this.boundDocumentKeydown); 568 569 if (this.dropdownToggle) { 570 this.dropdownToggle.removeEventListener('click', this.boundDropdownToggle); 571 this.dropdownToggle.removeEventListener('keydown', this.boundDropdownToggleKeydown); 572 } 573 574 if (this.dropdownContent) { 575 this.dropdownContent.removeEventListener('click', this.boundDropdownContentClick); 576 } 577 578 this.resetTabs(); 579 580 if (this.container) { 581 delete this.container._tabnavInstance; 582 } 583 584 this.isInitialized = false; 585 } 586 587 debounce(func, wait) { 588 let timeout; 589 return (...args) => { 590 clearTimeout(timeout); 591 timeout = setTimeout(() => func(...args), wait); 592 }; 593 } 594 595 ensureTabSelected() { 596 if (!this.tabs.length) return; 597 598 const hasSelectedTab = this.tabs.some(tab => 599 tab.getAttribute('aria-selected') === 'true' || tab.classList.contains('selected') 600 ); 601 602 if (!hasSelectedTab) { 603 this.activateTab(this.tabs[0]); 604 } 605 } 606 607 reorderTabsBySelection(selectedTab) { 608 const target = this.container.querySelector('.width-wrap') || this.container; 609 selectedTab.remove(); 610 target.insertBefore(selectedTab, target.firstChild); 611 this.tabs = Array.from(this.container.querySelectorAll('[role="tab"]')); 612 } 613 614 getFlexGap() { 615 const style = window.getComputedStyle(this.container); 616 const gap = style.gap || style.columnGap || '0px'; 617 return parseInt(gap, 10) || 0; 618 } 619 620 isTabVisible(tab) { 621 if (!tab) return false; 622 623 // Check offsetParent (null for display:none elements) 624 if (!tab.offsetParent && tab.tagName !== 'BODY') { 625 return false; 626 } 627 628 const style = window.getComputedStyle(tab); 629 if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') { 630 return false; 631 } 632 633 return true; 634 } 635 636 static refresh() { 637 document.querySelectorAll('.tabnav').forEach(container => { 638 const instance = TabNav.getInstance(container); 639 if (instance && !container.classList.contains('bypass')) { 640 instance.refresh(); 641 } 642 }); 643 } 644 645 static resetTabs() { 646 document.querySelectorAll('.tabnav').forEach(container => { 647 const instance = TabNav.getInstance(container); 648 if (instance && !container.classList.contains('bypass')) { 649 instance.resetTabs(); 650 } 651 }); 652 } 653 654 static getInstance(container) { 655 return container._tabnavInstance || null; 656 } 657 658 static initOnContainer(container) { 659 if (!container) return null; 660 if (TabNav.getInstance(container)) return TabNav.getInstance(container); 661 return new TabNav(container); 662 } 663 664 static initAll() { 665 document.querySelectorAll('.tabnav').forEach(TabNav.initOnContainer); 666 } 667 668 static saveFocusState(tab) { 669 try { 670 const tabId = tab.getAttribute('id'); 671 const tabHref = tab.getAttribute('href'); 672 const containerClass = tab.closest('.tabnav')?.className; 673 674 if (tabId || tabHref) { 675 sessionStorage.setItem('tabnav_last_focus', JSON.stringify({ 676 id: tabId, 677 href: tabHref, 678 containerClass: containerClass, 679 timestamp: Date.now() 680 })); 681 } 682 } catch (e) { 683 // SessionStorage might be unavailable 684 console.warn('TabNav: Could not save focus state', e); 685 } 686 } 687 688 static restoreFocusState() { 689 try { 690 const savedFocus = sessionStorage.getItem('tabnav_last_focus'); 691 if (!savedFocus) return false; 692 693 const focusData = JSON.parse(savedFocus); 694 695 // Only restore if it's recent (within 5 seconds) 696 if (Date.now() - focusData.timestamp > 5000) { 697 sessionStorage.removeItem('tabnav_last_focus'); 698 return false; 699 } 700 701 let targetTab = null; 702 703 // Try to find by ID first 704 if (focusData.id) { 705 targetTab = document.getElementById(focusData.id); 706 } 707 708 // Fall back to href if ID not found 709 if (!targetTab && focusData.href) { 710 targetTab = document.querySelector(`[role="tab"][href="${focusData.href}"]`); 711 } 712 713 // Fall back to any tab with matching href in the same container type 714 if (!targetTab && focusData.href && focusData.containerClass) { 715 const containers = document.querySelectorAll(`.tabnav`);
716 containers.forEach(container => { 717 if (container.className === focusData.containerClass) { 718 const tab = container.querySelector(`[role="tab"][href="${focusData.href}"]`); 719 if (tab) targetTab = tab; 720 } 721 }); 722 } 723 724 if (targetTab) { 725 // Wait for page to settle before focusing 726 requestAnimationFrame(() => { 727 targetTab.focus(); 728 // Scroll into view if needed 729 targetTab.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 730 }); 731 732 // Clear the saved state 733 sessionStorage.removeItem('tabnav_last_focus'); 734 return true; 735 } 736 737 return false; 738 } catch (e) { 739 console.warn('TabNav: Could not restore focus state', e); 740 return false; 741 } 742 } 743} 744 745document.addEventListener('DOMContentLoaded', () => { 746 TabNav.initAll(); 747 748 // Restore focus after a short delay to ensure all tabs are rendered 749 setTimeout(() => { 750 TabNav.restoreFocusState(); 751 }, 100); 752 753 const observer = new MutationObserver(mutations => { 754 let hasNewTabs = false; 755 mutations.forEach(mutation => { 756 mutation.addedNodes.forEach(node => { 757 if (node.nodeType !== Node.ELEMENT_NODE) return; 758 if (node.classList?.contains('tabnav')) { 759 TabNav.initOnContainer(node); 760 hasNewTabs = true; 761 } else { 762 const tabnavs = node.querySelectorAll?.('.tabnav'); 763 if (tabnavs?.length > 0) { 764 tabnavs.forEach(TabNav.initOnContainer); 765 hasNewTabs = true; 766 } 767 } 768 }); 769 }); 770 771 // Try to restore focus if new tabs were added (for AJAX/partial loads) 772 if (hasNewTabs) { 773 setTimeout(() => TabNav.restoreFocusState(), 50); 774 } 775 }); 776 777 observer.observe(document.body, { 778 childList: true, 779 subtree: true 780 }); 781}); 782 783window.initTabNav = (selector, options) => { 784 const container = document.querySelector(selector); 785 return container ? TabNav.initOnContainer(container, options) : null; 786}; 787 788window.initAllTabNavs = () => { 789 TabNav.initAll(); 790}; 791 792window.initializeTabNavsAndSelectFirst = () => { 793 document.querySelectorAll('.tabnav').forEach(container => { 794 const instance = TabNav.getInstance(container); 795 if (instance) instance.ensureTabSelected(); 796 }); 797}; 798 799window.restoreTabNavFocus = () => { 800 return TabNav.restoreFocusState(); 801}; 802 803if (typeof module !== 'undefined' && module.exports) { 804 module.exports = TabNav; 805}
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.