1// JavaScript Document 2class ActionBar { 3 /* Function Initialize Action Bar : START 4 Initializes one action bar instance, binds tab events, hydrates initial state, and sets up article overflow controls. 5 Function Initialize Action Bar : END */ 6 constructor(root) { 7 this.root = root; 8 this.tabs = Array.from(root.querySelectorAll('.action-bar__tab, [role="tab"]')); 9 this.storageKey = this._getStorageKey(); 10 this._onClick = this._onClick.bind(this); 11 this._onKeydown = this._onKeydown.bind(this); 12 this.root.addEventListener('click', this._onClick); 13 this.root.addEventListener('keydown', this._onKeydown); 14 this._hydrate(); 15 16 this._onOverflowScroll = this._updateOverflowControls.bind(this); 17 this._onOverflowResize = this._updateOverflowControls.bind(this); 18 this._onPrevOverflowClick = () => this._scrollOverflowBy(-1); 19 this._onNextOverflowClick = () => this._scrollOverflowBy(1); 20 21 if (this.root.classList.contains('action-bar')) { 22 // Wait one tick so any OverlayScrollbars init on the same page can complete first. 23 setTimeout(() => this._setupOverflowControls(), 0); 24 } 25 } 26 27 /* Function Get Selectors : START 28 Reads a tab's target selector list from data-action-bar-target or href and supports comma-separated panel selectors. 29 Function Get Selectors : END */ 30 _getSelectors(tab) { 31 const target = tab.getAttribute('data-action-bar-target'); 32 if (target) { 33 return target.split(',').map(s => s.trim()).filter(Boolean); 34 } 35 36 const href = tab.getAttribute('href'); 37 if (!href || !href.startsWith('#')) return []; 38 return [href.trim()]; 39 } 40 41 /* Function Get Tab Link : START 42 Finds the navigation link associated with a tab when the tab is an anchor or is nested inside an anchor. 43 Function Get Tab Link : END */ 44 _getTabLink(tab) { 45 if (!tab) return null; 46 return tab.matches('a[href]') ? tab : tab.closest('a[href]'); 47 } 48 49 /* Function Is Local Tab : START 50 Checks whether a tab controls panels on the current page instead of navigating to another page. 51 Function Is Local Tab : END */ 52 _isLocalTab(tab) { 53 return this._getSelectors(tab).length > 0; 54 } 55 56 /* Function Is Disabled Tab : START 57 Checks disabled state for button tabs and aria-disabled state for anchor or button tabs. 58 Function Is Disabled Tab : END */ 59 _isDisabledTab(tab) { 60 return Boolean(tab.disabled) || tab.getAttribute('aria-disabled') === 'true'; 61 } 62 63 /* Function Get Panels : START 64 Finds all panels targeted by a tab and dedupes them so one tab can safely control multiple panels. 65 Function Get Panels : END */ 66 _getPanels(tab) { 67 const sels = this._getSelectors(tab); 68 if (!sels.length) return []; 69 const seen = new Set(); 70 const out = []; 71 sels.forEach(sel => { 72 try { 73 document.querySelectorAll(sel).forEach(el => { 74 if (!seen.has(el)) { seen.add(el); out.push(el); } 75 }); 76 } catch (_error) { 77 // Ignore invalid selectors so link-style tabs can still work normally. 78 } 79 }); 80 return out; 81 } 82 83 /* Function Get Storage Key : START 84 Builds the sessionStorage key used to remember the active tab for this page and action bar. 85 Function Get Storage Key : END */ 86 _getStorageKey() { 87 const rootId = this.root.id || this.root.getAttribute('aria-label') || 'default'; 88 return `action-bar:${window.location.pathname}:${rootId}`; 89 } 90 91 /* Function Get Tab Identifier : START 92 Gets the stable identifier used for storing or restoring a tab. 93 Function Get Tab Identifier : END */ 94 _getTabIdentifier(tab) { 95 if (!tab) return null; 96 const link = this._getTabLink(tab); 97 return tab.id || this._getSelectors(tab)[0] || tab.getAttribute('href') || (link && link.getAttribute('href')) || null; 98 } 99 100 /* Function Match Tab Identifier : START 101 Checks whether a tab matches a stored identifier, hash, target selector, or href. 102 Function Match Tab Identifier : END */ 103 _tabMatchesIdentifier(tab, identifier) { 104 if (!tab || !identifier) return false; 105 const normalizedId = identifier.startsWith('#') ? identifier : `#${identifier}`; 106 return tab.id === identifier || 107 this._getSelectors(tab).includes(identifier) || 108 this._getSelectors(tab).includes(normalizedId) || 109 tab.getAttribute('href') === identifier || 110 tab.getAttribute('href') === normalizedId || 111 (this._getTabLink(tab) && this._getTabLink(tab).getAttribute('href') === identifier) || 112 (this._getTabLink(tab) && this._getTabLink(tab).getAttribute('href') === normalizedId); 113 } 114
115 /* Function Persist Active Tab : START 116 Saves the currently active tab to sessionStorage when storage is available. 117 Function Persist Active Tab : END */ 118 _persistActiveTab(tab) { 119 const identifier = this._getTabIdentifier(tab); 120 if (!identifier || !window.sessionStorage) return; 121 try { 122 window.sessionStorage.setItem(this.storageKey, identifier); 123 } catch (_error) { 124 // Ignore storage failures and keep the component functional. 125 } 126 } 127 128 /* Function Get Initial Active Tab : START 129 Chooses the initial active tab from the URL hash, saved session state, markup state, or the first tab. 130 Function Get Initial Active Tab : END */ 131 _getInitialActiveTab() { 132 const hash = window.location.hash; 133 if (hash) { 134 const hashMatch = this.tabs.find(tab => this._tabMatchesIdentifier(tab, hash)); 135 if (hashMatch) return hashMatch; 136 } 137 138 if (window.sessionStorage) { 139 try { 140 const saved = window.sessionStorage.getItem(this.storageKey); 141 if (saved) { 142 const savedMatch = this.tabs.find(tab => this._tabMatchesIdentifier(tab, saved)); 143 if (savedMatch) return savedMatch; 144 } 145 } catch (_error) { 146 // Ignore storage failures and fall through to markup state. 147 } 148 } 149 150 return this.tabs.find(tab => tab.getAttribute('aria-selected') === 'true') || this.tabs[0] || null; 151 } 152
153 /* Function Activate Tab : START 154 Activates a tab, shows its target panels, persists the selection, and scrolls it into view when needed. 155 Function Activate Tab : END */ 156 _activate(tab) { 157 if (this._isDisabledTab(tab)) return; 158 this.tabs.forEach(t => this._deactivate(t)); 159 const panels = this._getPanels(tab); 160 tab.setAttribute('aria-selected', 'true'); 161 tab.removeAttribute('tabindex'); 162 panels.forEach(panel => { 163 panel.hidden = false; 164 requestAnimationFrame(() => panel.classList.add('action-bar__panel--show')); 165 }); 166 this._persistActiveTab(tab); 167 this._ensureTabVisible(tab); 168 } 169 170 /* Function Deactivate Tab : START 171 Deactivates a tab and hides every panel targeted by that tab. 172 Function Deactivate Tab : END */ 173 _deactivate(tab) { 174 const panels = this._getPanels(tab); 175 tab.setAttribute('aria-selected', 'false'); 176 tab.setAttribute('tabindex', '-1'); 177 panels.forEach(panel => { 178 panel.hidden = true; 179 panel.classList.remove('action-bar__panel--show'); 180 }); 181 } 182 183 /* Function Select Tab : START 184 Updates the active tab state for link-style tabs that navigate away instead of controlling local panels. 185 Function Select Tab : END */ 186 _selectTab(tab) { 187 if (this._isDisabledTab(tab)) return;
188 this.tabs.forEach(t => { 189 t.setAttribute('aria-selected', t === tab ? 'true' : 'false'); 190 if (t === tab) t.removeAttribute('tabindex'); 191 else t.setAttribute('tabindex', '-1'); 192 }); 193 this._persistActiveTab(tab); 194 this._ensureTabVisible(tab); 195 } 196 197 /* Function Handle Click : START 198 Handles pointer activation for tab buttons using event delegation from the action bar root. 199 Function Handle Click : END */ 200 _onClick(e) { 201 const btn = e.target.closest('.action-bar__tab, [role="tab"]'); 202 if (!btn) return; 203 204 if (this._isDisabledTab(btn)) { 205 e.preventDefault(); 206 return; 207 } 208 209 if (!this._isLocalTab(btn)) { 210 this._selectTab(btn); 211 return; 212 } 213 214 e.preventDefault(); 215 this._activate(btn); 216 } 217 218 /* Function Handle Keydown : START 219 Handles keyboard navigation and activation for accessible tab behavior. 220 Function Handle Keydown : END */ 221 _onKeydown(e) { 222 const current = e.target.closest('.action-bar__tab, [role="tab"]'); 223 if (!current) return; 224 const idx = this.tabs.indexOf(current); 225 if (idx < 0) return; 226 let nextIdx = null; 227 const isVertical = this.root.classList.contains('action-bar--vertical'); 228 if (e.key === 'ArrowRight' || (isVertical && e.key === 'ArrowDown')) 229 nextIdx = (idx + 1) % this.tabs.length; 230 if (e.key === 'ArrowLeft' || (isVertical && e.key === 'ArrowUp')) 231 nextIdx = (idx - 1 + this.tabs.length) % this.tabs.length; 232 if (e.key === 'Home') nextIdx = 0; 233 if (e.key === 'End') nextIdx = this.tabs.length - 1; 234 if (nextIdx !== null) { e.preventDefault(); this.tabs[nextIdx].focus(); } 235 if (e.key === 'Enter' || e.key === ' ') { 236 if (this._isLocalTab(current)) { 237 e.preventDefault(); 238 this._activate(current); 239 } else { 240 this._selectTab(current); 241 } 242 } 243 } 244 245 /* Function Hydrate State : START 246 Normalizes the initial tab/panel state after page load and syncs aria-controls with targeted panel IDs. 247 Function Hydrate State : END */ 248 _hydrate() { 249 const initialTab = this._getInitialActiveTab();
250 this.tabs.forEach(t => { 251 const active = t === initialTab; 252 t.setAttribute('role', 'tab'); 253 t.setAttribute('aria-selected', active ? 'true' : 'false'); 254 if (!active) t.setAttribute('tabindex', '-1'); 255 if (active) t.removeAttribute('tabindex'); 256 const panels = this._getPanels(t); 257 panels.forEach(p => { 258 p.hidden = !active; 259 if (active) p.classList.add('action-bar__panel--show'); 260 }); 261 // Optional: sync aria-controls with all targeted IDs (if present) 262 const ids = panels.map(p => p.id).filter(Boolean); 263 if (ids.length) t.setAttribute('aria-controls', ids.join(' ')); 264 }); 265 if (initialTab) { 266 requestAnimationFrame(() => this._ensureTabVisible(initialTab, { behavior: 'auto', align: 'center' })); 267 } 268 } 269 270 /* Function Setup Overflow Controls : START 271 Creates overflow scroll controls for article action bars and wires resize/scroll listeners. 272 Function Setup Overflow Controls : END */ 273 _setupOverflowControls() { 274 this.scrollContainer = this.root.querySelector('.action-bar__scroll-area'); 275 if (!this.scrollContainer || this.root.querySelector('[data-action-bar-scroll-prev]')) return; 276 277 this.prevButton = this._createOverflowButton('prev'); 278 this.nextButton = this._createOverflowButton('next'); 279 this.scrollContainer.append(this.prevButton, this.nextButton); 280 281 this.prevButton.addEventListener('click', this._onPrevOverflowClick); 282 this.nextButton.addEventListener('click', this._onNextOverflowClick); 283 284 if ('ResizeObserver' in window) { 285 this._resizeObserver = new ResizeObserver(() => this._updateOverflowControls()); 286 this._resizeObserver.observe(this.scrollContainer); 287 const tabList = this.root.querySelector('.action-bar__list'); 288 if (tabList) this._resizeObserver.observe(tabList); 289 } 290 291 window.addEventListener('resize', this._onOverflowResize); 292 this._attachScrollViewportListener(); 293 this._updateOverflowControls(); 294 const activeTab = this.tabs.find(tab => tab.getAttribute('aria-selected') === 'true'); 295 if (activeTab) { 296 requestAnimationFrame(() => this._ensureTabVisible(activeTab, { behavior: 'auto', align: 'center' })); 297 } 298 } 299 300 /* Function Create Overflow Button : START 301 Builds the previous or next overflow button used to scroll horizontally through article tabs. 302 Function Create Overflow Button : END */ 303 _createOverflowButton(direction) { 304 const button = document.createElement('button'); 305 button.type = 'button'; 306 button.className = `btn btn--secondary btn--sm btn--icon-only action-bar__scroll-btn action-bar__scroll-btn--${direction}`; 307 button.setAttribute('aria-label', direction === 'prev' ? 'Scroll tabs left' : 'Scroll tabs right'); 308 button.toggleAttribute(`data-action-bar-scroll-${direction}`, true); 309 button.hidden = true; 310 button.innerHTML = direction === 'prev' 311 ? '<svg class="icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M9.53033 3.46967C9.82322 3.76256 9.82322 4.23744 9.53033 4.53033L6.06066 8L9.53033 11.4697C9.82322 11.7626 9.82322 12.2374 9.53033 12.5303C9.23744 12.8232 8.76256 12.8232 8.46967 12.5303L4.46967 8.53033C4.17678 8.23744 4.17678 7.76256 4.46967 7.46967L8.46967 3.46967C8.76256 3.17678 9.23744 3.17678 9.53033 3.46967Z" fill="currentColor"/></svg>' 312 : '<svg class="icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M6.46967 3.46967C6.76256 3.17678 7.23744 3.17678 7.53033 3.46967L11.5303 7.46967C11.8232 7.76256 11.8232 8.23744 11.5303 8.53033L7.53033 12.5303C7.23744 12.8232 6.76256 12.8232 6.46967 12.5303C6.17678 12.2374 6.17678 11.7626 6.46967 11.4697L9.93934 8L6.46967 4.53033C6.17678 4.23744 6.17678 3.76256 6.46967 3.46967Z" fill="currentColor"/></svg>'; 313 return button; 314 } 315 316 /* Function Get Overlay Instance : START 317 Gets the OverlayScrollbars instance for the tab scroll area when that plugin is available. 318 Function Get Overlay Instance : END */ 319 _getOverlayInstance() { 320 if (!this.scrollContainer || !window.OverlayScrollbarsGlobal) return null; 321 const { OverlayScrollbars } = window.OverlayScrollbarsGlobal; 322 if (typeof OverlayScrollbars !== 'function') return null; 323 try { 324 return OverlayScrollbars(this.scrollContainer); 325 } catch (_error) { 326 return null; 327 } 328 } 329 330 /* Function Get Scroll Viewport : START 331 Resolves the actual horizontal scrolling element, preferring OverlayScrollbars' viewport when present. 332 Function Get Scroll Viewport : END */ 333 _getScrollViewport() { 334 const instance = this._getOverlayInstance(); 335 if (instance && typeof instance.elements === 'function') { 336 const { viewport } = instance.elements(); 337 if (viewport) return viewport; 338 } 339 return this.scrollContainer || null; 340 } 341 342 /* Function Attach Scroll Viewport Listener : START 343 Attaches the scroll listener to the current scroll viewport and swaps listeners if the viewport changes. 344 Function Attach Scroll Viewport Listener : END */ 345 _attachScrollViewportListener() { 346 const viewport = this._getScrollViewport(); 347 if (!viewport || this.scrollViewport === viewport) return; 348 if (this.scrollViewport) { 349 this.scrollViewport.removeEventListener('scroll', this._onOverflowScroll); 350 } 351 this.scrollViewport = viewport; 352 this.scrollViewport.addEventListener('scroll', this._onOverflowScroll, { passive: true });
353 } 354 355 /* Function Update Overflow Controls : START 356 Shows, hides, enables, disables, and state-tags the overflow controls based on current horizontal scroll position. 357 Function Update Overflow Controls : END */ 358 _updateOverflowControls() { 359 if (!this.scrollContainer || !this.prevButton || !this.nextButton) return; 360 361 this._attachScrollViewportListener(); 362 const viewport = this.scrollViewport || this.scrollContainer; 363 const maxScrollLeft = Math.max(0, viewport.scrollWidth - viewport.clientWidth); 364 const isOverflowing = maxScrollLeft > 1; 365 const scrollLeft = viewport.scrollLeft; 366 const hasPrevOverflow = scrollLeft > 1; 367 const hasNextOverflow = scrollLeft < maxScrollLeft - 1; 368 369 this.root.classList.toggle('action-bar--scrollable', isOverflowing); 370 this.root.classList.toggle('action-bar--has-prev', isOverflowing && hasPrevOverflow); 371 this.root.classList.toggle('action-bar--has-next', isOverflowing && hasNextOverflow); 372 this.prevButton.hidden = !isOverflowing; 373 this.nextButton.hidden = !isOverflowing; 374 375 if (!isOverflowing) { 376 this.prevButton.disabled = true; 377 this.nextButton.disabled = true; 378 return; 379 } 380 381 this.prevButton.disabled = !hasPrevOverflow; 382 this.nextButton.disabled = !hasNextOverflow; 383 } 384 385 /* Function Scroll Overflow : START 386 Scrolls the tab list left or right when an overflow control button is clicked. 387 Function Scroll Overflow : END */ 388 _scrollOverflowBy(direction) { 389 if (!this.scrollContainer) return; 390 391 this._attachScrollViewportListener(); 392 const viewport = this.scrollViewport || this.scrollContainer; 393 const distance = Math.max(viewport.clientWidth * 0.65, 160) * direction; 394 395 viewport.scrollBy({ left: distance, behavior: 'smooth' }); 396 requestAnimationFrame(() => this._updateOverflowControls()); 397 setTimeout(() => this._updateOverflowControls(), 250); 398 } 399 400 /* Function Ensure Tab Visible : START 401 Keeps the active tab comfortably visible inside the horizontal scroll area. 402 Function Ensure Tab Visible : END */ 403 _ensureTabVisible(tab, options = {}) { 404 if (!this.scrollContainer || !tab) return; 405 406 this._attachScrollViewportListener(); 407 const viewport = this.scrollViewport || this.scrollContainer; 408 const edgeBuffer = options.edgeBuffer ?? 24; 409 const comfortableBuffer = options.comfortableBuffer ?? Math.max(edgeBuffer, Math.min(viewport.clientWidth * 0.18, 72)); 410 const align = options.align ?? 'auto'; 411 const behavior = options.behavior ?? 'smooth'; 412 const viewportRect = viewport.getBoundingClientRect(); 413 const tabRect = tab.getBoundingClientRect(); 414 const tabLeft = tabRect.left - viewportRect.left + viewport.scrollLeft; 415 const tabRight = tabLeft + tabRect.width; 416 const visibleLeft = viewport.scrollLeft + comfortableBuffer; 417 const visibleRight = viewport.scrollLeft + viewport.clientWidth - comfortableBuffer; 418 const maxScrollLeft = Math.max(0, viewport.scrollWidth - viewport.clientWidth); 419 let nextScrollLeft = null; 420 421 if (align === 'center') { 422 nextScrollLeft = Math.min( 423 maxScrollLeft, 424 Math.max(0, tabLeft - ((viewport.clientWidth - tabRect.width) / 2)) 425 ); 426 } else if (tabLeft < visibleLeft) { 427 nextScrollLeft = Math.max(0, tabLeft - comfortableBuffer); 428 } else if (tabRight > visibleRight) { 429 nextScrollLeft = Math.min(maxScrollLeft, Math.max(0, tabRight - viewport.clientWidth + comfortableBuffer)); 430 } 431 432 if (nextScrollLeft !== null) { 433 viewport.scrollTo({ left: nextScrollLeft, behavior }); 434 requestAnimationFrame(() => this._updateOverflowControls()); 435 setTimeout(() => this._updateOverflowControls(), 250); 436 } 437 } 438 439 /* Function Show Tab : START 440 Public helper for activating a tab element programmatically. 441 Function Show Tab : END */ 442 show(tab) { this._activate(tab); } 443 444 /* Function Show Tab By ID : START 445 Public helper for activating a tab by panel ID or hash. 446 Function Show Tab By ID : END */ 447 showById(id) { 448 const hash = id.startsWith('#') ? id : `#${id}`; 449 const t = this.tabs.find(t => 450 (t.getAttribute('data-action-bar-target') || '').split(',').map(s=>s.trim()).includes(hash) || 451 t.getAttribute('href') === hash 452 ); 453 if (t) this._activate(t); 454 } 455
456 /* Function Dispose Action Bar : START 457 Removes event listeners and observers created by this action bar instance. 458 Function Dispose Action Bar : END */ 459 dispose() { 460 this.root.removeEventListener('click', this._onClick); 461 this.root.removeEventListener('keydown', this._onKeydown); 462 if (this.scrollViewport) { 463 this.scrollViewport.removeEventListener('scroll', this._onOverflowScroll); 464 } 465 if (this.prevButton) { 466 this.prevButton.removeEventListener('click', this._onPrevOverflowClick); 467 } 468 if (this.nextButton) { 469 this.nextButton.removeEventListener('click', this._onNextOverflowClick); 470 } 471 if (this._resizeObserver) { 472 this._resizeObserver.disconnect(); 473 } 474 window.removeEventListener('resize', this._onOverflowResize); 475 } 476} 477 478/* Function Initialize Action Bars On DOM Ready : START 479 Initializes every action bar found on the page after the initial DOM is ready. 480 Function Initialize Action Bars On DOM Ready : END */ 481document.addEventListener('DOMContentLoaded', () => { 482 document.querySelectorAll('[data-action-bar]').forEach(el => new ActionBar(el)); 483});
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.