1/* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 2 MODAL / DIALOG â widget behavior (Phase-2 widget system; moved out of main.js) 3 4 Self-contained, no hard dependencies. As a shortcode widget the modal ALWAYS 5 loads: modal.php un-gates it via the hzostudio_component_markers filter, 6 because [hzo_modal] can render from surfaces the loader's post_content scan 7 can't see. The modal IIFE was removed from main.js so behavior lives here. 8 Focus trap + the shared background inert/scroll lock stay in main.js 9 (hzo.trapFocus / hzo.lockOverlayBackground) â they are cross-overlay 10 primitives shared with the drawer, lightbox, and mobile menu. 11 12 OPTION CONTRACT â every setting is a data-attribute on `.hzo-modal`, so the 13 block pattern (author writes attributes) and the [hzo_modal] shortcode 14 (modal.php generates them from settings) drive the SAME behavior. Every 15 option defaults OFF/legacy â attribute-free markup behaves exactly as before: 16 17 data-hzo-modal-static "true" â backdrop click does NOT close (confirm- 18 style dialogs; the close button/Esc still work) 19 data-hzo-modal-esc "false" â Esc does NOT close (pair with static 20 for a fully explicit-dismiss alertdialog) 21 data-hzo-modal-auto-open ms after init to auto-open (int; 0/absent = off). 22 Fires once per page load; skipped if any overlay 23 is already open at fire time. 24 25 Inside the dialog, [data-hzo-autofocus] marks the element to receive initial 26 focus (default: the close button, then the dialog itself). 27 28 A11y (WAI-ARIA APG Dialog): the panel carries role="dialog" + aria-modal 29 (authored in markup / emitted by the shortcode); focus is trapped while open; 30 the rest of <body> is inert + scroll-locked; Esc closes (unless opted out); 31 focus returns to the trigger on close. Nested dialogs stack via hzo.trapFocus. 32 33 Public API (back-compat â commerce.js and integrations call these): 34 hzo.openModal(idOrEl, trigger?) / hzo.closeModal(idOrEl) 35 hzo.modal = { init, setup, open, close } (init(scope) for AJAX content) 36 âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 37(function () { 38 'use strict'; 39 40 function boolAttr(el, name, def) { 41 var v = el.getAttribute('data-hzo-modal-' + name); 42 if (v === null || v === '') { return def; } 43 return v !== 'false' && v !== '0'; 44 } 45 46 function openModal(modal, trigger) { 47 if (!modal || modal.classList.contains('is-open')) { return; } 48 if (!window.hzo || typeof window.hzo.trapFocus !== 'function') { return; } // primitives (main.js) missing â never half-open 49 modal.classList.add('is-open'); 50 window.hzo.lockOverlayBackground(); // shared lock (total open-overlay count) 51 var dialog = modal.querySelector('.hzo-modal__dialog') || modal; 52 var initial = modal.querySelector('[data-hzo-autofocus]') || 53 modal.querySelector('.hzo-modal__close') || 54 dialog; 55 modal._hzoTrap = window.hzo.trapFocus(dialog, { 56 onEscape: function () { if (boolAttr(modal, 'esc', true)) { closeModal(modal); } }, 57 initialFocus: initial, 58 returnFocus: trigger || document.activeElement 59 }); 60 } 61 62 // G32: remember an auto-open dismissal for the session so a marketing/ 63 // announcement popup never re-nags a user who already closed it. 64 function autoOpenKey(modal) { return 'hzoModalAutoOpen:' + (modal.id || 'anon'); } 65 function autoOpenDismissed(modal) { 66 try { return window.sessionStorage.getItem(autoOpenKey(modal)) === '1'; } catch (e) { return false; } 67 } 68 function markAutoOpenDismissed(modal) { 69 try { window.sessionStorage.setItem(autoOpenKey(modal), '1'); } catch (e) { /* private mode / quota */ } 70 } 71 72 function closeModal(modal) { 73 if (!modal || !modal.classList.contains('is-open')) { return; } 74 modal.classList.remove('is-open'); 75 if (modal._hzoAutoOpen) { markAutoOpenDismissed(modal); } // G32
76 if (window.hzo && window.hzo.unlockOverlayBackground) { window.hzo.unlockOverlayBackground(); } 77 if (modal._hzoTrap) { modal._hzoTrap(); modal._hzoTrap = null; } // release() returns focus to the trigger 78 } 79 80 /* Portal to <body> (so ancestor overflow/transform/z-index can't clip or 81 mis-stack, and the background-inert scan sees clean body children) + wire 82 the modal's own close affordances. Idempotent via the ready guard. */ 83 function setup(modal) { 84 if (!modal || modal.getAttribute('data-hzo-modal-ready') === 'true') { return; } 85 modal.setAttribute('data-hzo-modal-ready', 'true'); 86 87 if (modal.parentNode !== document.body) { document.body.appendChild(modal); } 88 89 // G31: only a backdrop press that ALSO began on the backdrop closes â a 90 // text-selection drag that starts inside the dialog and releases on the 91 // backdrop must not close it. 92 var pressTarget = null; 93 modal.addEventListener('mousedown', function (e) { pressTarget = e.target; }); 94 modal.addEventListener('click', function (e) { 95 if (e.target === modal) { // backdrop click 96 if (pressTarget === modal && !boolAttr(modal, 'static', false)) { closeModal(modal); } 97 pressTarget = null; 98 return; 99 } 100 if (e.target.closest('[data-hzo-modal-close]')) { closeModal(modal); } 101 }); 102 103 // Auto-open (marketing/announcement popups). Once per page load; skipped if 104 // another overlay is already open, or (G32) if the user already dismissed 105 // this popup earlier in the session. 106 var delay = parseInt(modal.getAttribute('data-hzo-modal-auto-open') || '', 10); 107 if (delay > 0 && !autoOpenDismissed(modal)) { 108 modal._hzoAutoOpen = true; 109 setTimeout(function () { 110 if (document.body.classList.contains('hzo-overlay-open')) { return; } 111 if (autoOpenDismissed(modal)) { return; } 112 openModal(modal, null); 113 }, delay); 114 } 115 } 116 117 function init(scope) { 118 var modals = (scope || document).querySelectorAll('.hzo-modal'); 119 for (var m = 0; m < modals.length; m++) { setup(modals[m]); } 120 } 121 122 // Triggers are event-delegated so dynamically-inserted triggers work too. 123 document.addEventListener('click', function (e) { 124 var trigger = e.target.closest('[data-hzo-modal-open]'); 125 if (!trigger) { return; } 126 var modal = document.getElementById(trigger.getAttribute('data-hzo-modal-open')); 127 if (!modal) { return; } 128 e.preventDefault(); 129 setup(modal); // covers modals injected after init (AJAX content) 130 openModal(modal, trigger); 131 }); 132 133 if (document.readyState === 'loading') { 134 document.addEventListener('DOMContentLoaded', function () { init(); }); 135 } else { 136 init(); 137 } 138 139 // Shared engine namespace. openModal/closeModal keep their historical names 140 // (public API used by integrations); the hzo.modal object is the widget- 141 // convention surface for dynamically-injected instances. 142 window.hzo = window.hzo || {}; 143 window.hzo.openModal = function (idOrEl, trigger) { 144 var el = typeof idOrEl === 'string' ? document.getElementById(idOrEl) : idOrEl; 145 if (el) { setup(el); } 146 openModal(el, trigger); 147 }; 148 window.hzo.closeModal = function (idOrEl) { 149 closeModal(typeof idOrEl === 'string' ? document.getElementById(idOrEl) : idOrEl); 150 }; 151 window.hzo.modal = { init: init, setup: setup, open: window.hzo.openModal, close: window.hzo.closeModal }; 152})();
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.