PageSourceSearch

https://www.brotherssupply.com/wp-content/plugins/hozio-studio/components/tabs/tabs.js?ver=396f046422d6

js brotherssupply.com collected 2026-10-09 00:31:11 UTC 12,784 bytes, 244 lines download raw bytes

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.