PageSourceSearch

https://www.schoolhealthnylearn.com/javascripts/components/tabnav.js?1787161510

js schoolhealthnylearn.com collected 2026-09-29 15:15:14 UTC 23,794 bytes, 805 lines download raw bytes

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.