1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 QUANTITY STEPPER â widget behavior (Phase-2 widget system; moved out of 3 main.js "Quantity stepper â clamp + bounded buttons + change event"). 4 5 Self-contained, no hard dependencies (uses no main.js primitives). As a 6 shortcode widget the stepper ALWAYS loads: quantity-stepper.php un-gates it 7 via the hzostudio_component_markers filter (a no-op â the stepper was never 8 gated â kept for convention uniformity, see toast.php). 9 10 MARKUP CONTRACT (unchanged from main.js â this is public API; commerce.js 11 and integrations depend on it): 12 .hzo-qty[data-hzo-qty] wrapping 13 .hzo-qty__btn--minus / [data-hzo-qty-minus] 14 .hzo-qty__input 15 .hzo-qty__btn--plus / [data-hzo-qty-plus] 16 Clamps to data-min / data-max on the root (or the input's min / max, which 17 win; min defaults to 1), disables each button at its bound, and dispatches 18 a bubbling `hzo:qty-change` CustomEvent { value, min, max } (value is a 19 Number â commerce.js type-checks it). It NEVER POSTs â a real cart wires 20 onto the event (parity with the add-to-cart surface). Distinct selectors 21 from the mini-cart's data-hzo-qty-up/down so the two never collide. 22 CSS contract: quantity-stepper.css. 23 24 OPTION CONTRACT â settings are data-attributes on `.hzo-qty`, so the 25 authored markup and the [hzo_quantity_stepper] shortcode (quantity-stepper 26 .php generates them) drive the SAME behavior. Every option defaults to the 27 legacy value â attribute-free markup behaves exactly as before: 28 29 data-hzo-quantity-stepper-step increment per button press / arrow key 30 (int ⥠1; absent/invalid = 1, the legacy 31 step) 32 33 A11y (quantity-selector pattern): native text input (inputmode="numeric") 34 with aria-labelled ± buttons â no role="spinbutton" retrofit (a native 35 input already exposes value/edits to AT; the role would demand manual 36 aria-valuenow bookkeeping for no gain). APG spinbutton keyboard support: 37 ArrowUp / ArrowDown on the input step the value (Home/End are left native â 38 they edit the caret in a text input and hijacking them would break text 39 editing). Nothing here animates, so there is no reduced-motion branch; the 40 CSS kills its own transitions under prefers-reduced-motion. 41 42 Public API (the old main.js IIFE exposed no window.hzo names â nothing 43 historical to preserve): 44 hzo.quantityStepper = { init, setup, set } (init(scope) for AJAX content) 45 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 46(function () { 47 'use strict'; 48 49 function bounds(qty, input) { 50 var min = input.hasAttribute('min') ? parseInt(input.getAttribute('min'), 10) 51 : (parseInt(qty.getAttribute('data-min'), 10)); 52 if (isNaN(min)) min = 1; 53 var maxRaw = input.hasAttribute('max') ? input.getAttribute('max') : qty.getAttribute('data-max'); 54 var max = (maxRaw === null || maxRaw === '') ? null : parseInt(maxRaw, 10); 55 if (max !== null && isNaN(max)) max = null; 56 return { min: min, max: max }; 57 } 58 59 // Option: step size (default 1 = the legacy behavior). 60 function stepOf(qty) { 61 var s = parseInt(qty.getAttribute('data-hzo-quantity-stepper-step'), 10); 62 return (isNaN(s) || s < 1) ? 1 : s; 63 } 64 65 function apply(qty, input, val, emit) { 66 var b = bounds(qty, input); 67 if (isNaN(val)) val = b.min; 68 if (val < b.min) val = b.min; 69 if (b.max !== null && val > b.max) val = b.max; 70 input.value = val; 71 var minus = qty.querySelector('.hzo-qty__btn--minus, [data-hzo-qty-minus]'); 72 var plus = qty.querySelector('.hzo-qty__btn--plus, [data-hzo-qty-plus]'); 73 var disMinus = val <= b.min; 74 var disPlus = (b.max !== null && val >= b.max); 75 // G22: never disable the button that currently holds focus without first 76 // moving focus to the opposite, still-enabled button â a disabled focused 77 // control strands keyboard focus on <body>. 78 var active = document.activeElement; 79 if (disMinus && minus && active === minus && plus && !disPlus) { plus.focus(); } 80 else if (disPlus && plus && active === plus && minus && !disMinus) { minus.focus(); }
81 if (minus) minus.disabled = disMinus; 82 if (plus) plus.disabled = disPlus; 83 if (emit) qty.dispatchEvent(new CustomEvent('hzo:qty-change', { bubbles: true, detail: { value: val, min: b.min, max: b.max } })); 84 } 85 86 function nudge(qty, input, dir) { 87 var current = parseInt(input.value, 10); 88 if (isNaN(current)) current = bounds(qty, input).min; 89 apply(qty, input, current + dir * stepOf(qty), true); 90 } 91 92 // ââ Delegated listeners â bound ONCE at module scope, so dynamically- 93 // injected steppers work without setup() (same as the main.js original). 94 document.addEventListener('click', function (e) { 95 var btn = e.target.closest ? e.target.closest('.hzo-qty__btn--minus, .hzo-qty__btn--plus, [data-hzo-qty-minus], [data-hzo-qty-plus]') : null; 96 if (!btn) return; 97 var qty = btn.closest('.hzo-qty[data-hzo-qty]'); 98 if (!qty) return; 99 var input = qty.querySelector('.hzo-qty__input'); 100 if (!input) return; 101 nudge(qty, input, (btn.classList.contains('hzo-qty__btn--plus') || btn.hasAttribute('data-hzo-qty-plus')) ? 1 : -1); 102 }); 103 104 document.addEventListener('change', function (e) { 105 var input = e.target; 106 if (!input.classList || !input.classList.contains('hzo-qty__input')) return; 107 var qty = input.closest('.hzo-qty[data-hzo-qty]'); 108 if (!qty) return; 109 apply(qty, input, parseInt(input.value, 10), true); 110 }); 111 112 // APG spinbutton keyboard map on the input (ArrowUp/ArrowDown only â see 113 // header). preventDefault stops any caret movement / page scroll. 114 document.addEventListener('keydown', function (e) { 115 if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; 116 var input = e.target; 117 if (!input.classList || !input.classList.contains('hzo-qty__input')) return; 118 var qty = input.closest('.hzo-qty[data-hzo-qty]'); 119 if (!qty) return; 120 e.preventDefault(); 121 nudge(qty, input, e.key === 'ArrowUp' ? 1 : -1); 122 }); 123 124 /* Per-instance seeding: numeric keypad hint + initial clamped value / 125 disabled bounds, WITHOUT emitting (no phantom cart sync on load â 126 identical to the main.js original). Idempotent via the ready guard. */ 127 function setup(qty) { 128 if (!qty || qty.getAttribute('data-hzo-quantity-stepper-ready') === 'true') { return; } 129 var input = qty.querySelector('.hzo-qty__input'); 130 if (!input) return; 131 qty.setAttribute('data-hzo-quantity-stepper-ready', 'true'); 132 if (!input.getAttribute('inputmode')) input.setAttribute('inputmode', 'numeric'); 133 apply(qty, input, parseInt(input.value, 10), false); // seed disabled state, no event on load 134 } 135 136 function init(scope) { 137 var all = (scope || document).querySelectorAll('.hzo-qty[data-hzo-qty]'); 138 for (var i = 0; i < all.length; i++) { setup(all[i]); } 139 } 140 141 // Programmatic setter for integrations: clamps + syncs buttons + emits. 142 function set(rootOrId, value) { 143 var qty = typeof rootOrId === 'string' ? document.getElementById(rootOrId) : rootOrId; 144 if (!qty) return; 145 var input = qty.querySelector('.hzo-qty__input'); 146 if (!input) return; 147 apply(qty, input, parseInt(value, 10), true); 148 } 149 150 if (document.readyState === 'loading') { 151 document.addEventListener('DOMContentLoaded', function () { init(); }); 152 } else { 153 init(); 154 } 155 156 // Shared engine namespace (never a new top-level global). 157 window.hzo = window.hzo || {}; 158 window.hzo.quantityStepper = { init: init, setup: setup, set: set }; 159})();
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.