PageSourceSearch

https://www.hubertvestertoyota.com/app/themes/dv-framework/media/js/da-flyout.js?v=1.791.267

js hubertvestertoyota.com collected 2026-10-09 03:19:02 UTC 26,716 bytes, 618 lines download raw bytes

1/**
2 * @DealerAlchemist Flyout / Dropdown — DAFlyOut, DADropdown
3 *
4 * Expandable panels inside the pricing stacks Nexus compiles into `pricing_html`.
5 *
6 *   DAFlyOut   — an info icon on a price row, opening a small panel (a disclaimer, or a
7 *                per-program breakdown). Replaces DVPopUp for pricing disclaimers: DVPopUp is a
8 *                page-level modal, and a full-screen takeover to read twelve words of MSRP
9 *                disclaimer was never the right shape.
10 *   DADropdown — a full-width labelled bar that expands a list, e.g. "Up To $3,500 In Available
11 *                Incentives". Replaces the hand-written `ag-conditional-toggle` block.
12 *
13 * PORT. The canonical implementation lives in the Nexus repo at
14 * `libs/ui-kit/src/templates/srp/lib/daFlyout.ts` and is covered by `daFlyout.test.ts`. Keep the
15 * two in step: both are driven by the same hand-authored markup, and that markup is the contract
16 * between them. The fixtures in daFlyout.test.ts are the reference to copy from.
17 *
18 * WHY THIS EXISTS. Pricing stacks used to carry ~250 lines of inline `<script>` and `<style>` per
19 * dealer, per condition, pasted into an HTML box, purely to make these panels expand. Every copy
20 * drifted. This is that behaviour written once, called inline the way DVPopUp(this) already is:
21 *
22 *     <i class="da-flyout-trigger" onclick="DAFlyOut(this); return false;"></i>
23 *     <button class="da-dropdown-toggle" onclick="DADropdown(this); return false;">
24 *
25 * Loaded globally from header.php beside da-modal.js, so legacy SRP, studio SRP and the VDP all
26 * get it without each template asking.
27 *
28 * PANELS OVERLAY, THEY DO NOT GROW THE CARD.
29 *
30 * These panels open ON TOP of the page. The card keeps its height, nothing reflows, and the panel
31 * is free to be taller than the space under the row.
32 *
33 * Getting there needs more than `position:absolute; z-index:999` — that is what the legacy inline
34 * scripts did, and it is why the conditional-incentives bar rendered badly. `.srp-card-face`
35 * (srp-studio.css) sets `transform: rotateY(0deg)` and `overflow:hidden` for the flip animation,
36 * and a transformed ancestor does three things at once:
37 *
38 *   1. becomes the containing block for absolutely-positioned descendants — so `absolute` resolves
39 *      against the card, not the page,
40 *   2. becomes the containing block for `position:fixed` descendants too — so `fixed` does not
41 *      escape either,
42 *   3. opens a new stacking context — so no `z-index`, however large, can lift the panel above
43 *      anything outside the card,
44 *
45 * and `overflow:hidden` then clips whatever still hangs outside. No amount of CSS on the panel
46 * fixes that while the panel remains a descendant of the card.
47 *
48 * So the panel LEAVES the card while it is open: on open it is moved to `document.body` (a comment
49 * node holds its place) and positioned against its row with `position:fixed` coordinates taken from
50 * `getBoundingClientRect()`; on close it is put back. Where the browser supports the Popover API the
51 * panel is also promoted to the top layer, which puts it above every stacking context on the page
52 * for free. Both paths are re-measured on scroll and resize so the panel stays tethered to its row.
53 *
54 * Because an open panel is no longer inside its wrapper, `wrapper.querySelector(panel)` cannot find
55 * it — open panels are tracked in `openPanels` instead, and that registry is the lookup while open.
56 *
57 * PANELS NEST. A flyout row can sit inside a dropdown panel ("See Pricing Details" listing each
58 * line with its own (i)). The dropdown is then the flyout's parent: opening or clicking in the
59 * flyout keeps it open, closing it closes the flyout first, and Escape peels off the innermost
60 * panel. Parentage is `parent.panel.contains(child.wrapper)`, which still holds once both are in body.
61 *
62 * Styles are injected from here rather than enqueued separately: one file cannot go missing while
63 * the other loads, so a stack can never render half-styled.
64 */
65(function (window, document) {
66	"use strict";
67
68	/* Exported BEFORE the re-entry guard, not after it.
69	**
70	** These are the entire public surface: the markup calls them inline as `onclick="DAFlyOut(this)"`,
71	** so a page where the guard fired but the assignment never ran is a page where every trigger
72	** throws "DAFlyOut is not defined". Assigning first means a second copy of this file bails out
73	** of re-registering listeners while still leaving working globals behind, whichever copy won.
74	** Function declarations hoist, so all three are defined at this point. */
75	window.DAFlyOut = DAFlyOut;
76	window.DADropdown = DADropdown;
77	window.DAFlyOutCloseAll = closeAllPanels;
78
79	if (window.daFlyoutInitialized) return;
80	window.daFlyoutInitialized = true;
81
82	/* Duration of the open/close transition, kept in step with the injected CSS. */
83	var ANIMATION_MS = 250;
84
85	var STYLE_ELEMENT_ID = "da-flyout-styles";
86
87	/* Gap between the row and its panel, and the minimum breathing room against the viewport edge. */
88	var PANEL_GAP = 8;
89	var VIEWPORT_MARGIN = 8;
90	var MIN_PANEL_WIDTH = 220;
91	var MAX_PANEL_WIDTH = 380;
92
93	var OVERLAY_CLASS = "da-panel-overlay";
94	var VISIBLE_CLASS = "da-panel-visible";
95
96	/*
97	** `anchor` names the element the panel is measured against: the whole price row for a flyout,
98	** the toggle button for a dropdown, so each panel lines up with something the width of itself.
99	*/
100	var FLYOUT = {
101		wrapper: "[data-da-flyout]",
102		panel: "[data-da-flyout-panel]",
103		trigger: ".da-flyout-trigger",
104		openClass: "da-flyout-open",
105		anchor: "wrapper"
106	};
107
108	var DROPDOWN = {
109		wrapper: "[data-da-dropdown]",
110		panel: "[data-da-dropdown-panel]",
111		trigger: ".da-dropdown-toggle",
112		openClass: "da-dropdown-open",
113		anchor: "trigger"
114	};
115
116	var KINDS = [FLYOUT, DROPDOWN];
117
118	/* Every panel currently detached into document.body, with what it needs to get home again. */
119	var openPanels = [];
120
121	var supportsPopover = typeof HTMLElement !== "undefined"
122		&& typeof HTMLElement.prototype.showPopover === "function";
123
124	var STYLES = [
125		/* ── Flyout: an info icon on a price row ─────────────────────── */
126		/* A trigger is authored as a bare <i> and gets its glyph from icon-font classes
127		   ("fad fa-info-circle da-flyout-trigger"). Templates that omit those classes leave an
128		   EMPTY inline element: 0x0, invisible, and impossible to click - the panel markup is
129		   perfect and the flyout simply looks broken. So the trigger carries its own box, and
130		   draws a fallback badge when no icon font is going to fill it. The :not() keeps every
131		   template that does ship fa-* classes pixel-identical to before. */
132		".da-flyout-trigger { cursor: pointer; margin-left: 4px; color: inherit; }",
133		/* Everything below is scoped to the no-icon-font case so a trigger that DOES carry
134		   fa-* classes keeps Font Awesome's own box untouched - no width/height/line-height
135		   imposed on a glyph that sizes itself. */
136		".da-flyout-trigger:not([class*=\"fa-\"]) {",
137		"\tposition: relative; display: inline-block; box-sizing: border-box;",
138		"\twidth: 1em; height: 1em; line-height: 1; vertical-align: -.125em;",
139		"\tborder: 1px solid currentColor; border-radius: 50%;",
140		"\topacity: .65; font-style: normal;",
141		"}",
142		".da-flyout-trigger:not([class*=\"fa-\"])::before {",
143		"\tcontent: \"i\"; position: absolute; inset: 0;",
144		"\tdisplay: flex; align-items: center; justify-content: center;",
145		"\tfont-size: .7em; font-weight: 700; font-family: Georgia, \"Times New Roman\", serif;",
146		"}",
147		".da-flyout-trigger:not([class*=\"fa-\"]):hover { opacity: 1; }",
148		".da-flyout-trigger:focus-visible,",
149		".da-dropdown-toggle:focus-visible { outline: 2px solid rgba(0,0,0,.18); outline-offset: 2px; border-radius: 2px; }",
150
151		/* Closed is the only in-flow state. Panels are authored with `hidden`, so with neither CSS
152		   nor JS they stay collapsed rather than dumping every disclaimer into the card. Keyed on
153		   [hidden] rather than the class alone so it cannot tie with the overlay rule below. */
154		".da-flyout-panel[hidden], .da-dropdown-panel[hidden] { display: none; }",
155
156		/* ── The open panel, detached into document.body ─────────────
157		   `inset`, `margin`, `width`, `height` and `padding` are reset explicitly: the UA stylesheet
158		   for [popover] centres it with `inset:0; margin:auto` and adds its own border and padding,
159		   which would fight the coordinates set in JS. */
160		"." + OVERLAY_CLASS + " {",
161		"	display: block; position: fixed; inset: auto; margin: 0; padding: 0;",
162		"	width: auto; height: auto; max-height: 60vh; overflow-y: auto; overscroll-behavior: contain;",
163		"	box-sizing: border-box; text-align: left;",
164		"	background: #fff; border: 1px solid rgba(0,0,0,.10); border-radius: 10px;",
165		"	box-shadow: 0 12px 32px rgba(0,0,0,.18);",
166		/* Only needed on the fallback path; a top-layer popover is already above everything. */
167		"	z-index: 2147483000;",
168		"	opacity: 0; transform: translateY(4px);",
169		"	transition: opacity " + ANIMATION_MS + "ms ease, transform " + ANIMATION_MS + "ms ease;",
170		"}",
171		"." + OVERLAY_CLASS + "." + VISIBLE_CLASS + " { opacity: 1; transform: translateY(0); }",
172		"." + OVERLAY_CLASS + "::backdrop { background: transparent; }",
173
174		".da-flyout-panel-inner, .da-dropdown-panel-inner { padding: 10px 12px; }",
175		".da-flyout-note { margin: 0; font-size: .75rem; line-height: 1.35; color: #555; font-weight: 400; }",
176
177		/* ── Dropdown bar ────────────────────────────────────────────── */
178		".da-dropdown { width: 100%; position: relative; }",
179		".da-dropdown-toggle {",
180		"	display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 6px;",
181		"	padding: 7px 8px; border: none; border-radius: 6px; cursor: pointer; text-align: left;",
182		"	box-sizing: border-box; font-size: 13px; font-weight: 400; line-height: 1.2;",
183		"	transition: filter " + ANIMATION_MS + "ms ease;",
184		"}",
185		".da-dropdown-toggle:hover { filter: brightness(.95); }",
186		".da-dropdown-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
187		".da-dropdown-chevron {",
188		"	width: 6px; height: 6px; flex-shrink: 0;",
189		"	border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;",
190		"	transform: rotate(45deg); transition: transform " + ANIMATION_MS + "ms ease;",
191		"}",
192		".da-dropdown-open .da-dropdown-chevron { transform: rotate(-135deg); }",
193
194		/* ── Details variant: the "See Pricing Details" card bar ────────
195		   Opt in with `da-dropdown da-dropdown--details`. The look of the hand-rolled
196		   ag-conditional-toggle stacks: a light bar with a label over a meta line, a VIEW hint, and
197		   a frosted panel with dashed dividers. The panel rules match both on the wrapper and on the
198		   detached panel, which carries the variant class while it is in body. */
199		".da-dropdown--details .da-dropdown-toggle {",
200		"	gap: 12px; padding: 10px 12px; border: 1px solid rgba(0,0,0,.08); border-radius: 0;",
201		"	background: linear-gradient(180deg, #ffffff 0%, #fafafa 100%); color: #222; font-size: 12px;",
202		"	transition: background " + ANIMATION_MS + "ms ease, border-color " + ANIMATION_MS + "ms ease, box-shadow " + ANIMATION_MS + "ms ease;",
203		"}",
204		".da-dropdown--details .da-dropdown-toggle:hover {",
205		"	filter: none; background: #f7f7f7; border-color: rgba(0,0,0,.14); box-shadow: 0 4px 12px rgba(0,0,0,.06);",
206		"}",
207		".da-dropdown-copy { min-width: 0; display: flex; flex-direction: column; gap: 2px; }",
208		".da-dropdown--details .da-dropdown-label {",
209		"	font-size: 12px; font-weight: 700; color: #222; letter-spacing: .01em; white-space: normal;",
210		"}",
211		".da-dropdown-meta { display: block; font-size: 10px; line-height: 1.25; color: #666; }",
212		".da-dropdown-hint {",
213		"	flex-shrink: 0; margin-left: auto; white-space: nowrap;",
214		"	font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: #666;",
215		"}",
216		".da-dropdown-open .da-dropdown-hint { color: #222; }",
217		".da-dropdown--details .da-dropdown-chevron { width: 7px; height: 7px; border-color: #333; }",
218		"." + OVERLAY_CLASS + ".da-dropdown--details {",
219		"	background: rgba(255,255,255,.92); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);",
220		"}",
221		".da-dropdown--details .da-dropdown-panel-inner { padding: 12px 14px; }",
222		".da-dropdown--details .da-dropdown-panel-inner > * { padding: 8px 0; border-bottom: 1px dashed #d7d7d7; }",
223		".da-dropdown--details .da-dropdown-panel-inner > :last-child { border-bottom: none; }",
224		/* Rows are authored with an inline margin-bottom for the card; the dividers space them here. */
225		".da-dropdown--details .da-price-row { margin-bottom: 0 !important; }",
226		"@media (max-width: 480px) {",
227		"	.da-dropdown--details .da-dropdown-toggle { padding: 11px 12px; }",
228		"	.da-dropdown--details .da-dropdown-label { font-size: 11px; }",
229		"	.da-dropdown--details .da-dropdown-meta { font-size: 9px; }",
230		"	.da-dropdown--details .da-dropdown-hint { display: none; }",
231		"}",
232
233		/* ── Panel line items, shared by both ────────────────────────── */
234		".da-flyout-item, .da-dropdown-item {",
235		"	display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: start;",
236		"	column-gap: 10px; padding: 8px 0; line-height: 1.25;",
237		"}",
238		".da-flyout-item:not(:last-child),",
239		".da-dropdown-item:not(:last-child) { border-bottom: 1px dashed #d7d7d7; }",
240		".da-flyout-item-title, .da-dropdown-item-title {",
241		"	display: block; font-size: .8rem; font-weight: 700; color: #222;",
242		"}",
243		".da-flyout-item-disclaimer, .da-dropdown-item-disclaimer {",
244		"	display: block; margin-top: 3px; font-size: .625rem; line-height: 1.25; color: #666;",
245		"	font-weight: 400; white-space: normal; overflow-wrap: anywhere;",
246		"}",
247		".da-flyout-item-value, .da-dropdown-item-value {",
248		"	font-size: .8rem; font-weight: 700; white-space: nowrap; align-self: start;",
249		"}",
250		".da-flyout-empty, .da-dropdown-empty { padding: 8px 0; font-size: .75rem; color: #666; }",
251
252		/* Legacy themes set .dv-price-stack p { height: 23px } and size every descendant span, which
253		   squashes panel contents. Mostly moot now the open panel sits in body and those descendant
254		   selectors no longer match it, but kept so a panel still reads correctly before it opens. */
255		".da-flyout-panel p, .da-dropdown-panel p { height: auto !important; min-height: 0; }",
256		".da-flyout-panel span, .da-dropdown-panel span { font-size: inherit; }",
257
258		/* ── Price slash ─────────────────────────────────────────────
259		   The tapered red diagonal that used to be media/img/slash-1.png. Drawn in CSS because
260		   that src is a theme-absolute path: it 404s in the Nexus dashboard studio and the React
261		   client, and its positioning lives in theme CSS neither of them loads, so it rendered
262		   full-size and shoved the row apart. The gradient reproduces the tapered ends. */
263		".da-price-slash { position: relative; }",
264		".da-price-slash::after {",
265		"	content: \"\"; position: absolute; left: -6%; right: -6%; top: 50%; height: 2px;",
266		"	background: linear-gradient(to right, transparent, #E8112D 12%, #E8112D 88%, transparent);",
267		"	transform: translateY(-50%) rotate(-11deg); pointer-events: none;",
268		"}",
269
270		/* ── Legacy slash images, React contexts only ────────────────
271		   .srp-pricing-container exists only in the Nexus React renderers, never in this theme,
272		   so these rules are inert here and live WordPress pages keep the real image. Carried in
273		   the shared file purely to keep it identical to its Nexus counterpart. */
274		".srp-pricing-container .dv-price-stack p { position: relative; }",
275		".srp-pricing-container img.dv-price-slash,",
276		".srp-pricing-container img.da-price-slash {",
277		"	position: absolute; right: 0; top: 50%; width: 63px; height: 2px; max-width: 63px;",
278		"	overflow: hidden; text-indent: -9999px; color: transparent;",
279		"	background: linear-gradient(to right, transparent, #E8112D 12%, #E8112D 88%, transparent);",
280		"	transform: translateY(-50%) rotate(-11deg); pointer-events: none;",
281		"}",
282
283		/* Convenience for templates that hide a line on small screens 
283without writing their own CSS. */
284		"@media (max-width: 639px) { .da-price-row.da-hide-mobile { display: none !important; } }",
285		"@media (prefers-reduced-motion: reduce) {",
286		"	." + OVERLAY_CLASS + ", .da-dropdown-chevron { transition: none; }",
287		"}"
288	].join("\n");
289
290	function injectStyles() {
291		if (document.getElementById(STYLE_ELEMENT_ID)) return;
292
293		var style = document.createElement("style");
294		style.id = STYLE_ELEMENT_ID;
295		style.textContent = STYLES;
296		document.head.appendChild(style);
297	}
298
299	function findOpen(wrapper) {
300		for (var i = 0; i < openPanels.length; i++) {
301			if (openPanels[i].wrapper === wrapper) return openPanels[i];
302		}
303
304		return null;
305	}
306
307	function resolveParts(element, kind) {
308		if (!element || typeof element.closest !== "function") return null;
309
310		var wrapper = element.closest(kind.wrapper);
311		if (!wrapper) return null;
312
313		/* An open panel has been moved out of its wrapper, so the registry is the only way back. */
314		var open = findOpen(wrapper);
315		if (open) return open;
316
317		var panel = wrapper.querySelector(kind.panel);
318		if (!panel) return null;
319
320		var trigger = wrapper.querySelector(kind.trigger);
321
322		return {
323			kind: kind,
324			wrapper: wrapper,
325			panel: panel,
326			trigger: trigger,
327			anchor: (kind.anchor === "trigger" && trigger) ? trigger : wrapper,
328			placeholder: null
329		};
330	}
331
332	function isOpen(parts) {
333		return parts.wrapper.classList.contains(parts.kind.openClass);
334	}
335
336	/* Measure the panel against its row and pin it, flipping above when there is no room below. */
337	function positionPanel(parts) {
338		var panel = parts.panel;
339		var rect = parts.anchor.getBoundingClientRect();
340		var available = Math.min(MAX_PANEL_WIDTH, window.innerWidth - (VIEWPORT_MARGIN * 2));
341		var width = Math.max(Math.min(rect.width, available), Math.min(MIN_PANEL_WIDTH, available));
342
343		panel.style.width = width + "px";
344
345		var height = panel.offsetHeight;
346		var roomBelow = window.innerHeight - rect.bottom - PANEL_GAP;
347		var roomAbove = rect.top - PANEL_GAP;
348		var placeAbove = roomBelow < height && roomAbove > roomBelow;
349		var top = placeAbove ? rect.top - height - PANEL_GAP : rect.bottom + PANEL_GAP;
350		var left = rect.left;
351
352		/* Keep it on screen even when the row sits hard against an edge. */
353		top = Math.max(VIEWPORT_MARGIN, Math.min(top, window.innerHeight - height - VIEWPORT_MARGIN));
354		left = Math.max(VIEWPORT_MARGIN, Math.min(left, window.innerWidth - width - VIEWPORT_MARGIN));
355
356		panel.style.top = top + "px";
357		panel.style.left = left + "px";
358	}
359
360	/*
361	** Variant classes on the wrapper (`da-dropdown--details`). They style the panel too, and the panel
362	** leaves the wrapper while open, so it borrows them for as long as it is in body.
363	*/
364	function variantClasses(parts) {
365		return Array.prototype.filter.call(parts.wrapper.classList, function (name) {
366			return /^da-(dropdown|flyout)--/.test(name);
367		});
368	}
369
370	/*
371	** Move the panel to document.body, leaving a comment node to mark where it came from. This is
372	** what escapes the card's overflow, stacking context and containing block all at once.
373	*/
374	function detachPanel(parts) {
375		var panel = parts.panel;
376
377		if (panel.parentNode) {
378			parts.placeholder = document.createComment("da-panel");
379			panel.parentNode.insertBefore(parts.placeholder, panel);
380		}
381
382		document.body.appendChild(panel);
383		panel.classList.add(OVERLAY_CLASS);
384		variantClasses(parts).forEach(function (name) { panel.classList.add(name); });
385
386		/* The top layer sits above every stacking context, so the fallback z-index is not needed. */
387		if (supportsPopover) {
388			try {
389				panel.setAttribute("popover", "manual");
390				panel.showPopover();
391			} catch (error) {
392				panel.removeAttribute("popover");
393			}
394		}
395	}
396
397	/* Put the panel back where it came from, or drop it if its card was re-rendered meanwhile. */
398	function reattachPanel(parts) {
399		var panel = parts.panel;
400
401		if (supportsPopover && panel.hasAttribute("popover")) {
402			try {
403				panel.hidePopover();
404			} catch (error) {
405				/* Already hidden, e.g. the element left the document first. */
406			}
407
408			panel.removeAttribute("popover");
409		}
410
411		panel.classList.remove(OVERLAY_CLASS, VISIBLE_CLASS);
412		variantClasses(parts).forEach(function (name) { panel.classList.remove(name); });
413		panel.style.top = "";
414		panel.style.left = "";
415		panel.style.width = "";
416
417		var placeholder = parts.placeholder;
418
419		if (placeholder && placeholder.parentNode) {
420			placeholder.parentNode.insertBefore(panel, placeholder);
421			placeholder.parentNode.removeChild(placeholder);
422		} else if (panel.parentNode === document.body) {
423			/* The card went away under us - InstantSearch re-rendered the hit. Do not orphan it. */
424			document.body.removeChild(panel);
425		}
426
427		parts.placeholder = null;
428	}
429
430	function openPanel(parts) {
431		if (isOpen(parts)) return;
432
433		closeAllPanels(parts.wrapper);
434
435		/* Unhide first: an element that is still display:none cannot be measured or animated. */
436		parts.panel.hidden = false;
437		detachPanel(parts);
438		openPanels.push(parts);
439		positionPanel(parts);
440
441		requestAnimationFrame(function () {
442			requestAnimationFrame(function () {
443				parts.wrapper.classList.add(parts.kind.openClass);
444				parts.panel.classList.add(VISIBLE_CLASS);
445				if (parts.trigger) parts.trigger.setAttribute("aria-expanded", "true");
446			});
447		});
448	}
449
450	function closePanel(parts) {
451		if (!isOpen(parts)) return;
452
453		/* Children first: a flyout anchored to a row inside this panel has nothing to tether to. */
454		openPanels.slice().forEach(function (child) {
455			if (child !== parts && parts.panel.contains(child.wrapper)) closePanel(child);
456		});
457
458		parts.wrapper.classList.remove(parts.kind.openClass);
459		parts.panel.classList.remove(VISIBLE_CLASS);
460		if (parts.trigger) parts.trigger.setAttribute("aria-expanded", "false");
461
462		var index = openPanels.indexOf(parts);
463		if (index > -1) openPanels.splice(index, 1);
464
465		/* Reattach only once the fade has run, and only if nothing reopened it meanwhile. */
466		setTimeout(function () {
467			if (isOpen(parts)) return;
468
469			reattachPanel(parts);
470			parts.panel.hidden = true;
471		}, ANIMATION_MS);
472	}
473
474	/*
475	** The open panels an interaction at `node` belongs to: any panel whose row or detached body
476	** holds the node, plus every panel that in turn holds one of those rows - a flyout inside the
477	** "See Pricing Details" dropdown keeps the dropdown open. Checked against the panel element, not
478	** its position in the DOM, because the panel still contains its children's rows once it is in body.
479	*/
480	function keepChain(node) {
481		var chain = [];
482		if (!node || typeof node.nodeType !== "number") return chain;
483
484		openPanels.forEach(function (parts) {
485			if (parts.panel.contains(node) || parts.wrapper.contains(node)) chain.push(parts);
486		});
487
488		var grew = chain.length > 0;
489		while (grew) {
490			grew = false;
491
492			openPanels.forEach(function (parts) {
493				if (chain.indexOf(parts) > -1) return;
494
495				var isParent = chain.some(function (kept) { return parts.panel.contains(kept.wrapper); });
496				if (isParent) {
497					chain.push(parts);
498					grew = true;
499				}
500			});
501		}
502
503		return chain;
504	}
505
506	/**
507	 * Close every open panel, except the ones an interaction at `except` belongs to.
508	 * @param {Node} [except] A wrapper or any node inside a row or open panel; it and its parent panels stay open.
509	 */
510	function closeAllPanels(except) {
511		var keep = keepChain(except);
512
513		openPanels.slice().forEach(function (parts) {
514			if (keep.indexOf(parts) > -1) return;
515
516			closePanel(parts);
517		});
518	}
519
520	function toggle(element, kind) {
521		var parts = resolveParts(element, kind);
522		if (!parts) return;
523
524		if (isOpen(parts)) {
525			closePanel(parts);
526		} else {
527			openPanel(parts);
528		}
529	}
530
531	/**
532	 * Toggle the flyout owning `element`.
533	 * @param {Element} element Any node inside the flyout row, normally the trigger itself.
534	 */
535	function DAFlyOut(element) {
536		toggle(element, FLYOUT);
537	}
538
539	/**
540	 * Toggle the dropdown owning `element`.
541	 * @param {Element} element Any node inside the dropdown, normally the toggle button.
542	 */
543	function DADropdown(element) {
544		toggle(element, DROPDOWN);
545	}
546
547	injectStyles();
548
549	/*
550	** A pinned panel has to keep up with its row. Repositioning is rAF-throttled and runs on capture
551	** so it catches scrolling in the SRP results pane, not just the window.
552	*/
553	var repositionQueued = false;
554
555	function reposition() {
556		repositionQueued = false;
557
558		openPanels.slice().forEach(function (parts) {
559			/* The card was re-rendered or removed - close rather than leave a panel floating.
560			   closePanel's timeout does the reattach, which finds no placeholder and drops it. */
561			if (!document.contains(parts.wrapper)) {
562				closePanel(parts);
563				return;
564			}
565
566			positionPanel(parts);
567		});
568	}
569
570	function queueReposition() {
571		if (repositionQueued || openPanels.length === 0) return;
572
573		repositionQueued = true;
574		requestAnimationFrame(reposition);
575	}
576
577	window.addEventListener("scroll", queueReposition, true);
578	window.addEventListener("resize", queueReposition);
579
580	/* Outside click. Triggers stop here first via their inline `return false`. */
581	document.addEventListener("click", function (event) {
582		var target = event.target;
583		if (!target || typeof target.closest !== "function") return;
584
585		var onTrigger = KINDS.some(function (kind) { return target.closest(kind.trigger); });
586		if (onTrigger) return;
587
588		/* An open panel lives in body, so "inside" means inside the panel or its row - and the
589		   panels those sit in, so a click in a nested flyout leaves its dropdown open. */
590		closeAllPanels(target);
591	});
592
593	document.addEventListener("keydown", function (event) {
594		/* Innermost first: Escape in a flyout inside the dropdown dismisses just the flyout. */
595		if (event.key === "Escape") {
596			if (openPanels.length) closePanel(openPanels[openPanels.length - 1]);
597			return;
598		}
599
600		if (event.key !== "Enter" && event.key !== " ") return;
601
602		var target = event.target;
603		if (!target || typeof target.closest !== "function") return;
604
605		for (var i = 0; i < KINDS.length; i++) {
606			var trigger = target.closest(KINDS[i].trigger);
607			if (!trigger) continue;
608
609			/* A <button> toggle already activates natively on Enter/Space; an <i role="button"> does not. */
610			if (trigger.tagName === "BUTTON") return;
611
612			event.preventDefault();
613			toggle(trigger, KINDS[i]);
614			return;
615		}
616	});
617
618})(window, document);

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.