PageSourceSearch

https://www.brotherssupply.com/wp-content/plugins/hozio-studio/com…/swatches/swatches.js?ver=115a4abaadf1

js brotherssupply.com collected 2026-10-09 00:30:33 UTC 21,723 bytes, 464 lines download raw bytes

1/* ═════════════════════════════════════════════════════════════════════════
2   VARIATION SWATCHES (W-131) — widget behavior (Phase-2 widget system)
3
4   PROGRESSIVE ENHANCEMENT over WooCommerce's native variable-product
5   `.variations_form`. The native <select> is BOTH the JS-off fallback AND the
6   source of truth; this file paints tappable swatches (color chips / image
7   thumbs / size pills) as a radiogroup over each attribute row and, on
8   activation, sets the select's value and dispatches a native `change` — so
9   WooCommerce's own variations_form.js does ALL the real work (price,
10   availability, gallery image, the hidden variation_id, add-to-cart enable).
11
12   We never fork add-to-cart, register no endpoint, and emit no new hzo:* event
13   (native `change` is WC's contract; hzo.announce is an API, not a DOM event).
14   Out-of-stock / incompatible combinations are struck by MIRRORING WC's own
15   per-option `disabled` state — caught with a MutationObserver so it stays
16   accurate on every recompute, with or without jQuery.
17
18   Self-contained, no hard dependencies:
19     · Bails at load unless a `.variations_form` exists (near-zero off-product).
20     · hzo.announce (W-021) is guarded at event time — never a load-order dep.
21     · jQuery is used ONLY when present (WC's own bus) to commit the value the way
22       WC binds; a native fallback keeps the select correct without it.
23
24   A11y (WAI-ARIA APG Radio Group, explicit-activation variant): each attribute
25   group is role=radiogroup; each swatch role=radio + aria-checked, roving
26   tabindex, Arrow/Home/End move focus, Space/Enter activate. Selection does NOT
27   follow focus (deliberate — arrowing across an out-of-stock swatch must not
28   silently change the cart-bound selection; OOS swatches stay focusable +
29   discoverable, aria-disabled, and announce their unavailability on activation
30   so focus is never stranded — audit G22). Every swatch carries a real
31   aria-label (the term name); chips/thumbs are aria-hidden; the hover/focus tip
32   is a supplement, never the accessible name (§11.3).
33
34   Reduced motion / forced-colors / print handled in swatches.css.
35
36   Public API: window.hzo.swatches = { init, enhance } (init(scope) for
37   AJAX-injected content — e.g. a Quick-View modal, W-132).
38   ═════════════════════════════════════════════════════════════════════════ */
39(function () {
40    'use strict';
41
42    /* ── i18n bridge (guarded — JS strings translate on WPML/Polylang) ─────── */
43    var i18n = (window.wp && window.wp.i18n) ? window.wp.i18n : null;
44    function __(s) { return (i18n && i18n.__) ? i18n.__(s, 'hozio-studio') : s; }
45    function fmt(str) {
46        var args = arguments, i = 1;
47        if (i18n && i18n.sprintf) { return i18n.sprintf.apply(i18n, args); }
48        return String(str).replace(/%s/g, function () { return args[i++]; });
49    }
50
51    /* ── Shared-primitive guards (event-time, never parse-time) ────────────── */
52    function announce(msg) {
53        if (msg && window.hzo && typeof window.hzo.announce === 'function') {
54            window.hzo.announce(msg, 'polite');
55        }
56    }
57    function warn(msg) { if (window.console && console.warn) { console.warn('[hzo_swatches] ' + msg); } }
58
59    var FORM_SELECTOR = 'form.variations_form, .variations_form';
60
61    /* ── Config marker (optional) → per-page overrides ─────────────────────── */
62    function parseMap(str) {
63        var out = {};
64        if (!str) { return out; }
65        var pairs = String(str).split('|');
66        for (var i = 0; i < pairs.length; i++) {
67            var idx = pairs[i].indexOf(':');   // first colon only (URLs keep theirs)
68            if (idx === -1) { continue; }
69            var k = pairs[i].slice(0, idx).trim();
70            var v = pairs[i].slice(idx + 1).trim();
71            if (k) { out[k] = v; }
72        }
73        return out;
74    }
75
76    function readConfig() {
77        var cfg = { form: '', type: 'auto', types: {}, colors: {}, images: {}, tooltip: true, label: true };
78        var node = document.querySelector('.hzo-swatches-config[data-hzo-swatches]');
79        if (!node) { return cfg; }
80        cfg.form = node.getAttribute('data-hzo-swatches-form') || '';
81        var t = node.getAttribute('data-hzo-swatches-type');
82        if (t) { cfg.type = t; }
83        cfg.types = parseMap(node.getAttribute('data-hzo-swatches-types'));
84        cfg.colors = parseMap(node.getAttribute('data-hzo-swatches-colors'));
85        cfg.images = parseMap(node.getAttribute('data-hzo-swatches-images'));
86        if (node.getAttribute('data-hzo-swatches-tooltip') === 'false') { cfg.tooltip = false; }
87        if (node.getAttribute('data-hzo-swatches-label') === 'false') { cfg.label = false; }
88        return cfg;
89    }
90
91    /* ── Small DOM/value helpers ───────────────────────────────────────────── */
92    function attrName(select) {
93        return select.getAttribute('name') || select.getAttribute('data-attribute_name') || '';
94    }
95    function attrSlug(select) { return attrName(select).replace(/^attribute_/, ''); }
96
97    function isColor(v) {
98        if (!v) { return false; }
99        if (v.charAt(0) === '#') { return true; }
100        if (window.CSS && window.CSS.supports) { return window.CSS.supports('color', v); }
101        return false;
102    }
103
104    function groupLabel(select) {
105        if (select.id) {
106            var l = document.querySelector('label[for="' + select.id + '"]');
107            if (l) { return (l.textContent || '').trim(); }
108        }
109        var td = select.closest ? select.closest('td') : null;
110        var tr = td ? td.parentNode : null;
111        var th = tr ? tr.querySelector('.label, th') : null;
112        if (th) { return (th.textContent || '').trim(); }
113        return __('Options');
114    }
115
116    function parseVariations(form) {
117        var raw = form.getAttribute('data-product_variations');
118        if (!raw || raw === 'false') { return null; }   // WC AJAX-threshold mode
119        try { return JSON.parse(raw); } catch (e) { return null; }
120    }
121
122    function variationImage(variations, slug, value) {
123        if (!variations) { return ''; }
124        var key = 'attribute_' + slug;
125        for (var i = 0; i < variations.length; i++) {
126            var a = variations[i].attributes || {};
127            if (a[key] === value && variations[i].image && variations[i].image.src) {
128                return variations[i].image.src;
129            }
130        }
131        return '';
132    }
133
134    function stripHtml(html) {
135        var d = document.createElement('div');
136        d.innerHTML = html;                          // read-only: only textContent is used
137        return (d.textContent || '').replace(/\s+/g, ' ').trim();
138    }
139
140    function resolveType(slug, cfg) {
141        var bare = slug.replace(/^pa_/, '');
142        if (cfg.types[slug]) { return cfg.types[slug]; }
143        if (cfg.types[bare]) { return cfg.types[bare]; }
144        if (cfg.type && cfg.type !== 'auto') { return cfg.type; }
145        if (/colou?r/.test(slug)) { return 'color'; }   // image is opt-in only
146        return 'pill';
147    }
148
149    /* ── Swatch construction ───────────────────────────────────────────────── */
150    function makeSwatch(opt, type, slug, cfg, variations) {
151        var value = opt.value;
152        var name = (opt.textContent || value).trim();
153        var btn = document.createElement('button');
154        btn.type = 'button';
155        btn.setAttribute('role', 'radio');
156        btn.setAttribute('aria-checked', 'false');
157        btn.setAttribute('tabindex', '-1');
158        btn.setAttribute('data-value', value);
159        btn.setAttribute('aria-label', name);
160
161        var resolved = type;
162
163        if (type === 'color') {
164            var color = cfg.colors[value] || opt.getAttribute('data-hzo-swatch-color') || (isColor(value) ? value : '');
165            if (color) {
166                btn.style.setProperty('--hzo-swatch-color', color);
167                var chip = document.createElement('span');
168                chip.className = 'hzo-swatches__chip';
169                chip.setAttribute('aria-hidden', 'true');
170                btn.appendChild(chip);
171            } else {
172                resolved = 'pill';   // unresolved color → per-swatch pill fallback
173            }
174        } else if (type === 'image') {
175            var img = cfg.images[value] || variationImage(variations, slug, value);
176            if (img) {
177                var thumb = document.createElement('span');
178                thumb.className = 'hzo-swatches__chip hzo-swatches__chip--image';
179                thumb.setAttribute('aria-hidden', 'true');
180                thumb.style.backgroundImage = 'url("' + img.replace(/["\\]/g, '') + '")';
181                btn.appendChild(thumb);
182            } else {
183                resolved = 'pill';
184            }
185        }
186
187        btn.className = 'hzo-swatches__swatch hzo-swatches__swatch--' + resolved;
188
189        if (resolved === 'pill') {
190            var text = document.createElement('span');
191            text.className = 'hzo-swatches__text';
192            text.textContent = name;
193            btn.appendChild(text);
194        } else if (cfg.tooltip) {
195            // Supplemental tip for chip swatches (no visible text). The real
196            // accessible name is aria-label above; this tip is aria-hidden.
197            var tip = document.createElement('span');
198            tip.className = 'hzo-swatches__tip';
199            tip.setAttribute('aria-hidden', 'true');
200            tip.textContent = name;
201            btn.appendChild(tip);
202        }
203
204        if (opt.disabled) { markUnavailable(btn, true); }
205        return btn;
206    }
207
208    function markUnavailable(btn, on) {
209        if (on) {
210            btn.setAttribute('aria-disabled', 'true');
211            btn.classList.add('is-unavailable');
212        } else {
213            btn.removeAttribute('aria-disabled');
214            btn.classList.remove('is-unavailable');
215        }
216    }
217
218    function buildGroup(select, cfg, variations) {
219        var slug = attrSlug(select);
220        var type = resolveType(slug, cfg);
221        var group = document.createElement('div');
222        group.className = 'hzo-swatches';
223        group.setAttribute('role', 'radiogroup');
224        group.setAttribute('aria-label', groupLabel(select));
225        group.setAttribute('data-hzo-swatches-for', select.id || slug);
226        group.setAttribute('data-hzo-swatches-type', type);
227
228        var made = 0;
229        var options = select.options;
230        for (var i = 0; i < options.length; i++) {
231            var opt = options[i];
232            if (!opt.value) { continue; }             // skip "Choose an option"
233            group.appendChild(makeSwatch(opt, type, slug, cfg, variations));
234            made++;
235        }
236        if (!made) { return null; }
237
238        if (cfg.label) {
239            var sel = document.createElement('span');
240            sel.className = 'hzo-swatches__selected';
241            sel.setAttribute('aria-hidden', 'true');
242            group.appendChild(sel);
243        }
244        return group;
245    }
246
247    /* ── Group state helpers ───────────────────────────────────────────────── */
248    function swatches(group) { return group.querySelectorAll('.hzo-swatches__swatch'); }
249    function swatchName(sw) { return sw.getAttribute('aria-label') || sw.getAttribute('data-value') || ''; }
250
251    function optionFor(select, value) {
252        var opts = select.options;
253        for (var i = 0; i < opts.length; i++) { if (opts[i].value === value) { return opts[i]; } }
254        return null;
255    }
256
257    function setRoving(list, target) {
258        for (var i = 0; i < list.length; i++) {
259            list[i].setAttribute('tabindex', list[i] === target ? '0' : '-1');
260        }
261    }
262    function firstEnabled(list) {
263        for (var i = 0; i < list.length; i++) {
264            if (list[i].getAttribute('aria-disabled') !== 'true') { return list[i]; }
265        }
266        return null;
267    }
268
269    // Reflect the select's current value + WC's per-option disabled state onto
270    // the swatches. Idempotent — safe to run from click, change, or observer.
271    function syncGroup(group, select) {
272        var val = select.value;
273        var list = swatches(group);
274        var checked = null;
275        for (var i = 0; i < list.length; i++) {
276            var sw = list[i];
277            var v = sw.getAttribute('data-value');
278            var opt = optionFor(select, v);
279            markUnavailable(sw, !!(opt && opt.disabled));
280            var on = (val !== '' && v === val);
281            sw.setAttribute('aria-checked', on ? 'true' : 'false');
282            sw.classList.toggle('is-selected', on);
283            if (on) { checked = sw; }
284        }
285        var rove = checked || firstEnabled(list) || list[0];
286        if (rove) { setRoving(list, rove); }
287        var label = group.querySelector('.hzo-swatches__selected');
288        if (label) { label.textContent = checked ? swatchName(checked) : ''; }
289    }
290
291    /* ── Commit a value the way WooCommerce is listening ───────────────────── */
292    function commitValue(select, value) {
293        if (window.jQuery) {
294            // WC binds change through jQuery; set + trigger once via the same bus.
295            try { window.jQuery(select).val(value).trigger('change'); return; } catch (e) { /* fall through */ }
296        }
297        select.value = value;
298        var ev;
299        try { ev = new Event('change', { bubbles: true }); }
300        catch (e2) { ev = document.createEvent('Event'); ev.initEvent('change', true, false); }
301        select.dispatchEvent(ev);
302    }
303
304    /* ── Selection announcement (SR) — LIVE-REGION-OUTSIDE-FRAGMENT via W-021 ── */
305    function selectsIn(form) { return form.querySelectorAll('.variations select'); }
306    function selectedName(select) {
307        var o = select.options[select.selectedIndex];
308        return o ? (o.textContent || o.value).trim() : select.value;
309    }
310
311    function matchVariation(variations, list) {
312        if (!variations) { return null; }
313        for (var i = 0; i < variations.length; i++) {
314            var attrs = variations[i].attributes || {};
315            var ok = true;
316            for (var j = 0; j < list.length; j++) {
317                var name = attrName(list[j]);
318                var want = list[j].value;
319                var has = attrs[name];
320                // An empty variation attribute value matches ANY chosen value.
321                if (has !== undefined && has !== '' && has !== want) { ok = false; break; }
322            }
323            if (ok) { return variations[i]; }
324        }
325        return null;
326    }
327
328    function announceSelection(form, variations) {
329        var list = selectsIn(form);
330        var chosen = [];
331        var allChosen = true;
332        for (var i = 0; i < list.length; i++) {
333            if (!list[i].value) { allChosen = false; }
334            else { chosen.push(selectedName(list[i])); }
335        }
336        if (!chosen.length) { announce(__('Selection cleared.')); return; }
337        if (!allChosen || !variations) { announce(chosen.join(', ')); return; }
338
339        var match = matchVariation(variations, list);
340        if (!match) { announce(fmt(__('%s is unavailable.'), chosen.join(', '))); return; }
341
342        var parts = [fmt(__('Selected: %s'), chosen.join(', '))];
343        if (match.price_html) { parts.push(stripHtml(match.price_html)); }
344        parts.push(match.is_in_stock === false ? __('Out of stock') : __('In stock'));
345        announce(parts.join('. ') + '.');
346    }
347
348    /* ── Activation + keyboard ─────────────────────────────────────────────── */
349    function activate(group, select, sw, form, variations) {
350        var list = swatches(group);
351        setRoving(list, sw);
352        sw.focus();
353
354        if (sw.getAttribute('aria-disabled') === 'true') {
355            announce(fmt(__('%s is unavailable with the current selection.'), swatchName(sw)));
356            return;
357        }
358        var value = sw.getAttribute('data-value');
359        var already = sw.getAttribute('aria-checked') === 'true';
360        commitValue(select, already ? '' : value);   // re-activate clears (WC disables ATC)
361        syncGroup(group, select);                     // immediate reflect; observer re-confirms
362        announceSelection(form, variations);
363    }
364
365    function moveFocus(group, current, dir) {
366        var list = swatches(group);
367        if (!list.length) { return; }
368        var idx = 0;
369        for (var i = 0; i < list.length; i++) { if (list[i] === current) { idx = i; break; } }
370        var next;
371        if (dir === 'home') { next = 0; }
372        else if (dir === 'end') { next = list.length - 1; }
373        else {
374            next = idx + dir;
375            if (next < 0) { next = list.length - 1; }
376            if (next >= list.length) { next = 0; }
377        }
378        setRoving(list, list[next]);
379        list[next].focus();
380    }
381
382    function wireGroup(group, select, form, variations) {
383        group.addEventListener('click', function (e) {
384            var sw = e.target.closest ? e.target.closest('.hzo-swatches__swatch') : null;
385            if (sw && group.contains(sw)) { activate(group, select, sw, form, variations); }
386        });
387        group.addEventListener('keydown', function (e) {
388            var sw = e.target.closest ? e.target.closest('.hzo-swatches__swatch') : null;
389            if (!sw) { return; }
390            var k = e.key;
391            if (k === ' ' || k === 'Spacebar' || k === 'Enter') {
392                e.preventDefault();
393                activate(group, select, sw, form, variations);
394                return;
395            }
396            var dir = null;
397            if (k === 'ArrowRight' || k === 'ArrowDown') { dir = 1; }
398            else if (k === 'ArrowLeft' || k === 'ArrowUp') { dir = -1; }
399            else if (k === 'Home') { dir = 'home'; }
400            else if (k === 'End') { dir = 'end'; }
401            if (dir === null) { return; }
402            e.preventDefault();
403            moveFocus(group, sw, dir);
404        });
405    }
406
407    // Mirror WC's option-disabling + programmatic resets without a jQuery dep.
408    function observeSelect(group, select) {
409        select.addEventListener('change', function () { syncGroup(group, select); });
410        if (!window.MutationObserver) { return; }
411        var mo = new window.MutationObserver(function () { syncGroup(group, select); });
412        mo.observe(select, { attributes: true, subtree: true, attributeFilter: ['disabled'], childList: true });
413    }
414
415    /* ── Per-form enhancement ──────────────────────────────────────────────── */
416    function enhance(form, cfg) {
417        if (!form || form.getAttribute('data-hzo-swatches-ready') === 'true') { return; }
418        var selects = form.querySelectorAll('table.variations select, .variations select');
419        if (!selects.length) { return; }
420        cfg = cfg || readConfig();
421        form.setAttribute('data-hzo-swatches-ready', 'true');
422        form.classList.add('hzo-swatches-active');   // CSS hides the native <select>s
423
424        var variations = parseVariations(form);
425        for (var i = 0; i < selects.length; i++) {
426            var select = selects[i];
427            var group = buildGroup(select, cfg, variations);
428            if (!group) { continue; }
429            // Place the radiogroup right after the select inside its td.value.
430            if (select.parentNode) { select.parentNode.appendChild(group); }
431            wireGroup(group, select, form, variations);
432            observeSelect(group, select);
433            syncGroup(group, select);                // initial checked + roving + label
434        }
435    }
436
437    /* ── Init ──────────────────────────────────────────────────────────────── */
438    function init(scope) {
439        var root = scope || document;
440        var cfg = readConfig();
441        var selector = cfg.form || FORM_SELECTOR;
442        var forms;
443        try { forms = root.querySelectorAll(selector); }
444        catch (e) { warn('invalid form selector "' + selector + '" — falling back to default.'); forms = root.querySelectorAll(FORM_SELECTOR); }
445
446        for (var i = 0; i < forms.length; i++) { enhance(forms[i], cfg); }
447
448        if (!forms.length && document.querySelector('.hzo-swatches-config[data-hzo-swatches]')) {
449            // Dev-warning convention §14 — the shortcode is on a page with no
450            // variable product for it to enhance.
451            warn('no WooCommerce variations_form found on this page — swatches have nothing to enhance.');
452        }
453    }
454
455    if (document.readyState === 'loading') {
456        document.addEventListener('DOMContentLoaded', function () { init(); });
457    } else {
458        init();
459    }
460
461    // Shared namespace — enhance(form) for AJAX-injected forms (Quick-View W-132).
462    window.hzo = window.hzo || {};
463    window.hzo.swatches = { init: init, enhance: enhance };
464})();

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.