PageSourceSearch

https://www.brotherssupply.com/wp-content/plugins/hozio-studio/com…/readmore/readmore.js?ver=dfba8b8a99c4

js brotherssupply.com collected 2026-10-09 00:31:04 UTC 12,796 bytes, 301 lines download raw bytes

1/* ═════════════════════════════════════════════════════════════════════════
2   READ-MORE CONTENT EXPANDER (W-092) — widget behavior · roadmap #14
3
4   Activates the WAI-ARIA APG Disclosure pattern on a .hzo-readmore block: a
5   real <button> toggle carrying aria-expanded + aria-controls that clamps the
6   full prose to a fade-masked preview and expands it back. The FULL text always
7   stays in the DOM — collapse is CSS overflow + max-block-size + a fade mask
8   (never display:none), so crawlers / AI / find-in-page / AT read all of it.
9
10   FAIL-VISIBLE: adding the .hzo-readmore--ready class is what enables the CSS
11   clamp AND reveals the toggle. Until this JS runs (or if it throws) the block
12   shows the full copy with no orphaned button — a stripped/missed engine is
13   benign, never a permanently-trapped-clamped block.
14
15   OPTION CONTRACT — data-hzo-readmore-* attributes on the .hzo-readmore root
16   (the [hzo_readmore] shortcode and the block pattern write the same attrs):
17
18     data-hzo-readmore-collapsed        int lines | <length>  (default 4 lines)
19     data-hzo-readmore-collapsed-sm     override at <48em
20     data-hzo-readmore-collapsed-md     override at 48-64em
21     data-hzo-readmore-collapsed-lg     override at >=64em
22     data-hzo-readmore-fade             false | <length>      (mask off / depth)
23     data-hzo-readmore-default          expanded              (initial state)
24     data-hzo-readmore-scroll-on-collapse  false              (default on)
25     data-hzo-readmore-deep-link        false                 (default on)
26     data-hzo-readmore-more / -less     translated toggle labels
27
28   A11y: focus stays on the toggle across the state change; the chevron is
29   aria-hidden. Force-expand (instant, no anim) on focusin into clipped copy,
30   on :target / hashchange (deep link), and print is handled in CSS — so
31   keyboard / AT never land on visually-clipped content (PRESERVE-2).
32
33   Public API (shared namespace — never a new top-level global):
34     hzo.readmore = { init, setup }   init(scope?) scans for .hzo-readmore;
35                                      setup(root) wires one block (AJAX content).
36   ═════════════════════════════════════════════════════════════════════════ */
37(function () {
38	'use strict';
39
40	window.hzo = window.hzo || {};
41
42	var mm = window.matchMedia;
43
44	function reduceMotion() {
45		return !!(mm && mm('(prefers-reduced-motion: reduce)').matches);
46	}
47
48	function attr(root, name) {
49		var v = root.getAttribute('data-hzo-readmore-' + name);
50		return (v === null || v === '') ? null : v;
51	}
52
53	/* Translated toggle label — prefers the server-emitted (already localized)
54	   data-attr, falls back to wp.i18n, then to English. */
55	function label(root, which) {
56		var v = attr(root, which);
57		if (v) { return v; }
58		var en = (which === 'more') ? 'Read more' : 'Show less';
59		if (window.wp && wp.i18n && typeof wp.i18n.__ === 'function') {
60			return wp.i18n.__(en, 'hozio-studio');
61		}
62		return en;
63	}
64
65	/* Effective `collapsed` value for the current viewport (per-breakpoint
66	   overrides win over the base). Recomputed on resize. */
67	function effectiveCollapsed(root) {
68		if (mm) {
69			if (mm('(min-width: 64em)').matches && attr(root, 'collapsed-lg')) {
70				return attr(root, 'collapsed-lg');
71			}
72			if (mm('(min-width: 48em)').matches && mm('(max-width: 63.9375em)').matches && attr(root, 'collapsed-md')) {
73				return attr(root, 'collapsed-md');
74			}
75			if (mm('(max-width: 47.9375em)').matches && attr(root, 'collapsed-sm')) {
76				return attr(root, 'collapsed-sm');
77			}
78		}
79		return attr(root, 'collapsed') || '4';
80	}
81
82	/* Resolve a `collapsed` value to a CSS length string for the collapsed
83	   height. Integer → lines * computed line-height (px). A length → verbatim. */
84	function collapsedLength(content, val) {
85		val = String(val).trim();
86		if (/^\d+$/.test(val)) {
87			var cs = getComputedStyle(content);
88			var lh = parseFloat(cs.lineHeight);
89			if (isNaN(lh)) { lh = (parseFloat(cs.fontSize) || 16) * 1.6; }
90			return (parseInt(val, 10) * lh) + 'px';
91		}
92		return val;
93	}
94
95	/* Push the resting collapsed height into the CSS var the clamp rule reads,
96	   so clearing the inline max-block-size after an animation settles onto the
97	   exact same height (no jump). */
98	function syncCollapsedVar(root, content) {
99		root.style.setProperty('--hzo-readmore-max', collapsedLength(content, effectiveCollapsed(root)));
100	}
101
102	/* The collapsed height as a plain px number (for the fits-check). Integer →
103	   lines * line-height; a px/rem/em length → resolved to px. NaN if unparseable
104	   (→ arm anyway, the safe default). */
105	function collapsedPxNum(root, content) {
106		var raw = String(effectiveCollapsed(root)).trim();
107		var cs = getComputedStyle(content);
108		if (/^\d+$/.test(raw)) {
109			var lh = parseFloat(cs.lineHeight);
110			if (isNaN(lh)) { lh = (parseFloat(cs.fontSize) || 16) * 1.6; }
111			return parseInt(raw, 10) * lh;
112		}
113		var m = raw.match(/^([0-9]+(?:\.[0-9]+)?)(px|rem|em)$/);
114		if (!m) { return NaN; }
115		var n = parseFloat(m[1]);
116		if (m[2] === 'px') { return n; }
117		if (m[2] === 'rem') { return n * (parseFloat(getComputedStyle(document.documentElement).fontSize) || 16); }
118		return n * (parseFloat(cs.fontSize) || 16); // em
119	}
120
121	function isCollapsed(root) {
122		return !root.classList.contains('is-expanded');
123	}
124
125	/* ── Expand / collapse. Animates max-block-size between the collapsed height
126	      and the measured content height; reduced-motion skips the animation. ── */
127	function setLabel(root, expanded) {
128		var span = root.querySelector('.hzo-readmore__label');
129		if (span) { span.textContent = expanded ? label(root, 'less') : label(root, 'more'); }
130		var btn = root.querySelector('.hzo-readmore__toggle');
131		if (btn) { btn.setAttribute('aria-expanded', expanded ? 'true' : 'false'); }
132	}
133
134	function afterTransition(content, done) {
135		var fired = false;
136		function handler(e) {
137			if (e && e.propertyName && e.propertyName.indexOf('max') === -1) { return; }
138			if (fired) { return; }
139			fired = true;
140			content.removeEventListener('transitionend', handler);
141			done();
142		}
143		content.addEventListener('transitionend', handler);
144		// Fallback in case transitionend never fires (interrupted, display swap).
145		setTimeout(function () { if (!fired) { fired = true; content.removeEventListener('transitionend', handler); done(); } }, 600);
146	}
147
148	function expand(root) {
149		var content = root.querySelector('.hzo-readmore__content');
150		if (!content || root.classList.contains('is-expanded')) { return; }
151
152		if (reduceMotion()) {
153			root.classList.add('is-expanded');
154			content.style.maxBlockSize = '';
155			setLabel(root, true);
156			return;
157		}
158		// From: current clamped height → To: full content height, then release.
159		content.style.maxBlockSize = content.offsetHeight + 'px';
160		root.classList.add('is-expanded');            // drops the mask immediately
161		setLabel(root, true);
162		// Force reflow so the from-height is committed before the to-height.
163		void content.offsetHeight;
164		content.style.maxBlockSize = content.scrollHeight + 'px';
165		afterTransition(content, function () {
166			if (root.classList.contains('is-expanded')) { content.style.maxBlockSize = ''; }
167		});
168	}
169
170	function collapse(root) {
171		var content = root.querySelector('.hzo-readmore__content');
172		if (!content || !root.classList.contains('is-expanded')) { return; }
173
174		syncCollapsedVar(root, content);
175		var target = collapsedLength(content, effectiveCollapsed(root));
176
177		if (reduceMotion()) {
178			root.classList.remove('is-expanded');
179			content.style.maxBlockSize = '';           // CSS var rule takes over
180			setLabel(root, false);
181			maybeScrollIntoView(root);
182			return;
183		}
184		// From: full height → To: collapsed height, then release to the var rule.
185		content.style.maxBlockSize = content.offsetHeight + 'px';
186		void content.offsetHeight;                     // commit the from-height
187		root.classList.remove('is-expanded');          // restores the mask
188		setLabel(root, false);
189		content.style.maxBlockSize = target;
190		afterTransition(content, function () {
191			if (!root.classList.contains('is-expanded')) { content.style.maxBlockSize = ''; }
192			maybeScrollIntoView(root);
193		});
194	}
195
196	function toggle(root) {
197		if (isCollapsed(root)) { expand(root); } else { collapse(root); }
198	}
199
200	/* On collapse, bring the block top back into view if it scrolled above the
201	   viewport (so the reader isn't stranded mid-page). Off when opted out. */
202	function maybeScrollIntoView(root) {
203		if (attr(root, 'scroll-on-collapse') === 'false') { return; }
204		var rect = root.getBoundingClientRect();
205		if (rect.top >= 0) { return; }                 // top already visible — don't fight the viewport
206		try {
207			root.scrollIntoView({ block: 'start', behavior: reduceMotion() ? 'auto' : 'smooth' });
208		} catch (e) {
209			root.scrollIntoView();
210		}
211	}
212
213	/* Instant expand (no animation) — the fail-visible primitive for focus /
214	   deep-link so a reader is never stranded on clipped copy. */
215	function forceExpand(root) {
216		if (!root || root.classList.contains('is-expanded')) { return; }
217		var content = root.querySelector('.hzo-readmore__content');
218		root.classList.add('is-expanded');
219		if (content) { content.style.maxBlockSize = ''; }
220		setLabel(root, true);
221	}
222
223	function setup(root) {
224		if (!root || !root.classList || root.classList.contains('hzo-readmore--ready')) { return; } // idempotent
225		var content = root.querySelector('.hzo-readmore__content');
226		var btn = root.querySelector('.hzo-readmore__toggle');
227		if (!content || !btn) { return; }
228
229		// Content that already fits the collapsed height needs NO toggle — a
230		// clamp there is a no-op and a toggle a dead control (§States / G22). Leave
231		// the block un-armed (full copy shows, the actions row stays CSS-hidden
231)
232		// and warn so the author knows the clamp does nothing here (§14/G57). The
233		// data-fit flag also makes this outcome idempotent across re-inits.
234		var capPx = collapsedPxNum(root, content);
235		if (!isNaN(capPx) && content.scrollHeight <= capPx + 1) {
236			root.setAttribute('data-hzo-readmore-fit', 'true');
237			if (window.console && console.warn) {
238				console.warn('[hzo_readmore] content fits within collapsed height — no toggle rendered', root);
239			}
240			return;
241		}
242
243		// Prime the resting collapsed height, then arm: the .hzo-readmore--ready
244		// class enables the CSS clamp + reveals the toggle.
245		syncCollapsedVar(root, content);
246		root.classList.add('hzo-readmore--ready');
247		setLabel(root, !isCollapsed(root));
248
249		btn.addEventListener('click', function () { toggle(root); });
250
251		// Recompute the resting collapsed height on resize (per-breakpoint +
252		// reflowed line-height). Only meaningful while collapsed.
253		if ('ResizeObserver' in window) {
254			var ro = new ResizeObserver(function () {
255				if (isCollapsed(root)) { syncCollapsedVar(root, content); }
256			});
257			ro.observe(root);
258		}
259	}
260
261	function init(scope) {
262		var roots = (scope || document).querySelectorAll('.hzo-readmore:not(.hzo-readmore--ready):not([data-hzo-readmore-fit])');
263		for (var i = 0; i < roots.length; i++) { setup(roots[i]); }
264	}
265
266	/* ── Force-expand listeners (content-preserving guarantee) ── */
267
268	// Keyboard / AT lands on a link inside clipped copy → expand its block.
269	document.addEventListener('focusin', function (e) {
270		var content = e.target && e.target.closest ? e.target.closest('.hzo-readmore__content') : null;
271		if (!content) { return; }
272		var root = content.closest('.hzo-readmore');
273		if (root && isCollapsed(root)) { forceExpand(root); }
274	});
275
276	// Deep link: a #hash into the region (or an element inside it) force-expands.
277	function revealHashTarget() {
278		if (!location.hash || location.hash.length < 2) { return; }
279		var t;
280		try { t = document.getElementById(location.hash.slice(1)); } catch (e) { t = null; }
281		if (!t) { return; }
282		var root = t.classList && t.classList.contains('hzo-readmore') ? t : t.closest('.hzo-readmore');
283		if (root && attr(root, 'deep-link') !== 'false' && isCollapsed(root)) { forceExpand(root); }
284	}
285	window.addEventListener('hashchange', revealHashTarget);
286
287	function boot() {
288		try { init(); revealHashTarget(); }
289		catch (e) {
290			if (window.console && console.warn) { console.warn('[hzo_readmore] init failed — content left visible: ' + e); }
291		}
292	}
293	if (document.readyState === 'loading') {
294		document.addEventListener('DOMContentLoaded', boot);
295	} else {
296		boot();
297	}
298
299	// Shared engine namespace (widget convention) — never a new top-level global.
300	window.hzo.readmore = { init: init, setup: setup };
301})();

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.