PageSourceSearch

https://www.galls.com/assets/cart/cart-drawer/cart-drawer.js?v=0.0.42

js galls.com collected 2026-09-24 19:39:59 UTC 32,053 bytes, 791 lines download raw bytes

1/**
2 * Variation Cart Drawer
3 *
4 * Provides a new cart-drawer experience for the A/B test variation group.
5 * Reuses the existing #samCartDrawer_Right container (same animation, overlay,
6 * scroll-lock) but renders different content:
7 *   - Title:   green check-circle + "{qty} Item(s) Added to Your Cart"
8 *   - Items:   rendered by the original samCD_FetchCartItems() — no duplication
9 *   - Footer:  Subtotal + View Cart (primary) → Checkout (secondary); USP adds ID.me text
10 *   - Cross-sell carousel injected inside the scrollable area
11 *
12 * Flow (eliminates flicker — all DOM writes happen BEFORE drawer animates in):
13 *   1. samCD_ShowCD() detects variation and calls showVariationCartDrawer()
14 *   2. Title, footer, and content layout are rendered synchronously
15 *   3. samCD_ShowCartDrawer() opens the drawer shell (overlay, slide-in)
16 *   4. samCD_FetchCartItems() AJAX populates items, then calls
17 *      onVariationCartItemsLoaded(subtotalPrice) — which re-renders the
18 *      variation footer with the subtotal, updates title qty, and injects
19 *      the cross-sell carousel.
20 *
21 * Cross-sell products are prefetched on page load by impulse-cross-sell.js
22 * so they render instantly when the drawer opens.
23 *
24 * @see cart/cross-sell/cart-drawer.css
25 * @see cart/cross-sell/impulse-cross-sell.js  (cross-sell carousel)
26 * @see ai/features/cross-sell.md
27 */
28(function () {
29  'use strict';
30
31  /* ======================== STATE ======================== */
32
33  let _active = false;       // true while variation drawer is open
34  let _addedQty = 1;         // qty user selected to add (read from #SIQTY, same as main.js)
35  let _addedItemKey = '';    // item SKU key (e.g. "GL1660 TAN SM") to match against cart response
36  let _focusTrapHandler = null; // keydown handler ref for focus trapping
37  let _skeletonFallbackTimer = null; // safety net to clear skeletons that never load
38
39
40  /* ======================== PUBLIC API ======================== */
41
42  /**
43   * Entry point — called from the samCD_ShowCD() guard in gtsamfooter_js.js.
44   *
45   * Renders variation title, footer, and content layout synchronously BEFORE
46   * opening the drawer shell, so the user never sees the original styling.
47   */
48  function showVariationCartDrawer() {
49    _active = true;
50
51    // Capture qty and item SKU. For Quick View adds, addToCartQV() pre-sets
52    // window._variationAddedQty and window._variationAddedItemKey before
53    // the QV modal DOM is cleared. For PDP adds, read from #SIQTY and .sku.
54    if (window._variationAddedQty) {
55      _addedQty = window._variationAddedQty;
56    } else {
57    var siqtyEl = document.getElementById('SIQTY');
58    _addedQty = siqtyEl ? (parseInt(siqtyEl.value, 10) || 1) : 1;
59    }
60
61    if (window._variationAddedItemKey) {
62      _addedItemKey = window._variationAddedItemKey;
63    } else {
64      var skuEl = document.querySelector('.sku');
65      _addedItemKey = skuEl ? skuEl.textContent.trim() : '';
66      window._variationAddedItemKey = _addedItemKey;
67    }
68
69    // 0. Add BEM block class to the drawer panel (enables CSS overrides for header/footer)
70    var drawerPanel = document.getElementById('samCartDrawer_Right');
71    if (drawerPanel) drawerPanel.classList.add('cart-drawer');
72
73    // 1. Render variation layout BEFORE opening (eliminates flicker)
74    _renderTitle();
75    _renderFooter();
76    _activateContentLayout();
77
78    // 2. Open drawer (overlay, slide-in, scroll-lock) — content already in place
79    samCD_ShowCartDrawer();
80    samCD_FetchYouMightLikeItems();
81
82    // 2a. Focus close button & enable focus trap for accessibility
83    _focusCloseButton();
84    _enableFocusTrap();
85
86    // 2b. Inject shipping bar skeleton (replaced by _renderVariationBar after AJAX)
87    _renderShippingBarSkeleton();
88
89    // 2c. Inject product skeleton into items wrapper (replaced by samCD_FetchCartItems)
90    _renderProductSkeleton();
91
92    // 2d. Inject cross-sell skeleton so it appears from frame 1 (replaced by _injectCrossSell)
93    var csContent = document.getElementById('samCD_Content');
94    if (csContent) {
95      var csContainer = csContent.querySelector('[data-target="crossSellMiniCart"]');
96      if (!csContainer) {
97        csContainer = document.createElement('div');
98        csContainer.setAttribute('data-target', 'crossSellMiniCart');
99        csContent.appendChild(csContainer);
100      }
101      _renderCrossSellSkeleton(csContainer);
102    }
103
104    // 2e. Safety net — if the shipping bar / cross-sell content never renders
105    //     (render path errors, feature disabled, script not loaded, etc.), the
106    //     skeleton would pulse forever. Remove any leftover skeleton after a delay.
107    if (_skeletonFallbackTimer) clearTimeout(_skeletonFallbackTimer);
108    _skeletonFallbackTimer = setTimeout(_hideStuckSkeletons, 5000);
109
110    // 3. Kick off cart-item fetch using the ORIGINAL function (no duplication).
111    //    It renders items into #samCDIS_row_flex_wrapper and writes the original
112    //    footer into #samCD_BottomSection_SubTotal, then calls
113    //    onVariationCartItemsLoaded() which re-renders the variation footer,
114    //    updates the title qty, and injects the cross-sell carousel.
115    if (parseInt($('.cart--total').html()) === 0) {
116      samCD_FetchEmptyCartContents();
117    } else {
118      samCD_FetchCartItems();
119    }
120
121    // 4. GA4 event -- deferred to onVariationCartItemsLoaded() so item data is available
122  }
123
124  /**
125   * Clean up variation-drawer artifacts when the drawer closes.
126   * Safe to call even when the variation drawer was never opened.
127   */
128  function cleanupVariationDrawer() {
129    if (!_active) return;
130    _active = false;
131    _addedQty = 1;
132    _addedItemKey = '';
133    delete window._variationAddedItemKey;
134    delete window._variationAddedQty;
135
136    // Cancel the skeleton safety-net timer so it never fires against a closed/reopened drawer
137    if (_skeletonFallbackTimer) {
138      clearTimeout(_skeletonFallbackTimer);
139      _skeletonFallbackTimer = null;
140    }
141
142    // Remove focus trap
143    _disableFocusTrap();
144
145    // Destroy cross-sell carousel
146    if (typeof destroyCrossSellOnMiniCart === 'function') {
147      destroyCrossSellOnMiniCart();
148    }
149
150    // Remove injected cross-sell container
151    var crossSellEl = document.querySelector('#samCD_Content [data-target="crossSellMiniCart"]');
152    if (crossSellEl) crossSellEl.remove();
153
154    // Remove injected shipping bar
155    var shipBarEl = document.querySelector('#samCD_Content [data-target="cartDrawerShippingBar"]');
156    if (shipBarEl) shipBarEl.remove();
157
158    // Remove BEM block class from the drawer panel
159    var drawerPanel = document.getElementById('samCartDrawer_Right');
160    if (drawerPanel) drawerPanel.classList.remove('cart-drawer');
161
162    // Restore original class on #samCD_Content
163    var content = document.getElementById('samCD_Content');
164    if (content) {
165      content.classList.remove('cart-drawer--active');
166      content.classList.add('samCartDrawer_Right_FlexRow');
167    }
168  }
169
170
171  /* ======================== TITLE ======================== */
172
173  /**
174   * Render the variation title synchronously (before drawer opens).
175   * Uses a data-target attribute so we can update the qty text after items load.
176   */
177  function _renderTitle() {
178    $('#samCD_Title').html(
179      '<div style="width: 100%;">' +
180      '  <div class="b5-active">' +
181      '    <div class="d-flex justify-content-between align-items-center">' +
182      '      <div class="d-flex align-items-center gap-8">' +
183      '        <svg class="feather-icon" style="width: 20px; height: 20px; color: #16a34a;">' +
184      '          <use href="/images/icons/feather-sprite.svg#check-circle"></use>' +
185      '        </svg>' +
186      '        <span class="cart-drawer__title-text" data-target="cartDrawerTitleText"><span class="cart-drawer__skeleton-qty"></span> Item Added to Your Cart</span>' +
187      '      </div>' +
188      '      <button class="cart-drawer__close-btn" onclick="samCD_CloseCartDrawer()" aria-label="Close cart">' +
189      '        <svg class="feather-icon" style="width: 20px; height: 20px;">' +
190      '          <use href="/images/icons/feather-sprite.svg#x"></use>' +
191      '        </svg>' +
192      '      </button>' +
193      '    </div>' +
194      '    <!-- Cart reservation banner -->' +
195      '    <div class="cart-reservation-banner">' +
196      '      <div class="cart-reservation cart-reservation--mini-cart">' +
197      '        <div class="cart-reservation__icon">' +
198      '          <span class="glyphicon glyphicon-flash"></span>' +
199      '        </div>' +
200      '        <p class="cart-reservation__text">' +
201      '          Your cart is reserved for 30 mins <span class="cart-reservation__timer"></span>' +
202      '        </p>' +
203      '      </div>' +
204      '    </div>' +
205      '  </div>' +
206      '</div>'
207    );
208
209    // Init cart timer for the banner
210    if (typeof CartTimer !== 'undefined' && CartTimer.init) {
211      CartTimer.init();
212    }
213  }
214
215
216  /* ======================== FOOTER ======================== */
217
218  /**
219   * Render the variation footer (View Cart → Checkout) synchronously.
220   * Called both at initial render and after samCD_FetchCartItems() overwrites.
221   * @param {string} [subtotalPrice] — e.g. "$49.99", captured from original footer
222   * @param {number} [totalItemCount] — total qty across all cart items (from AJAX)
223   */
224  function _renderFooter(subtotalPrice, totalItemCount) {
225    var footerEl = document.getElementById('samCD_BottomSection_SubTotal');
226    if (!footerEl) return;
227
228    var isUSP = (typeof getBrandName === 'function' && getBrandName() === 'U.S. Patriot');
229
230    // Build ID.me text for USP (plain text, no link)
231    var idmeHTML = '';
232    if (isUSP) {
233      idmeHTML =
234        '<p class="cart-drawer__idme">' +
235        '  Verify with ID.me at checkout' +
236        '</p>';
237    }
238
239    // Subtotal row — only shown after cart items load (subtotalPrice is passed)
240    var subtotalHTML = '';
241    if (subtotalPrice) {
242      var itemCount = totalItemCount || 0;
243      var countText = itemCount > 0
244        ? ' <span class="cart-drawer__subtotal-count">
244(' + itemCount + (itemCount === 1 ? ' item' : ' items') + ')</span>'
245        : '';
246      subtotalHTML =
247        '<div class="cart-drawer__subtotal">' +
248        '  <span class="cart-drawer__subtotal-label">Subtotal' + countText + '</span>' +
249        '  <span class="cart-drawer__subtotal-price">' + subtotalPrice + '</span>' +
250        '</div>';
251    }
252
253    footerEl.innerHTML =
254      '<div class="cart-drawer__footer">' +
255      subtotalHTML +
256      '  <a href="/cart" class="cart-drawer__btn cart-drawer__btn--primary">View Cart</a>' +
257      '  <a href="/checkout" class="cart-drawer__btn cart-drawer__btn--secondary" data-action="checkout">Checkout</a>' +
258      idmeHTML +
259      '</div>';
260
261    // GA4: cart_drawer_begin_checkout on checkout button click
262    var checkoutBtn = footerEl.querySelector('[data-action="checkout"]');
263    if (checkoutBtn) {
264      checkoutBtn.addEventListener('click', function () {
265        if (typeof sendGA4Event !== 'function') return;
266        var items = [];
267        if (window.samArrCD_CartItems && typeof removeFromMinicartObj === 'function') {
268          for (var i = 0; i < window.samArrCD_CartItems.length; i++) {
269            var obj = removeFromMinicartObj(window.samArrCD_CartItems[i]);
270            if (obj && obj.items && obj.items[0]) items.push(obj.items[0]);
271          }
272        }
273        dataLayer.push({ ecommerce: null });
274        sendGA4Event('cart_drawer_begin_checkout', { ecommerce: { currency: 'USD', items: items } });
275      });
276    }
277
278    footerEl.style.display = '';
279  }
280
281
282  /* ======================== CONTENT LAYOUT ======================== */
283
284  /**
285   * Swap #samCD_Content from original centered layout to variation column layout.
286   * Called synchronously before drawer opens so background is white from frame 1.
287   */
288  function _activateContentLayout() {
289    var content = document.getElementById('samCD_Content');
290    if (!content) return;
291    content.classList.remove('samCartDrawer_Right_FlexRow');
292    content.classList.add('cart-drawer--active');
293  }
294
295
296  /* ======================== POST-RENDER HOOK ======================== */
297
298  /**
299   * Called by gtsamfooter_js.js after samCD_FetchCartItems() writes the
300   * original footer into #samCD_BottomSection_SubTotal.
301   *
302   * Replaces the MutationObserver approach with an explicit hook — easier
303   * to reason about, no DOM-scraping, and timing is guaranteed.
304   *
305   * @param {string} subtotalPrice - e.g. "$123.45" from samCD_TmpSubTotal_Price
306   * @param {number} totalItemCount - total qty across ALL cart items (from AJAX)
307   */
308  function onVariationCartItemsLoaded(subtotalPrice, totalItemCount) {
309    if (!_active) return;
310
311    // 1. Re-render variation footer (original was overwritten by samCD_FetchCartItems)
312    _renderFooter(subtotalPrice || '', totalItemCount || 0);
313
314    // 2. Update the title with the item qty
315    _updateTitleQty();
316
317    // 3. Convert interactive qty controls to static text
318    _convertQtyToStatic();
319
320    // 4. Fix price to reflect only the just-added items (unit price × addedQty)
321    _fixAddedItemPrice();
322
323    // 5. Convert legacy accordion to BEM classes
324    _convertAccordionToBEM();
325
326    // 6. Inject cross-sell into the scrollable area
327    _injectCrossSell();
328
329    // 7. GA4: view_mini_cart with the just-added item (deferred from showVariationCartDrawer)
330    if (typeof sendGA4Event === 'function') {
331      var ecomItems = [];
332      if (window.samArrCD_CartItems && window.samArrCD_CartItems[0] && typeof removeFromMinicartObj === 'function') {
333        var obj = removeFromMinicartObj(window.samArrCD_CartItems[0]);
334        if (obj && obj.items) ecomItems = obj.items;
335      }
336      dataLayer.push({ ecommerce: null });
337      sendGA4Event('view_mini_cart', { ecommerce: { currency: 'USD', items: ecomItems } });
338    }
339  }
340
341
342  /* ======================== TITLE QTY UPDATE ======================== */
343
344  /**
345   * Update the title text with the qty that was added to the cart.
346   * Uses _addedQty captured from #SIQTY at add-to-cart time (same as main.js).
347   */
348  function _updateTitleQty() {
349    var titleEl = document.querySelector('[data-target="cartDrawerTitleText"]');
350    if (!titleEl) return;
351
352    var qty = _addedQty;
353    titleEl.textContent = qty + (qty > 1 ? ' Items' : ' Item') + ' Added to Your Cart';
354
355    // Remove skeleton placeholder if still present
356    var skelQty = titleEl.querySelector('.cart-drawer__skeleton-qty');
357    if (skelQty) skelQty.remove();
358  }
359
360
361  /* ======================== STATIC QTY ======================== */
362
363  /**
364   * Replace interactive qty selects / 10+ inputs with plain static text.
365   * The select and input are hidden via CSS (.cart-drawer .samCDIS_row_flex_qty_select,
366   * .cart-drawer .samCD_QtyTenPlus_Div { display: none }). This function appends
367   * a visible <span> with the qty value next to the existing "Qty:" text.
368   */
369  function _convertQtyToStatic() {
370    var qtyContainers = document.querySelectorAll('.cart-drawer .samCDIS_row_flex_qty');
371    for (var i = 0; i < qtyContainers.length; i++) {
372      var container = qtyContainers[i];
373      // Skip if already converted
374      if (container.querySelector('.cart-drawer__qty-static')) continue;
375
376      // Use the qty that was just added (not the total cart qty for this SKU)
377      var qty = _addedQty;
378
379      // Create static qty element
380      var span = document.createElement('span');
381      span.className = 'cart-drawer__qty-static';
382      span.textContent = 'Qty: ' + qty;
383
384      // Move qty into .samCDIS_row_flex_info (after options, before price)
385      var row = container.closest('.samCDIS_row_flex');
386      if (row) {
387        var infoEl = row.querySelector('.samCDIS_row_flex_info');
388        var priceEl = row.querySelector('.samCDIS_row_flex_options_price');
389        if (infoEl && priceEl) {
390          infoEl.insertBefore(span, priceEl);
391        } else if (infoEl) {
392          infoEl.appendChild(span);
393        } else {
394          container.appendChild(span);
395        }
396      } else {
397        container.appendChild(span);
398      }
399    }
400  }
401
402
403  /* ======================== CUSTOMIZATIONS ACCORDION ======================== */
404
405  /**
406   * Replace legacy samCDIS_row_flex_accordion elements with new BEM classes.
407   *
408   * The footer JS (gtsamfooter_js.js) renders the accordion with legacy classes
409   * and wires up click handlers via getElementsByClassName. Rather than fighting
410   * the highly-specific base CSS selectors with overrides, we re-create the
411   * accordion with clean BEM classes and our own click handler.
412   *
413   * Legacy elements (.samCDIS_row_flex_accordion, .samCDIS_row_flex_accordion_panel)
414   * are hidden via CSS (display: none !important).
415   */
416  function _convertAccordionToBEM() {
417    var rows = document.querySelectorAll('.cart-drawer .samCDIS_row_flex');
418    for (var i = 0; i < rows.length; i++) {
419      var row = rows[i];
420      var infoEl = row.querySelector('.samCDIS_row_flex_info');
421      if (!infoEl) continue;
422
423      // Skip if already converted
424      if (infoEl.querySelector('.cart-drawer__customs-toggle')) continue;
425
426      // Find the legacy panel (contains the VAS HTML)
427      var legacyPanel = infoEl.querySelector('.samCDIS_row_flex_accordion_panel');
428      if (!legacyPanel || !legacyPanel.innerHTML.trim()) continue;
429
430      // -- Build new toggle button --
431      var toggle = document.createElement('button');
432      toggle.className = 'cart-drawer__customs-toggle';
433      toggle.setAttribute('aria-expanded', 'false');
434      toggle.textContent = 'Customizations';
435
436      // -- Build new panel --
437      var panel = document.createElement('div');
438      panel.className = 'cart-drawer__customs-panel';
439      panel.innerHTML = legacyPanel.innerHTML;
440
441      // Hide the customization fee line (Total/Included Customization: $XX.XX)
442      var optionsDivs = infoEl.querySelectorAll('.samCDIS_row_flex_options');
443      for (var j = 0; j < optionsDivs.length; j++) {
444        if (optionsDivs[j].textContent.indexOf('Customization') >= 0) {
445          optionsDivs[j].style.display = 'none';
446          break;
447        }
448      }
449
450      // Insert new toggle + panel before the hidden legacy panel
451      infoEl.insertBefore(toggle, legacyPanel);
452      infoEl.insertBefore(panel, legacyPanel);
453
454      // -- Wire up click handler --
455      (function (btn, pnl) {
456        btn.addEventListener('click', function () {
457          var isOpen = btn.classList.toggle('cart-drawer__customs-toggle--open');
458          btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
459          if (isOpen) {
460            pnl.style.maxHeight = pnl.scrollHeight + 'px';
461          } else {
462            pnl.style.maxHeight = null;
463          }
464        });
465      })(toggle, panel);
466    }
467  }
468
469
470  /* ======================== ADDED-ITEM PRICE ======================== */
471
472  /**
473   * Fix the displayed price to reflect only the items just added to cart.
474   *
475   * The server returns `item_total` (total for ALL qty of that SKU in the cart)
476   * in `.samCDIS_row_flex_price`. For the variation drawer we want:
477   *   unit_price × _addedQty
478   *
479   * Unit price is parsed from `.samCDIS_row_flex_options_price`. Sale items
480   * render as: <span class="original">$48.99</span><br><span class="discount">$36.75</span>
481   * so we must use the .discount span (sale price) when present.
482   */
483  function _fixAddedItemPrice() {
484    var rows = document.querySelectorAll('.cart-drawer .samCDIS_row_flex');
485    for (var i = 0; i < rows.length; i++) {
486      var row = rows[i];
487
488      // 1. Parse unit price from the "ea." element
489      var unitPriceEl = row.querySelector('.samCDIS_row_flex_options_price');
490      if (!unitPriceEl) continue;
491
492      // Use the sale (discount) price when the item is on sale,
493      // otherwise fall back to the first dollar amount found.
494      var rawText;
495      var discountEl = unitPriceEl.querySelector('.discount');
496      if (discountEl) {
497        rawText = discountEl.textContent.replace(/[^0-9.]/g, '');
498      } else {
499        rawText = unitPriceEl.textContent.replace(/[^0-9.]/g, '');
500      }
501      var unitPrice = parseFloat(rawText);
502      if (isNaN(unitPrice)) continue;
503
504      // 2. Calculate the added-item total
505      var addedTotal = unitPrice * _addedQty;
506
507      // 3. Write into .samCDIS_row_flex_price (overwrites server's item_total)
508      var priceEl = row.querySelector('.samCDIS_row_flex_price');
509      if (priceEl) {
510        priceEl.textContent = '$' + addedTotal.toFixed(2);
511      }
512    }
513  }
514
515
516  /* ======================== SKELETON ======================== */
517
518  /**
519   * Inject a skeleton placeholder for the shipping progress bar.
520   * Mimics the truck + thin bar + label layout. Automatically replaced
521   * when BuyMoreSaveMore._renderVariationBar() renders the real bar
522   * via [data-target="cartDrawerShippingBar"].
523   */
524  function _renderShippingBarSkeleton() {
525    var content = document.getElementById('samCD_Content');
526    if (!content) return;
527
528    var container = content.querySelector('[data-target="cartDrawerShippingBar"]');
529    if (!container) {
530      container = document.createElement('div');
531      container.setAttribute('data-target', 'cartDrawerShippingBar');
532      var itemsEl = content.querySelector('#samCDIS_row_flex_wrapper');
533      if (itemsEl) {
534        itemsEl.parentNode.insertBefore(container, itemsEl);
535      } else {
536        content.insertBefore(container, content.firstChild);
537      }
538    }
539
540    container.innerHTML =
541      '<div class="cart-drawer__ship-skeleton">' +
542      '  <div class="cart-drawer__ship-skeleton-bar"></div>' +
543      '  <div class="cart-drawer__ship-skeleton-label"></div>' +
544      '</div>';
545    container.style.display = '';
546  }
547
548
549  /**
550   * Inject a skeleton placeholder into #samCDIS_row_flex_wrapper.
551   * Mimics a single product row: image square + 3 text lines.
552   * Automatically replaced when samCD_FetchCartItems() writes real content
553   * via $('#samCDIS_row_flex_wrapper').html(...).
554   * Reuses the existing @keyframes Skeleton_Pulse from algolia_recommend.css.
555   */
556  function _renderProductSkeleton() {
557    var wrapper = document.getElementById('samCDIS_row_flex_wrapper');
558    if (!wrapper) return;
559
560    wrapper.style.display = '';
561    wrapper.innerHTML =
562      '<div class="cart-drawer__skeleton">' +
563      '  <div class="cart-drawer__skeleton-image"></div>' +
564      '  <div class="cart-drawer__skeleton-lines">' +
565      '    <div class="cart-drawer__skeleton-line" style="width: 85%; height: 21px"></div>' +
566      '    <div class="cart-drawer__skeleton-line" style="width: 60%; height: 19px"></div>' +
567      '    <div class="cart-drawer__skeleton-line" style="width: 50%; height: 17px"></div>' +
568      '    <div class="cart-drawer__skeleton-line" style="width: 45%; height: 17px"></div>' +
569      '    <div class="cart-drawer__skeleton-line" style="width: 35%; height: 19px"></div>' +
570      '  </div>' +
571      '</div>';
572  }
573
574
575  /**
576   * Safety net for skeletons that never get replaced by real content.
577   * Scheduled after the drawer opens (see showVariationCartDrawer). If the
578   * shipping bar or cross-sell add-ons render path failed — early return, the
579   * feature is disabled, the script didn't load, or it threw — the skeleton
580   * would otherwise pulse forever. Each branch only acts when the skeleton is
581   * STILL present AND real content has NOT rendered, so a slow-but-successful
582   * load is never clobbered (and a late render simply re-creates / re-shows it).
583   * Product-row skeleton is intentionally left alone.
584   */
585  function _hideStuckSkeletons() {
586    // Shipping / free-shipping promo bar
587    var shipBar = document.querySelector('#samCD_Content [data-target="cartDrawerShippingBar"]');
588    if (shipBar &&
589        shipBar.querySelector('.cart-drawer__ship-skeleton') &&
590        !shipBar.querySelector('.cart-drawer__ship')) {
591      shipBar.remove();
592    }
593
594    // Cross-sell "Popular Add-Ons" carousel
595    var crossSell = document.querySelector('#samCD_Content [data-target="crossSellMiniCart"]');
596    if (crossSell &&
597        crossSell.querySelector('.cart-drawer__cs-skeleton') &&
598        !crossSell.querySelector('.cross-sell--mini-cart')) {
599      crossSell.style.display = 'none';
600      crossSell.innerHTML = '';
601    }
602  }
603
604
605  /**
606   * Inject a skeleton placeholder for the cross-sell carousel.
607   * Shows a heading line + 3 card placeholders
608   * (image, swatches, 2 description lines, price, add button each).
609   * Replaced when renderCrossSellOnMiniCart() writes real carousel HTML.
610   */
611  function _renderCrossSellSkeleton(container) {
612    container.innerHTML =
613      '<div class="cart-drawer__cs-skeleton">' +
614      '  <div class="cart-drawer__cs-skeleton-heading"></div>' +
615      '  <div class="cart-drawer__cs-skeleton-cards">' +
616      '    <div class="cart-drawer__cs-skeleton-card">' +
617      '      <div class="cart-drawer__cs-skeleton-img"></div>' +
618      '      <div class="cart-drawer__cs-skeleton-line" style="width: 35%"></div>' +
619      '      <div class="cart-drawer__cs-skeleton-line" style="width: 90%"></div>' +
620      '      <div class="cart-drawer__cs-skeleton-line" style="width: 80%"></div>' +
621      '      <div class="cart-drawer__cs-skeleton-line" style="width: 50%"></div>' +
622      '      <div class="cart-drawer__cs-skeleton-btn"></div>' +
623      '    </div>' +
624      '    <div class="cart-drawer__cs-skeleton-card">' +
625      '      <div class="cart-drawer__cs-skeleton-img"></div>' +
626      '      <div class="cart-drawer__cs-skeleton-line" style="width: 30%"></div>' +
627      '      <div class="cart-drawer__cs-skeleton-line" style="width: 85%"></div>' +
628      '      <div class="cart-drawer__cs-skeleton-line" style="width: 70%"></div>' +
629      '      <div class="cart-drawer__cs-skeleton-line" style="width: 45%"></div>' +
630      '      <div class="cart-drawer__cs-skeleton-btn"></div>' +
631      '    </div>' +
632      '    <div class="cart-drawer__cs-skeleton-card">' +
633      '      <div class="cart-drawer__cs-skeleton-img"></div>' +
634      '      <div class="cart-drawer__cs-skeleton-line" style="width: 40%"></div>' +
635      '      <div class="cart-drawer__cs-skeleton-line" style="width: 88%"></div>' +
636      '      <div class="cart-drawer__cs-skeleton-line" style="width: 75%"></div>' +
637      '      <div class="cart-drawer__cs-skeleton-line" style="width: 55%"></div>' +
638      '      <div class="cart-drawer__cs-skeleton-btn"></div>' +
639      '    </div>' +
640      '  </div>' +
641      '</div>';
642    container.style.display = '';
643  }
644
645
646  /* ======================== CROSS-SELL INJECTION ======================== */
647
648  /**
649   * Inject a cross-sell container into #samCD_Content (the scrollable area)
650   * and trigger the carousel render.
651   * Cross-sell products are already prefetched (impulse-cross-sell.js fetches
652   * on page load), so renderCrossSellOnMiniCart() renders from cache instantly.
653   */
654  function _injectCrossSell() {
655    var content = document.getElementById('samCD_Content');
656    if (!content) return;
657
658    // Reuse existing container if present (e.g. from CGBCHEAD.html), otherwise create one
659    var container = content.querySelector('[data-target="crossSellMiniCart"]');
660    if (container) {
661      // Skip if carousel is already rendered inside it
662      if (container.querySelector('.cross-sell--mini-cart')) return;
663    } else {
664      container = document.createElement('div');
665      container.setAttribute('data-target', 'crossSellMiniCart');
666      content.appendChild(container);
667    }
668
669    // Show a skeleton placeholder while the carousel loads
670    _renderCrossSellSkeleton(container);
671
672    // Render the cross-sell carousel (from impulse-cross-sell.js).
673    // renderCrossSellOnMiniCart is async — if it fails on first call (Algolia
674    // not ready), retry once after a short delay.
675    if (typeof renderCrossSellOnMiniCart === 'function') {
676      renderCrossSellOnMiniCart().then(function () {
677        // Check if carousel actually rendered content
678        if (!container.querySelector('.cross-sell--mini-cart')) {
679          setTimeout(function () {
680            if (_active && typeof renderCrossSellOnMiniCart === 'function') {
681              renderCrossSellOnMiniCart();
682            }
683          }, 800);
684        }
685      }).catch(function () {
686        // Retry once after delay
687        setTimeout(function () {
688          if (_active && typeof renderCrossSellOnMiniCart === 'function') {
689            renderCrossSellOnMiniCart();
690          }
691        }, 800);
692      });
693    }
694  }
695
696
697  /* ======================== ACCESSIBILITY ======================== */
698
699  /**
700   * Move focus to the close button after the drawer opens.
701   * Uses requestAnimationFrame so the DOM has painted the drawer content.
702   */
703  function _focusCloseButton() {
704    requestAnimationFrame(function () {
705      var closeBtn = document.querySelector('.cart-drawer .cart-drawer__close-btn');
706      if (closeBtn) closeBtn.focus();
707    });
708  }
709
710  /**
711   * Enable focus trapping inside the drawer panel so Tab/Shift+Tab
712   * cycle only through focusable elements within #samCartDrawer_Right.
713   */
714  function _enableFocusTrap() {
715    var panel = document.getElementById('samCartDrawer_Right');
716    if (!panel) return;
717
718    _focusTrapHandler = function (e) {
719      if (e.key !== 'Tab') return;
720
721      var focusable = panel.querySelectorAll(
722        'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
723      );
724      if (!focusable.length) return;
725
726      var first = focusable[0];
727      var last = focusable[focusable.length - 1];
728
729      if (e.shiftKey) {
730        if (document.activeElement === first) {
731          e.preventDefault();
732          last.focus();
733        }
734      } else {
735        if (document.activeElement === last) {
736          e.preventDefault();
737          first.focus();
738        }
739      }
740    };
741
742    document.addEventListener('keydown', _focusTrapHandler);
743  }
744
745  /**
746   * Remove the focus trap keydown listener.
747   */
748  function _disableFocusTrap() {
749    if (_focusTrapHandler) {
750      document.removeEventListener('keydown', _focusTrapHandler);
751      _focusTrapHandler = null;
752    }
753  }
754
755
756  /* ======================== CLOSE HOOK ======================== */
757
758  /**
759   * Wrap samCD_CloseCartDrawer to hook cleanup.
760   * Must run after the footer JS defines samCD_CloseCartDrawer.
761   */
762  function _hookCloseFunction() {
763    if (typeof samCD_CloseCartDrawer !== 'function') {
764      // Retry after DOM is ready (footer JS may not have loaded yet)
765      document.addEventListener('DOMContentLoaded', _hookCloseFunction);
766      return;
767    }
768
769    var originalClose = samCD_CloseCartDrawer;
770    window.samCD_CloseCartDrawer = function () {
771      cleanupVariationDrawer();       // no-op if _active === false
772      return originalClose.apply(this, arguments);
773    };
774  }
775
776
777  /* ======================== INIT ======================== */
778
779  // Expose the entry points globally
780  window.showVariationCartDrawer = showVariationCartDrawer;
781  window.cleanupVariationDrawer = cleanupVariationDrawer;
782  window.onVariationCartItemsLoaded = onVariationCartItemsLoaded;
783
784  // Hook into the close function when ready
785  if (document.readyState === 'loading') {
786    document.addEventListener('DOMContentLoaded', _hookCloseFunction);
787  } else {
788    _hookCloseFunction();
789  }
790
791})();

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.