PageSourceSearch

https://go-rooterplumbing.com/wp-content/plugins/hozio-studio/comp…/section-intro.js?ver=02f98b78091b

js go-rooterplumbing.com collected 2026-10-05 08:15:20 UTC 9,455 bytes, 185 lines download raw bytes

1/* ═══════════════════════════════════════════════════════════════════════════
2   SECTION INTRO HEADER (W-090) — progressive-enhancement behavior.
3
4   The eyebrow / heading / text are static server-rendered structural content and
5   need NO JS. This file adds only the three interactive faces, each guarded so a
6   load failure degrades gracefully and a page with no intro is a pure no-op:
7
8     1. FILTERS — a toggle-button GROUP (role=group, aria-pressed) that dispatches
9        the lightweight `hzo:filter { value, target }` CustomEvent (bubbling from
10        the group root) to a REMOTE grid named by data-hzo-intro-filter. Radio-like
11        single-select. If the target selector matches NOTHING on the page the group
12        is disabled (a false affordance suppressed, not a dead control — G11). The
13        intro NEVER touches the grid's DOM (event-based decoupling); the grid owns
14        results + its own live-region announcement (LIVE-REGION-OUTSIDE-FRAGMENT).
15
16     2. TABS — the APG tablist a11y (roving tabindex, Arrow/Home/End, activation,
17        `hzo:tabchange`) is DELEGATED to the always-loaded tabs.js (the tablist is
18        wrapped in .hzo-tabs). tabs.js only toggles panels INSIDE its own root; a
19        section intro's tabpanels live REMOTELY (elsewhere on the page), so this
20        file syncs those panels' [hidden] on init + on every hzo:tabchange.
21
22     3. MEASUREMENT — a delegated tel: mirror (a `tel:` CTA tap → dataLayer
23        phone_call; desktop tel: no-ops silently, so the number is ALSO the
24        anchor's selectable link text), plus select_content on filter/tab change.
25        Bound once; a strict no-op when window.dataLayer is never created.
26
27   This file is idempotent (roots guarded by data-hzo-section-intro-ready), needs
28   no dependencies, and scripts NO motion (state changes are instant; the CSS
29   transitions die under prefers-reduced-motion). Exposed on the shared namespace
30   for AJAX-injected instances.
31   ═══════════════════════════════════════════════════════════════════════════ */
32(function () {
33    'use strict';
34
35    window.hzo = window.hzo || {};
36
37    function pushDataLayer(payload) {
38        window.dataLayer = window.dataLayer || [];
39        window.dataLayer.push(payload);
40    }
41
42    // ── Filters ────────────────────────────────────────────────────────────────
43    // Select a chip (radio-like), then dispatch hzo:filter to the remote grid.
44    function selectChip(group, chip) {
45        var chips = group.querySelectorAll('.hzo-section-intro__chip');
46        for (var i = 0; i < chips.length; i++) {
47            var on = chips[i] === chip;
48            chips[i].setAttribute('aria-pressed', on ? 'true' : 'false');
49            chips[i].classList.toggle('is-active', on);
50        }
51        var value  = chip.getAttribute('data-hzo-intro-value') || '';
52        var target = group.getAttribute('data-hzo-intro-filter') || '';
53
54        // Emit the lightweight W-090 signal — the grid self-scopes by detail.target.
55        group.dispatchEvent(new CustomEvent('hzo:filter', {
56            bubbles: true,
57            detail: { value: value, target: target }
58        }));
59
60        // Measurement (§13): filter selection mirrors select_content.
61        pushDataLayer({ event: 'select_content', content_type: 'section_filter', item_id: value });
62    }
63
64    function setupFilterGroup(group) {
65        if (group.getAttribute('data-hzo-intro-filter-ready') === 'true') { return; }
66        group.setAttribute('data-hzo-intro-filter-ready', 'true');
67
68        // Verify the target exists — a selector that matches nothing is a false
69        // affordance: disable the group rather than ship a dead control (G11).
70        var target = group.getAttribute('data-hzo-intro-filter') || '';
71        var exists = false;
72        try { exists = !!(target && document.querySelector(target)); } catch (e) { exists = false; }
73        if (!exists) {
74            group.setAttribute('aria-disabled', 'true');
75            if (window.console && console.warn) {
76                console.warn('[hzo_section_intro] filter-target "' + target + '" matches no element on the page — chips disabled.');
77            }
78        }
79    }
80
81    // ── Tabs (remote-panel visibility; tabs.js owns the tablist a11y) ───────────
82    function panelById(id) {
83        if (!id) { return null; }
84        return document.getElementById(id);
85    }
86
87    // Show the panel for the selected tab; hide the panels of its siblings.
88    function syncTabPanels(tablist, selectedControls) {
89        var tabs = tablist.querySelectorAll('[role="tab"]');
90        for (var i = 0; i < tabs.length; i++) {
91            var controls = tabs[i].getAttribute('aria-controls');
92            var panel = panelById(controls);
93            if (panel) { panel.hidden = (controls !== selectedControls); }
94        }
95    }
96
97    function currentSelected(tablist) {
98        var tabs = tablist.querySelectorAll('[role="tab"]');
99        for (var i = 0; i < tabs.length; i++) {
100            if (tabs[i].getAttribute('aria-selected') === 'true') {
101                return tabs[i].getAttribute('aria-controls');
102            }
103        }
104        return tabs.length ? tabs[0].getAttribute('aria-controls') : '';
105    }
106
107    function setupTablist(tablist) {
108        if (tablist.getAttribute('data-hzo-intro-tabs-ready') === 'true') { return; }
109        tablist.setAttribute('data-hzo-intro-tabs-ready', 'true');
110        // Initial remote-panel sync from the server-selected tab (tabs.js fires no
111        // event on its init pass, so we do the first sync ourselves).
112        syncTabPanels(tablist, currentSelected(tablist));
113    }
114
115    // ── Delegated document listeners (bound ONCE; dynamically-injected intros work
116    //    with no re-init and double-binding is impossible) ──────────────────────
117    function bindDelegates() {
118        if (window.hzo.__introBound) { return; }
119        window.hzo.__introBound = true;
120
121        // Filter chip tap.
122        document.addEventListener('click', function (e) {
123            var chip = e.target && e.target.closest
124                ? e.target.closest('.hzo-section-intro__actions--filters .hzo-section-intro__chip')
125                : null;
126            if (!chip) { return; }
127            var group = chip.closest('[data-hzo-intro-filter]');
128            if (!group || group.getAttribute('aria-disabled') === 'true') { return; }
129            selectChip(group, chip);
130        });
131
132        // tel: CTA tap → dataLayer phone_call (measurement only; the number is also
133        // the anchor's selectable text, so this is not the desktop fallback).
134        document.addEventListener('click', function (e) {
135            var a = e.target && e.target.closest
136                ? e.target.closest('[data-hzo-section-intro] a[href^="tel:"]')
137                : null;
138            if (!a) { return; }
139            pushDataLayer({
140                event: 'phone_call',
141                phone_number: (a.getAttribute('href') || '').replace(/^tel:/, ''),
142                source: 'section_intro'
143            });
144        });
145
146        // hzo:tabchange (delegated from tabs.js's .hzo-tabs root) → sync remote
147        // panels + mirror select_content. Only handle intros (our tablist inside).
148        document.addEventListener('hzo:tabchange', function (e) {
149            var root = e.target;
150            if (!root || !root.querySelector) { return; }
151            var tablist = root.querySelector('.hzo-section-intro__actions--tabs');
152            if (!tablist) { return; }
153            var id = e.detail && e.detail.id ? e.detail.id : '';
154            syncTabPanels(tablist, id);
155            pushDataLayer({ event: 'select_content', content_type: 'section_tab', item_id: id });
156        });
157    }
158
159    // ── Init (idempotent) ───────────────────────────────────────────────────────
160    function setup(root) {
161        if (!root || root.getAttribute('data-hzo-section-intro-ready') === 'true') { return; }
162        root.setAttribute('data-hzo-section-intro-ready', 'true');
163
164        var groups = root.querySelectorAll('[data-hzo-intro-filter]');
165        for (var g = 0; g < groups.length; g++) { setupFilterGroup(groups[g]); }
166
167        var tablists = root.querySelectorAll('.hzo-section-intro__actions--tabs');
168        for (var t = 0; t < tablists.length; t++) { setupTablist(tablists[t]); }
169    }
170
171    function init(scope) {
172        bindDelegates();
173        var roots = (scope || document).querySelectorAll('[data-hzo-section-intro]:not
173([data-hzo-section-intro-ready])');
174        for (var i = 0; i < roots.length; i++) { setup(roots[i]); }
175    }
176
177    // Shared engine namespace (never a new top-level global).
178    window.hzo.sectionIntro = { init: init, setup: setup };
179
180    if (document.readyState === 'loading') {
181        document.addEventListener('DOMContentLoaded', function () { init(); });
182    } else {
183        init();
184    }
185})();

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.