PageSourceSearch

https://www.brotherssupply.com/wp-content/plugins/hozio-studio/com…/cta-band/cta-band.js?ver=0adba6d3bc7b

js brotherssupply.com collected 2026-10-09 00:30:10 UTC 16,472 bytes, 341 lines download raw bytes

1/* ═════════════════════════════════════════════════════════════════════════
2   CLOSING CTA BAND (W-091) — progressive-enhancement behavior.
3
4   The band is fully functional server-rendered: copy + trust + native <a>
5   actions (or a native <form> capture) work with this file absent. cta-band.js
6   only ATTACHES where the band actually needs runtime, each leg guarded so a
7   failure degrades to the static markup:
8
9     1. VIDEO BG — plays the ambient <video> in view (IntersectionObserver),
10        wires the REQUIRED visible pause control (aria-pressed + visible label
11        stay in sync, WCAG 2.2.2 / 2.5.3), NEVER autoplays under
12        prefers-reduced-motion (poster shows, control still works), pauses
13        offscreen. A user pause always wins over the IO resume.
14
15     2. ASYNC CAPTURE — only when the capture <form> carries
16        data-hzo-cta-band-async (a same-site /wp-json/ route). Synchronous
17        client validation first (invalid -> aria-invalid + focus, no network),
18        then submit-lock + .is-loading + aria-busy set <100ms BEFORE fetch,
19        AbortController 8s timeout, navigator.onLine gate. Success swaps the
20        input-group in place for a confirmation (no navigation), snapshot ->
21        restore focus to the confirmation, and announces via hzo.announce()
22        (body-portaled OUTSIDE the swapped fragment, G30). Every .catch
23        reconciles: re-enable, PRESERVE entered values, specific recoverable
24        message (offline vs timeout vs server). Without the async marker the
25        form is a normal POST (states belong to the destination — G26).
26
27     3. DESKTOP CLICK-TO-COPY — a kind=call action's tel: no-ops on desktop
28        (§13 dead-desktop-CTA): on a fine pointer with clipboard support, the
29        click copies the number + announces "Copied", instead of a dead tel:.
30        Coarse pointers keep the native tel: dial. The number is also visible
31        selectable text regardless.
32
33     4. MOBILE STICKY HANDOFF — mobile="sticky" clones the actions into a
34        .hzo-sticky-bar and hands reveal to the always-loaded sticky-cta.js
35        (data-hzo-sticky-after = the band). Clones tag location:'sticky' so a
36        docked-copy tap is attributed without double-counting the conversion.
37
38     5. MEASUREMENT — via the dataLayer (never gtag directly): phone_call
39        (kind=call), select_content (kind=book|quote|link), generate_lead
40        (async capture success). No PII in the payloads.
41
42   The band CONSUMES hzo:reveal (via .hzo-reveal / W-020) but dispatches no new
43   hzo:* event (an event with zero consumers is a dead event). Idempotent
44   (data-hzo-cta-band-ready). Namespaced on window.hzo.ctaBand.
45   ═════════════════════════════════════════════════════════════════════════ */
46(function () {
47    'use strict';
48
49    window.hzo = window.hzo || {};
50
51    var reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
52
53    function __(s) {
54        return (window.wp && wp.i18n && wp.i18n.__) ? wp.i18n.__(s, 'hozio-studio') : s;
55    }
56    function sprintf(fmt, a) {
57        return (window.wp && wp.i18n && wp.i18n.sprintf) ? wp.i18n.sprintf(fmt, a) : fmt.replace('%s', a);
58    }
59    function announce(msg, level) {
60        if (window.hzo && typeof window.hzo.announce === 'function') {
61            window.hzo.announce(msg, level || 'polite');
62        }
63    }
64    function pushDL(obj) {
65        window.dataLayer = window.dataLayer || [];
66        window.dataLayer.push(obj);
67    }
68
69    /* ─── 1. Video background ────────────────────────────────────────────── */
70    function setupVideo(band) {
71        var video = band.querySelector('[data-hzo-cta-band-video]');
72        var toggle = band.querySelector('[data-hzo-cta-band-video-toggle]');
73        if (!video) { return; }
74        video.muted = true;
75
76        var userPaused = false;
77        var label = toggle ? toggle.querySelector('.hzo-cta-band__video-toggle-label') : null;
78        var playText = __('Play'), pauseText = __('Pause');
79        var playAria = __('Play background video'), pauseAria = __('Pause background video');
80
81        function sync() {
82            if (!toggle) { return; }
83            var paused = video.paused;
84            toggle.setAttribute('aria-pressed', paused ? 'true' : 'false');
85            toggle.setAttribute('aria-label', paused ? playAria : pauseAria);
86            if (label) { label.textContent = paused ? playText : pauseText; }
87        }
88        function tryPlay() {
89            var p = video.play();
90            if (p && typeof p.catch === 'function') { p.catch(function () {}); }
91        }
92
93        if (reduce) {
94            // Never autostart under reduced motion — poster shows, pause reflects
95            // paused; an explicit Play is the user's own choice (not vetoed).
96            video.autoplay = false;
97            video.removeAttribute('autoplay');
98            try { video.pause(); } catch (e) {}
99        } else {
100            tryPlay();
101        }
102
103        if (toggle) {
104            toggle.addEventListener('click', function () {
105                if (video.paused) { userPaused = false; tryPlay(); }
106                else { userPaused = true; video.pause(); }
107            });
108            video.addEventListener('play', sync);
109            video.addEventListener('pause', sync);
110            sync();
111        }
112
113        if ('IntersectionObserver' in window) {
114            new IntersectionObserver(function (entries) {
115                var visible = entries[entries.length - 1].isIntersecting;
116                if (!visible) {
117                    if (!video.paused) { video.pause(); }
118                } else if (!userPaused && !reduce) {
119                    tryPlay();
120                }
121            }).observe(band);
122        }
123    }
124
125    /* ─── 2. Async inline capture ────────────────────────────────────────── */
126    function validEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }
127    function validPhone(v) { return v.replace(/\D/g, '').length >= 10; }
128
129    function setupCapture(band) {
130        var form = band.querySelector('form[data-hzo-cta-band-async]');
131        if (!form) { return; } // non-async capture is a plain native POST — nothing to do
132        var url = form.getAttribute('data-hzo-cta-band-async');
133        var method = form.getAttribute('data-hzo-cta-band-method') || 'email';
134        var input = form.querySelector('input');
135        var submit = form.querySelector('.hzo-cta-band__submit');
136        var status = form.querySelector('[data-hzo-cta-band-status]');
137        if (!url || !input || !submit) { return; }
138
139        function showStatus(msg, isError) {
140            if (!status) { return; }
141            status.textContent = msg;
142            status.hidden = false;
143            status.classList.toggle('is-error', !!isError);
144        }
145        function lock() {
146            submit.classList.add('is-loading');
147            submit.setAttribute('aria-busy', 'true');
148            submit.disabled = true;            // holds focus through the lock (no handoff)
149        }
150        function unlock() {
151            submit.classList.remove('is-loading');
152            submit.setAttribute('aria-busy', 'false');
153            submit.disabled = false;
154        }
155
156        form.addEventListener('submit', function (e) {
157            e.preventDefault();
158            if (submit.disabled) { return; }   // spam guard: one in-flight request
159
160            var value = input.value.replace(/^\s+|\s+$/g, '');
161            var ok = (method === 'phone') ? validPhone(value) : validEmail(value);
162            if (!ok) {
163                input.setAttribute('aria-invalid', 'true');
164                var vmsg = (method === 'phone')
165                    ? __('Please enter a valid phone number.')
166                    : __('Please enter a valid email address.');
167                showStatus(vmsg, true);
168                announce(vmsg, 'assertive');
169                input.focus();
170                return;
171            }
172            input.removeAttribute('aria-invalid');
173
174            if (window.navigator && navigator.onLine === false) {
175                var offmsg = __('You appear to be offline — reconnect and try again.');
176                showStatus(offmsg, true);
177                announce(offmsg, 'assertive');
178                return;
179            }
180
181            lock();
182            status && (status.hidden = true);
183
184            var controller = ('AbortController' in window) ? new AbortController() : null;
185            var timedOut = false;
186            var timer = window.setTimeout(function () {
187                timedOut = true;
188                if (controller) { controller.abort(); }
189            }, 8000);
190
191            var payload = {};
192            payload[method] = value;
193
194            var opts = {
195                method: 'POST',
196                headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
197                credentials: 'same-origin',
198                body: JSON.stringify(payload)
199            };
200            if (controller) { opts.signal = controller.signal; }
201            if (window.wpApiSettings && wpApiSettings.nonce) {
202                opts.headers['X-WP-Nonce'] = wpApiSettings.nonce;
203            }
204
205            window.fetch(url, opts).then(function (res) {
206                if (!res.ok) { throw new Error('http ' + res.status); }
207                return res;
208            }).then(function () {
209                window.clearTimeout(timer);
210                complete();
211            }).catch(function () {
212                window.clearTimeout(timer);
213                unlock();                       // re-enable; PRESERVE entered value (never cleared)
214                var msg = timedOut
215                    ? __('That took too long — please try again.')
216                    : ((window.navigator && navigator.onLine === false)
217                        ? __('You appear to be offline — reconnect and try again.')
218                        : __('Couldn’t send that — check your connection and try again.'));
219                showStatus(msg, true);
220                announce(msg, 'assertive');
221            });
222        });
223
224        function complete() {
225            var group = form.querySelector('.hzo-input-group');
226            var msg = __('Thanks — we’ll be in touch shortly.');
227            var confirm = document.createElement('p');
228            confirm.className = 'hzo-cta-band__confirm';
229            confirm.setAttribute('tabindex', '-1');
230            confirm.textContent = msg;
231            if (group) { form.replaceChild(confirm, group); }
232            else { form.appendChild(confirm); }
233            if (status) { status.hidden = true; }
234            // Snapshot -> restore focus into the confirmation (PRESERVE-2). The
235            // announce region is body-portaled OUTSIDE this fragment (G30).
236            try { confirm.focus(); } catch (e) {}
237            announce(msg, 'polite');
238            pushDL({ event: 'generate_lead', form: 'cta_band', method: method });
239        }
240    }
241
242    /* ─── 3. Desktop click-to-copy for kind=call ─────────────────────────── */
243    var finePointer = !!(window.matchMedia && window.matchMedia('(pointer: fine)').matches);
244
245    function bindCopy(band) {
246        if (!finePointer || !(navigator.clipboard && navigator.clipboard.writeText)) { return; }
247        var calls = band.querySelectorAll('a[data-hzo-cta-kind="call"][data-hzo-cta-copy]');
248        for (var i = 0; i < calls.length; i++) {
249            (function (a) {
250                a.addEventListener('click', function (e) {
251                    // Desktop tel: is a dead click — copy the number instead.
252                    e.preventDefault();
253                    var num = a.getAttribute('data-hzo-cta-copy') || '';
254                    navigator.clipboard.writeText(num).then(function () {
255                        var msg = sprintf(__('Copied %s'), num);
256                        announce(msg, 'polite');
257                        if (window.hzo && typeof window.hzo.toast === 'function') {
258                            window.hzo.toast(msg, { type: 'success' });
259                        }
260                    }).catch(function () {});
261                });
262            })(calls[i]);
263        }
264    }
265
266    /* ─── 4. Mobile sticky handoff ───────────────────────────────────────── */
267    function setupSticky(band) {
268        if (band.getAttribute('data-hzo-cta-band-mobile') !== 'sticky') { return; }
269        var actions = band.querySelector('.hzo-cta-band__actions');
270        if (!actions) { return; }
271        if (document.querySelector('[data-hzo-sticky-bar][data-hzo-cta-band-clone
271="' + band.id + '"]')) { return; }
272
273        var bar = document.createElement('div');
274        bar.className = 'hzo-sticky-bar';
275        bar.setAttribute('data-hzo-sticky-bar', '');
276        bar.setAttribute('data-hzo-cta-band-clone', band.id);
277        bar.setAttribute('data-hzo-sticky-after', '#' + band.id);
278        bar.setAttribute('data-hzo-sticky-cta-until', '#' + band.id);
279        bar.setAttribute('role', 'region');
280        bar.setAttribute('aria-label', __('Quick contact actions'));
281
282        var links = actions.querySelectorAll('.hzo-cta-band__action');
283        for (var i = 0; i < links.length; i++) {
284            var clone = links[i].cloneNode(true);
285            clone.setAttribute('data-hzo-cta-location', 'sticky'); // dedupe attribution
286            // The clones ride a LIGHT sticky bar, not the dark band — drop the
287            // on-dark button styling and the large size so they read correctly.
288            clone.classList.remove('hzo-btn--on-dark', 'hzo-btn--lg');
289            bar.appendChild(clone);
290        }
291        document.body.appendChild(bar);
292
293        // Hand reveal to the always-loaded sticky-cta.js (guarded — never a hard dep).
294        if (window.hzo && window.hzo.stickyCta && typeof window.hzo.stickyCta.init === 'function') {
295            window.hzo.stickyCta.init(document);
296        }
297    }
298
299    /* ─── 5. Measurement (delegated, bound once) ─────────────────────────── */
300    function bindMeasurement() {
301        if (window.hzo.__ctaBandMeasureBound) { return; }
302        window.hzo.__ctaBandMeasureBound = true;
303        document.addEventListener('click', function (e) {
304            var a = e.target && e.target.closest ? e.target.closest('.hzo-cta-band__action') : null;
305            if (!a) { return; }
306            var kind = a.getAttribute('data-hzo-cta-kind') || 'link';
307            var location = a.getAttribute('data-hzo-cta-location') || 'band';
308            if (kind === 'call') {
309                pushDL({ event: 'phone_call', source: 'cta_band', location: location });
310            } else {
311                pushDL({ event: 'select_content', content_type: 'cta_band', item_id: kind, location: location });
312            }
313        }, true);
314    }
315
316    /* ─── Init (idempotent) ──────────────────────────────────────────────── */
317    function setup(band) {
318        if (!band || band.getAttribute('data-hzo-cta-band-ready') === 'true') { return; }
319        band.setAttribute('data-hzo-cta-band-ready', 'true');
320        if (!band.id) { band.id = 'hzo-cta-band-' + Math.random().toString(36).slice(2, 10); }
321        setupVideo(band);
322        setupCapture(band);
323        bindCopy(band);
324        setupSticky(band);
325    }
326
327    function init(scope) {
328        var bands = (scope || document).querySelectorAll('[data-hzo-cta-band]');
329        for (var i = 0; i < bands.length; i++) { setup(bands[i]); }
330        bindMeasurement();
331    }
332
333    if (document.readyState === 'loading') {
334        document.addEventListener('DOMContentLoaded', function () { init(); });
335    } else {
336        init();
337    }
338
339    // Shared engine namespace — never a new top-level global.
340    window.hzo.ctaBand = { init: init, setup: setup };
341})();

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.