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.