PageSourceSearch

https://www.actionbullet.com/wp-content/plugins/hozio-studio/components/modal/modal.js?ver=c290c0223d4d

js actionbullet.com collected 2026-10-08 02:30:55 UTC 8,177 bytes, 152 lines download raw bytes

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.