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.