1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 MINI-CART â widget behavior (Phase-2 widget system; moved out of main.js) 3 4 The mini-cart IS a .hzo-drawer composition â open/close/trap/lock/Esc/ 5 focus-return machinery all lives in components/drawer/drawer.js (open it 6 via data-hzo-drawer-open="hzo-mini-cart" or hzo.openDrawer; commerce.js 7 auto-opens it after add-to-cart). THIS file only owns the line-item 8 behavior: 9 10 · qty steppers (±, min 1 â use remove to go lower) recompute the line 11 total + subtotal from each item's data-unit-price; 12 · remove deletes the line, moves focus (tag-removal pattern: next â 13 previous â empty-state CTA/close), recomputes, and shows the 14 .hzo-empty state + hides the footer when the last item goes. 15 16 UI surface only â real cart wiring is assets/js/commerce.js + the 17 fragment pipeline in includes/wc-ajax.php. Contract: component-patterns §46. 18 19 COMMERCE COUPLING (every touchpoint below is frozen public API): 20 · Live WooCommerce carts: rows carry data-cart-item-key and their prices 21 are server-rendered wc_price HTML (correct currency/format). 22 commerce.js + the cart-fragments filter own those numbers â writing 23 our client-side money here would corrupt them for ~600 ms until the 24 sync lands. Only the demo/static drawer (data-unit-price, no item key) 25 gets the client-side money math; the empty/footer/list visibility sync 26 runs either way. 27 · REGISTRATION ORDER IS API: commerce.js's own delegated click handler 28 reads the settled count off the row, expecting THIS handler to have 29 already stepped it (optimistic update first). This script's listener 30 must register before commerce.js runs â components enqueue before 31 hozio-studio-commerce in hozio-studio.php's priority-999 closure. 32 · commerce.js focus-restores through the same selectors used here 33 (.hzo-minicart__item[data-cart-item-key], [data-hzo-qty-up/down], 34 [data-hzo-minicart-remove], .hzo-minicart .hzo-drawer__close) â all 35 byte-frozen. 36 37 OPTION CONTRACT â data-attributes on the `.hzo-minicart` root; the block 38 pattern (hand-authored) and the [hzo_mini_cart] shortcode (mini-cart.php 39 generates them) drive the SAME behavior. Every option defaults to the 40 legacy value â attribute-free markup behaves exactly as before: 41 42 data-hzo-mini-cart-currency money prefix for the DEMO/static math 43 (default "$", the legacy hardcoded format; 44 live carts never touch this â their money 45 is server-rendered wc_price HTML) 46 data-hzo-mini-cart-max-qty upper clamp for the ± stepper (int ⥠1; 47 absent/invalid = unclamped, the legacy 48 behavior). A count already above the clamp 49 is left alone â the stepper just refuses 50 to go higher. 51 52 A11y: the drawer supplies the WAI-ARIA APG Dialog semantics (trap, Esc, 53 focus return); in here the counts are aria-live="polite" (authored markup 54 and wc-ajax fragments both carry it), and remove moves focus per the APG 55 deletion pattern (next control â previous â empty-state CTA â drawer 56 close) so focus never drops to <body> inside the trap. Nothing here 57 animates â no reduced-motion branch needed (mini-cart.css kills its own 58 transitions under prefers-reduced-motion). 59 60 Self-contained: uses NO shared hzo.* primitives, so there is no 61 hozio-studio-main dependency to guard. 62 63 Public API (the old main.js IIFE exposed no window.hzo names â nothing 64 historical to preserve): 65 hzo.miniCart = { init, setup, recompute } (init(scope) for AJAX content) 66 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 67(function () { 68 'use strict'; 69 70 // Money for the DEMO/static path only (live carts keep server-rendered 71 // wc_price HTML). Default '$' prefix = the legacy hardcoded format. 72 function money(n, symbol) { 73 return (symbol || '$') + (Math.round(n * 100) / 100).toFixed(2); 74 } 75 76 function recompute(cart) {
77 var items = cart.querySelectorAll('.hzo-minicart__item'); 78 // Live WooCommerce cart? Its rows carry data-cart-item-key and their 79 // prices are server-rendered wc_price HTML (correct currency/format). 80 // commerce.js + the cart-fragments filter own those numbers â writing 81 // our client-side money here would corrupt them for ~600ms until the 82 // sync lands. Only the demo/static drawer (data-unit-price) gets the 83 // client-side money math; either way the visibility sync runs. 84 var live = !!cart.querySelector('.hzo-minicart__item[data-cart-item-key]'); 85 if (!live) { 86 var symbol = cart.getAttribute('data-hzo-mini-cart-currency') || '$'; 87 var subtotal = 0; 88 for (var i = 0; i < items.length; i++) { 89 var price = parseFloat(items[i].getAttribute('data-unit-price')) || 0; 90 var countEl = items[i].querySelector('.hzo-minicart__count'); 91 var qty = countEl ? (parseInt(countEl.textContent, 10) || 0) : 0; 92 var lineEl = items[i].querySelector('.hzo-minicart__linetotal'); 93 if (lineEl) lineEl.textContent = money(price * qty, symbol); 94 subtotal += price * qty; 95 } 96 var sv = cart.querySelector('.hzo-minicart__subtotal-value'); 97 if (sv) sv.textContent = money(subtotal, symbol); 98 } 99 var isEmpty = items.length === 0; 100 var empty = cart.querySelector('.hzo-minicart__empty'); 101 var footer = cart.querySelector('.hzo-minicart__footer'); 102 var list = cart.querySelector('.hzo-minicart__items'); 103 if (empty) empty.hidden = !isEmpty; 104 if (footer) footer.hidden = isEmpty; 105 if (list) list.hidden = isEmpty; 106 } 107 108 // Option: stepper upper clamp (null = unclamped, the legacy behavior). 109 function maxQtyOf(cart) { 110 var v = parseInt(cart.getAttribute('data-hzo-mini-cart-max-qty') || '', 10); 111 return (isNaN(v) || v < 1) ? null : v; 112 } 113 114 // Delegated once at script scope (exactly like the legacy IIFE â carts and 115 // fragment-swapped lists injected later keep working). Registered BEFORE 116 // commerce.js's own click handler (see REGISTRATION ORDER in the header). 117 document.addEventListener('click', function (e) { 118 if (!e.target.closest) return; 119 var cart = e.target.closest('.hzo-minicart'); 120 if (!cart) return; 121 122 var step = e.target.closest('[data-hzo-qty-up], [data-hzo-qty-down]'); 123 if (step) { 124 var item = step.closest('.hzo-minicart__item'); 125 var count = item && item.querySelector('.hzo-minicart__count'); 126 if (!count) return; 127 var q = parseInt(count.textContent, 10) || 1; 128 if (step.hasAttribute('data-hzo-qty-up')) { 129 var max = maxQtyOf(cart); 130 if (max === null || q < max) q = q + 1; // clamp only blocks going higher 131 } else { 132 q = Math.max(1, q - 1); 133 } 134 count.textContent = q; 135 recompute(cart); 136 return; 137 } 138 139 var rm = e.target.closest('[data-hzo-minicart-remove]'); 140 if (rm) { 141 var row = rm.closest('.hzo-minicart__item'); 142 var list = row && row.parentElement; 143 if (!list) return; 144 var removes = Array.prototype.slice.call(list.querySelectorAll('[data-hzo-minicart-remove]')); 145 var idx = removes.indexOf(rm); 146 list.removeChild(row); 147 recompute(cart); // toggle empty/footer/list visibility FIRST, so the 148 // empty-state CTA is focusable before we move focus to it 149 var remaining = list.querySelectorAll('[data-hzo-minicart-remove]'); 150 if (remaining.length) { 151 remaining[Math.min(idx, remaining.length - 1)].focus(); // next, or previous if last 152 } else { 153 var fallback = cart.querySelector('.hzo-minicart__empty a, .hzo-minicart__empty button') || 154 cart.querySelector('.hzo-drawer__close'); 155 if (fallback) fallback.focus(); 156 } 157 } 158 }); 159 160 /* Per-root init â idempotent via the ready guard. Setup is just the 161 initial recompute (line totals + empty/footer/list visibility), the 162 same pass the legacy IIFE ran over every cart at load. */ 163 function setup(cart) { 164 if (!cart || cart.getAttribute('data-hzo-mini-cart-ready') === 'true') return; 165 cart.setAttribute('data-hzo-mini-cart-ready', 'true'); 166 recompute(cart); 167 } 168 169 function init(scope) { 170 var carts = (scope || document).querySelectorAll('.hzo-m
170inicart'); 171 for (var c = 0; c < carts.length; c++) setup(carts[c]); 172 } 173 174 if (document.readyState === 'loading') { 175 document.addEventListener('DOMContentLoaded', function () { init(); }); 176 } else { 177 init(); 178 } 179 180 // Shared engine namespace (never a new top-level global). 181 window.hzo = window.hzo || {}; 182 window.hzo.miniCart = { init: init, setup: setup, recompute: recompute }; 183})();
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.