1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 TABS (panel switch) â widget behavior (Phase-2 widget system; moved out of 3 main.js) 4 5 Self-contained, no hard dependencies. As a shortcode widget the tabs 6 component ALWAYS loads: tabs.php un-gates it via the 7 hzostudio_component_markers filter, because [hzo_tabs] can render from 8 surfaces the loader's post_content scan can't see (header/footer parts, 9 text widgets, template/ACF fields). 10 11 WAI-ARIA tabs (APG Tabs pattern). MANUAL activation (default): arrow keys 12 move focus between tabs but do NOT switch the panel â Enter/Space (or 13 click) activates the focused tab. Manual is safer for panels with heavy 14 content (arrowing doesn't thrash loads). Roving tabindex follows focus; 15 the selected tab is the tab stop. On load the JS syncs everything from 16 whichever tab has aria-selected="true" (or .is-active, or the first tab). 17 The arrow-key axis follows aria-orientation on the tablist (absent = 18 horizontal: Left/Right; "vertical": Up/Down); Home/End jump to the ends. 19 20 Deep-linking (FROZEN for attribute-less markup): the tabs' aria-controls 21 values ARE the panel ids. A URL hash matching a panel id activates that 22 tab on init (beats the authored selection â no replaceState there, the 23 hash is already what the user arrived with), and every USER activation 24 mirrors the panel id into the hash via history.replaceState â no history 25 entry, no scroll jump. Markup contract: component-patterns.md §36. 26 27 OPTION CONTRACT â every setting is a data-attribute on `.hzo-tabs`, so the 28 block pattern (author writes attributes) and the [hzo_tabs] shortcode 29 (tabs.php generates them from settings) drive the SAME behavior. Every 30 option defaults OFF/legacy â attribute-free markup behaves exactly as 31 before: 32 33 data-hzo-tabs-activation "auto" â automatic activation: arrows/Home/End 34 activate the tab they move focus to (APG "tabs 35 with automatic activation" â fine for light 36 panels). Absent/other = manual (legacy). 37 data-hzo-tabs-hash "false" â hash deep-linking off for this 38 tabset: the URL hash is ignored on init and 39 user activations never touch the URL (use when 40 a page's hash is owned by something else, or 41 for secondary tabsets on a page that already 42 has a deep-linked one). Absent = on (legacy). 43 44 No motion is scripted here (panel switching is instant; the tab-chrome CSS 45 transitions die under prefers-reduced-motion in tabs.css), and tabs have 46 no dismiss paths to guard. 47 48 Public API (no historical hzo.* names existed for this component â the old 49 main.js IIFE exposed nothing): 50 hzo.tabs = { init, setup } (init(scope) for AJAX content) 51 hzo:tabchange â CustomEvent bubbling from the .hzo-tabs root on every 52 USER activation (click/Enter/Space, or arrows under activation=auto; 53 never on init sync), detail { id, index, tab, panel }. id is the 54 panel id (the aria-controls value, '' if none), index the tab's 55 position in the tabset. 56 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 57(function () { 58 'use strict'; 59 60 function boolAttr(el, name, def) { 61 var v = el.getAttribute('data-hzo-tabs-' + name); 62 if (v === null || v === '') { return def; } 63 return v !== 'false' && v !== '0'; 64 } 65 66 function tabsIn(root) { 67 // Scope to THIS tabset: a nested [hzo_tabs] inside a panel would otherwise 68 // be enumerated (and toggled) by the outer tabset's activate()/keydown. 69 // Keep only tabs whose nearest .hzo-tabs ancestor is this root. 70 return Array.prototype.slice.call(root.querySelectorAll('[role="tab"]')).filter(function (t) { 71 return t.closest('.hzo-tabs') === root; 72 }); 73 } 74 75 function panelOf(root, tab) { 76 var id = tab.getAttribute('aria-controls'); 77 return id ? root.querySelector('#' + (window.CSS && CSS.escape ? CSS.escape(id) : id)) : null; 78 } 79 80 function setRoving(tabs, focusTab) { 81 for (var i = 0; i < tabs.length; i++) tabs[i].tabIndex = (tabs[i] === focusTab) ? 0 : -1; 82 } 83 84 // G16: the block PATTERN (tabs.html) ships FIXED ids (hzo-tab-1 / hzo-panel-1). 85 // Drop that pattern twice on a page and the ids collide â invalid HTML that 86 // breaks aria-controls / aria-labelledby resolution and hash deep-linking. 87 // Re-namespace a tabset's ids to a unique per-instance prefix WHEN its ids are 88 // already taken by an earlier-processed tabset, keeping the tabâpanel wiring 89 // intact. Processed in document order (init loops roots in order), so the 90 // FIRST tabset keeps its clean ids (deep-links stay valid) and only later 91 // duplicates are renamed. Shortcode output already carries unique ids 92 // (hzo-tabs-sc-N-â¦), so it never trips this. AJAX-injected tabsets dedupe 93 // against everything seen so far (module-scope registry). 94 var tabsInstance = 0; 95 var usedTabIds = {}; 96 function uniquifyIds(root, tabs) { 97 var i, needsPrefix = false;
98 for (i = 0; i < tabs.length; i++) { 99 if (usedTabIds[tabs[i].id] || usedTabIds[tabs[i].getAttribute('aria-controls')]) { needsPrefix = true; break; } 100 } 101 if (needsPrefix) { 102 var prefix = 'hzo-tabs-i' + (++tabsInstance) + '-'; 103 for (i = 0; i < tabs.length; i++) { 104 var tab = tabs[i]; 105 var panel = panelOf(root, tab); // resolve by OLD id first 106 var oldPanelId = tab.getAttribute('aria-controls'); 107 if (tab.id) { tab.id = prefix + tab.id; } 108 if (oldPanelId) { 109 var newPanelId = prefix + oldPanelId; 110 tab.setAttribute('aria-controls', newPanelId); 111 if (panel) { 112 panel.id = newPanelId; 113 if (tab.id) { panel.setAttribute('aria-labelledby', tab.id); } 114 } 115 } 116 } 117 } 118 // Claim the FINAL ids (renamed or original) so later tabsets see them taken. 119 for (i = 0; i < tabs.length; i++) { 120 if (tabs[i].id) { usedTabIds[tabs[i].id] = true; } 121 var pid = tabs[i].getAttribute('aria-controls'); 122 if (pid) { usedTabIds[pid] = true; } 123 } 124 } 125 126 function activate(root, tab, isUser) { 127 var tabs = tabsIn(root); 128 for (var i = 0; i < tabs.length; i++) { 129 var sel = tabs[i] === tab; 130 tabs[i].setAttribute('aria-selected', sel ? 'true' : 'false'); 131 tabs[i].classList.toggle('is-active', sel); 132 var panel = panelOf(root, tabs[i]); 133 if (panel) panel.hidden = !sel; 134 } 135 setRoving(tabs, tab); // selected tab becomes the tab stop 136 if (!isUser) return; // init sync: no hash write, no event (legacy) 137 // USER activations only: mirror the panel id into the hash. replaceState 138 // (not location.hash) so there's no scroll jump and no history-entry- 139 // per-click. data-hzo-tabs-hash="false" opts the tabset out. 140 var id = tab.getAttribute('aria-controls'); 141 if (id && boolAttr(root, 'hash', true) && window.history && history.replaceState) { 142 try { history.replaceState(null, '', '#' + id); } catch (err) { /* sandboxed/file: context â hash stays as-is */ } 143 } 144 root.dispatchEvent(new CustomEvent('hzo:tabchange', { 145 bubbles: true, 146 detail: { id: id || '', index: tabs.indexOf(tab), tab: tab, panel: panelOf(root, tab) } 147 })); 148 } 149 150 function moveFocus(tabs, tab) { // manual activation: move focus only 151 setRoving(tabs, tab); 152 tab.focus(); 153 } 154 155 function setup(root) { 156 if (!root || root.getAttribute('data-hzo-tabs-ready') === 'true') { return; } 157 var list = root.querySelector('[role="tablist"]'); 158 var tabs = tabsIn(root); 159 if (!list || !tabs.length) { return; } // not wireable yet â a later init(scope) may retry 160 root.setAttribute('data-hzo-tabs-ready', 'true'); 161 162 // G16: de-collide fixed pattern ids BEFORE wiring + initial sync, so 163 // aria-controls / hash matching below run against the final ids. 164 uniquifyIds(root, tabs); 165 166 // Click activation, delegated on the root so tabs injected into an 167 // already-initialized tabset still work (the keydown handler below 168 // re-queries the same way). No preventDefault â matches the legacy 169 // per-tab handler, so tabs authored as <a> keep native anchor behavior. 170 root.addEventListener('click', function (e) { 171 var tab = e.target && e.target.closest ? e.target.closest('[role="tab"]') : null; 172 if (!tab || !root.contains(tab)) { return; } 173 activate(root, tab, true); 174 }); 175 176 list.addEventListener('keydown', function (e) { 177 var ts = tabsIn(root); 178 var i = ts.indexOf(document.activeElement); 179 if (i < 0) return; 180 var horizontal = list.getAttribute('aria-orientation') !== 'vertical'; 181 var nextKey = horizontal ? 'ArrowRight' : 'ArrowDown'; 182 var prevKey = horizontal ? 'ArrowLeft' : 'ArrowUp'; 183 var next = null;
184 if (e.key === nextKey) next = ts[(i + 1) % ts.length]; 185 else if (e.key === prevKey) next = ts[(i - 1 + ts.length) % ts.length]; 186 else if (e.key === 'Home') next = ts[0]; 187 else if (e.key === 'End') next = ts[ts.length - 1]; 188 else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); activate(root, document.activeElement, true); return; } 189 else return; 190 e.preventDefault(); 191 moveFocus(ts, next); 192 // Automatic activation (opt-in): moving focus also selects the tab. 193 if (root.getAttribute('data-hzo-tabs-activation') === 'auto') activate(root, next, true); 194 }); 195 196 // Sync initial state from the authored selection (aria-selected / .is-active / first). 197 var initial = null; 198 for (var j = 0; j < tabs.length; j++) { 199 if (tabs[j].getAttribute('aria-selected') === 'true' || tabs[j].classList.contains('is-active')) { initial = tabs[j]; break; } 200 } 201 // Hash deep-link beats the authored selection: #<panel-id> (a tab's 202 // aria-controls value) opens straight to that tab. No replaceState 203 // here â the hash is already what the user arrived with. 204 if (boolAttr(root, 'hash', true)) { 205 var hash = window.location.hash ? window.location.hash.slice(1) : ''; 206 if (hash) { 207 for (var h = 0; h < tabs.length; h++) { 208 if (tabs[h].getAttribute('aria-controls') === hash) { initial = tabs[h]; break; } 209 } 210 } 211 } 212 activate(root, initial || tabs[0]); 213 } 214 215 function init(scope) { 216 var roots = (scope || document).querySelectorAll('.hzo-tabs'); 217 for (var r = 0; r < roots.length; r++) { setup(roots[r]); } 218 } 219 220 // G35: a hash change AFTER load (a #panel-id link clicked elsewhere on the 221 // page, or Back/Forward) also activates the matching tab â deep-linking is not 222 // only an on-load affordance. Bound once at module scope. 223 window.addEventListener('hashchange', function () { 224 var hash = window.location.hash ? window.location.hash.slice(1) : ''; 225 if (!hash) { return; } 226 var sel = (window.CSS && CSS.escape) ? CSS.escape(hash) : hash.replace(/["\\]/g, '\\$&'); 227 var tab = document.querySelector('[role="tab"][aria-controls="' + sel + '"]'); 228 if (!tab) { return; } 229 var root = tab.closest('.hzo-tabs'); 230 if (root && root.getAttribute('data-hzo-tabs-ready') === 'true' && boolAttr(root, 'hash', true)) { 231 activate(root, tab, true); 232 } 233 }); 234 235 if (document.readyState === 'loading') { 236 document.addEventListener('DOMContentLoaded', function () { init(); }); 237 } else { 238 init(); 239 } 240 241 // Shared engine namespace (never a new top-level global). 242 window.hzo = window.hzo || {}; 243 window.hzo.tabs = { init: init, setup: setup }; 244})();
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.