1/* OrderArt â new deals popup loader (Phase 2) 2 * 3 * Drops the deal-builder popup onto any page and makes a deal's "Add to cart" 4 * open it INSTEAD of the old full-page redirect flow. 5 * 6 * The popup runs in a transparent full-viewport overlay iframe (/products/deal-popup) 7 * so it can't clash with the theme's CSS. 8 * 9 * Loaded globally via global-js-vars.php; OA_DEALS is populated on the deals listing 10 * and fetched lazily (via AJAX_URL.getDealData) on all other pages. 11 */ 12if (window.__oaDealLoaderInit) { /* already loaded once â skip re-execution */ } 13else { window.__oaDealLoaderInit = true; } 14(function () { 15 if (window.__oaDealLoaderDone) return; 16 window.__oaDealLoaderDone = true; 17 // Deal keys defined in the popup app (sample data for the pilot). 18 var SAMPLE_KEYS = ['pizza-meal-deal', 'family-feast', 'flying-solo']; 19 20 var frame = null, frameReady = false, pendingMsg = null, toastEl = null; 21 22 // Set when a deal child is removed so the next "added" event only posts the new item(s). 23 // Shape: { dealId, remainingProductIds: { productId: true } } 24 var pendingRefill = null; 25 26 function injectFrame() { 27 if (document.getElementById('oaDealFrame')) { frame = document.getElementById('oaDealFrame'); return; } 28 29 frame = document.createElement('iframe'); 30 frame.id = 'oaDealFrame'; 31 frame.src = (window.OA_DEAL_POPUP_URL || '/products/deal-popup') + '?v=11'; 32 frame.title = 'Deal builder'; 33 frame.setAttribute('allowtransparency', 'true'); 34 frame.style.cssText = 'display:none;position:fixed;inset:0;width:100%;height:100%;border:0;z-index:99999;background:transparent;'; 35 document.body.appendChild(frame); 36 37 toastEl = document.createElement('div'); 38 toastEl.id = 'oaDealToast'; 39 toastEl.style.cssText = 'position:fixed;bottom:28px;left:50%;transform:translateX(-50%) translateY(16px);background:#1A1410;color:#fff;padding:13px 22px;border-radius:100px;font:600 14px/1 system-ui,sans-serif;box-shadow:0 6px 24px rgba(0,0,0,.32);z-index:100000;opacity:0;transition:opacity .3s, transform .3s;pointer-events:none;white-space:nowrap;'; 40 document.body.appendChild(toastEl); 41 } 42 43 function showFrame() { frame.style.display = 'block'; } 44 function hideFrame() { frame.style.display = 'none'; } 45 46 function toast(msg) { 47 if (!toastEl) return; 48 toastEl.textContent = msg; 49 toastEl.style.opacity = '1'; 50 toastEl.style.transform = 'translateX(-50%) translateY(0)'; 51 setTimeout(function () { 52 toastEl.style.opacity = '0'; 53 toastEl.style.transform = 'translateX(-50%) translateY(16px)'; 54 }, 3000); 55 } 56 57 function oaCsrf() { 58 if (window.RESTAURANT_CONFIG && window.RESTAURANT_CONFIG.csrf && window.RESTAURANT_CONFIG.csrf.value) return window.RESTAURANT_CONFIG.csrf.value; 59 var m = document.querySelector('meta[name="csrf-token"]'); 60 return m ? m.getAttribute('content') : ''; 61 } 62 function oaCsrfKey() { 63 if (window.RESTAURANT_CONFIG && window.RESTAURANT_CONFIG.csrf && window.RESTAURANT_CONFIG.csrf.key) return window.RESTAURANT_CONFIG.csrf.key; 64 return '_csrf'; 65 } 66 function cartUrl() { 67 if (window.AJAX_URL && window.AJAX_URL.addToCart) return window.AJAX_URL.addToCart; 68 return (window.OA_DEAL_POPUP_URL || '').replace(/\/products\/deal-popup.*/, '') + '/cart/add-to-cart'; 69 } 70 function removeItemUrl() { 71 if (window.AJAX_URL && window.AJAX_URL.removeCartItem) return window.AJAX_URL.removeCartItem; 72 return (window.OA_DEAL_POPUP_URL || '').replace(/\/products\/deal-popup.*/, '') + '/cart/remove-item'; 73 } 74 function themeLoader(on) { 75 var l = document.querySelector('.loader-wrapper'); 76 if (l) l.classList.toggle('active_loader', !!on); 77 } 78 function parseJsonResponse(t) { 79 var marker = 'YII-BLOCK-BODY-BEGIN]]>'; 80 var i = t.indexOf(marker); 81 if (i >= 0) t = t.slice(i + marker.length); 82 try { return JSON.parse(t); } catch (e) { return null; } 83 } 84 function cartPost(params) { 85 var csrfKey = oaCsrfKey(); 86 params[csrfKey] = oaCsrf(); 87 return fetch(cartUrl(), { 88 method: 'POST', 89 credentials: 'same-origin', 90 headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' }, 91 body: new URLSearchParams(params).toString() 92 }).then(function (r) { return r.text(); }).then(parseJsonResponse); 93 } 94 function setHtml(el, html) { 95 // Use jQuery .html() when available so inline scripts execute and delegated 96 // event handlers are correctly re-attached â matches how products-page.998.js updates the cart. 97 if (window.$ && window.$.fn) { $(el).html(html); } else { el.innerHTML = html; } 98 } 99 function applyCart(resp) { 100 if (!resp) return; 101 // add-to-cart returns html_cart; remove-item returns data â handle both. 102 // Use [data-ajax-ui="cartWrapper"] not .sidebarright because cart_tape.php also 103 // renders a .sidebarright div (mobile tape) which querySelector would find f
103irst. 104 var html = resp.html_cart || (typeof resp.data === 'string' ? resp.data : null); 105 if (html) { 106 var sb = document.querySelector('[data-ajax-ui="cartWrapper"]'); if (sb) setHtml(sb, html); 107 } 108 if (resp.html) { 109 var mf = document.querySelector('.mobile_footer_tape'); if (mf) setHtml(mf, resp.html); 110 } 111 if (typeof resp.cart_qty !== 'undefined') { 112 document.querySelectorAll('[data-cart-qty]').forEach(function (e) { e.innerHTML = resp.cart_qty; }); 113 } 114 } 115 116 // Force-refresh the cart sidebar from the server, then show toast / reload. 117 function onCartDone(label) { 118 var onCheckout = /\/cart\/checkout/.test(window.location.pathname) 119 || !!document.querySelector('[data-cart-checkout-items-list]'); 120 if (onCheckout) { 121 location.reload(); 122 return; 123 } 124 // Non-checkout: refresh sidebar HTML from a dedicated endpoint so the panel 125 // always reflects the true session state, regardless of what html_cart the 126 // addToCart response carried. 127 // Prefer the explicit URL; fall back to deriving it from OA_DEAL_POPUP_URL so 128 // the refresh works even when a cached page predates the getCartHtml key. 129 var refreshUrl = (window.AJAX_URL && window.AJAX_URL.getCartHtml) 130 || (window.OA_DEAL_POPUP_URL && window.OA_DEAL_POPUP_URL.replace(/deal-popup(\?.*)?$/, 'cart-html')); 131 if (refreshUrl) { 132 var csrfKey = oaCsrfKey(); 133 var body = new URLSearchParams(); 134 body.set(csrfKey, oaCsrf()); 135 fetch(refreshUrl, { 136 method: 'POST', 137 credentials: 'same-origin', 138 headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' }, 139 body: body.toString() 140 }) 141 .then(function (r) { return r.text(); }) 142 .then(function (t) { 143 var resp = parseJsonResponse(t); 144 if (resp && resp.html_cart) { 145 var sb = document.querySelector('[data-ajax-ui="cartWrapper"]'); 146 if (sb) setHtml(sb, resp.html_cart); 147 } 148 }) 149 .catch(function () { /* sidebar stays as-is on network error */ }) 150 .finally(function () { 151 themeLoader(false); 152 toast((label || 'Deal') + ' added to cart'); 153 }); 154 } else { 155 themeLoader(false); 156 toast((label || 'Deal') + ' added to cart'); 157 } 158 } 159 function buildProductParams(s) { 160 var params = { id: s.productId, qty: 1, size: s.size || '' }; 161 if (s.included) { 162 Object.keys(s.included).forEach(function (iid) { 163 params['default_ingredients[' + iid + ']'] = 'on'; 164 params['default_ingredients_quantity[' + iid + ']'] = s.included[iid]; 165 }); 166 } 167 if (s.addons) { 168 Object.keys(s.addons).forEach(function (aid) { 169 if ((+s.addons[aid] || 0) > 0) { 170 params['add_on_ingredients[' + aid + ']'] = 'on'; 171 params['add_on_ingredients_quantity[' + aid + ']'] = s.addons[aid]; 172 } 173 }); 174 } 175 if (s.bases) { 176 if (s.basesGrouped) { 177 // Group-based ("new product config") products: gkey is a dynamic 178 // `bases_<group_id>` name, not a literal base1/2/3. Same dual-field 179 // pattern as default_ingredients/add_on_ingredients above â
180 // `bases_ingredients[id]` is what CartHelper::getSelectedProductAttributesPrice() 181 // prices from, `bases_ingredients_quantity[id]` is what 182 // CartHelper::getDishAttributes() persists into the cart row. 183 Object.keys(s.bases).forEach(function (gkey) { 184 if (s.bases[gkey]) { 185 params['bases_ingredients[' + s.bases[gkey] + ']'] = 'on'; 186 params['bases_ingredients_quantity[' + s.bases[gkey] + ']'] = 1; 187 } 188 }); 189 } else { 190 Object.keys(s.bases).forEach(function (gkey) { 191 if (s.bases[gkey]) { params[gkey + '_ingredients[' + s.bases[gkey] + ']'] = 'on'; } 192 }); 193 } 194 } 195 if (s.chilli) { params['chilli'] = s.chilli; } 196 return params; 197 } 198 199 // ââ Full deal add (new deal: parent + all children) ââ 200 function feedCart(d) { 201 if (!d.dealId) { toast((d.label || 'Deal') + ' added'); return; } 202 203 // Save selections so the popup can be pre-filled if a child is deleted later. 204 window.OA_CART_DEALS = window.OA_CART_DEALS || {}; 205 window.OA_CART_DEALS[String(d.dealId)] = { label: d.label, selection: (d.selection || []).slice() }; 206 207 themeLoader(true); 208 var chain = cartPost({ id: d.dealId, qty: 1, is_deal: 1 }).then(function (r) { 209 applyCart(r); 210 // Detect products auto-linked by the backend (getFirstProdUrlOnDealsAdd) so we 211 // don't post them again and create an orphaned standalone duplicate. 212 var alreadyLinked = {}; 213 var html = (r && r.html_cart) ? r.html_cart : ''; 214 if (html) { 215 var tmp = document.createElement('div'); 216 tmp.innerHTML = html; 217 var dealBlock = tmp.querySelector('[data-deal-id="' + d.dealId + '"]'); 218 if (dealBlock) { 219 dealBlock.querySelectorAll('[data-is-deal-child][data-product-id]').forEach(function (el) { 220 alreadyLinked[String(el.getAttribute('data-product-id'))] = true; 221 }); 222 } 223 } 224 return alreadyLinked; 225 }); 226 (d.selection || []).forEach(function (s) { 227 if (!s.productId) return; 228 // Auto-selected items are already added by the backend (DealsHelper::getFirstProdUrlOnDealsAdd). 229 if (s.autoSelected) return; 230 chain = chain.then(function (alreadyLinked) { 231 if (alreadyLinked[String(s.productId)]) { 232 return alreadyLinked; // already added by backend; skip to prevent standalone duplicate 233 } 234 return cartPost(buildProductParams(s)).then(function (r) { 235 applyCart(r); 236 return alreadyLinked; 237 }); 238 }); 239 }); 240 chain.then(function () { 241 onCartDone(d.label); 242 }).catch(function (e) { 243 themeLoader(false); 244 if (window.console) console.error('[oaDeal] cart feed failed', e); 245 toast('Could not add deal to cart'); 246 }); 247 } 248 249 // ââ Refill: only add the item(s) the user just picked for the empty slot ââ 250 // DealsHelper::dealsLogic scans all deal parents in the cart and automatically 251 // links any newly added item to an unfilled slot â no need to re-post the parent. 252 function addMissingDealChildren(d, remainingProductIds) { 253 var newItems = (d.selection || []).filter(function (s) { 254 return s.productId && !remainingProductIds[String(s.productId)]; 255 }); 256 if (newItems.length === 0) { toast((d.label || 'Deal') + ' complete'); return; } 257 258 // Update saved selection to reflect the now-complete deal 259 window.OA_CART_DEALS = window.OA_CART_DEALS || {}; 260 window.OA_CART_DEALS[String(d.dealId)] = { label: d.label, selection: (d.selection || []).slice() }; 261 262 themeLoader(true); 263 var chain = Promise.resolve(null); 264 newItems.forEach(function (s) { 265 chain = chain 266 .then(function () { return cartPost(buildProductParams(s)); }) 267 .then(function (r) { applyCart(r); return r; }); 268 }); 269 chain.then(function () { 270 onCartDone(d.label || 'Deal'); 271 }).catch(function (e) { 272 themeLoader(false); 273 if (window.console) console.error('[oaDeal] refill failed', e); 274 toast('Could not update deal'); 275 }); 276 } 277 278 // Fill each Hot-Deals card's price. 279 function fillDealPrices() { 280 if (!window.OA_DEALS) return; 281 document.querySelectorAll('[data-action="addToCartDeals"]').forEach(function (btn) { 282 var deal = window.OA_DEALS[btn.getAttribute('data-id')]; 283 if (!deal || deal.price == null) return; 284 var card = btn.closest('.oad-card'); 285 if (!card) return; 286 var priceStr = Number(deal.price).toFixed(2).replace(/\.00$/, ''); 287 var amt = card.querySelector('.oad-amt'); 288 if (amt && !/\d/.test(amt.textContent)) amt.textContent = priceStr; 289 var bp = card.querySelector('.oad-btnprice'); 290 if (bp && !/\d/.test(bp.textContent)) { 291 var sym = (bp.textContent || '$').replace(/[\s\d.]/g, '') || '$'; 292 bp.textContent = sym + priceStr; 293 } 294 }); 295 } 296 297 // Return the step index (0-based) that owns a given productId in a deal, or -1. 298 function findStepByProduct(dealData, productId) { 299 if (!dealData || !dealData.products || !dealData.steps) return -1; 300 var pid = String(productId); 301 for (var catId in dealData.products) { 302 var prods = dealData.products[catId]; 303 var found = false;
304 for (var i = 0; i < prods.length; i++) { if (String(prods[i].id) === pid) { found = true; break; } } 305 if (!found) continue; 306 for (var si = 0; si < dealData.steps.length; si++) { 307 var tiers = dealData.steps[si].tiers || []; 308 for (var ti = 0; ti < tiers.length; ti++) { 309 if (String(tiers[ti].category) === String(catId)) return si; 310 } 311 } 312 } 313 return -1; 314 } 315 316 function openPopup(dealKey, dealData, initialSelections, targetStepIdx) { 317 showFrame(); 318 var msg = { type: 'openDeal', dealId: dealKey }; 319 if (dealData) msg.deal = dealData; 320 if (initialSelections && initialSelections.length > 0) msg.initialSelections = initialSelections; 321 if (typeof targetStepIdx === 'number' && targetStepIdx >= 0) msg.targetStepIdx = targetStepIdx; 322 if (frameReady) frame.contentWindow.postMessage(msg, '*'); 323 else pendingMsg = msg; 324 } 325 326 // Bridge: listen for the popup app's lifecycle messages. 327 window.addEventListener('message', function (e) { 328 var d = e.data || {}; 329 if (d.source !== 'oaDealPopup') return; 330 if (d.type === 'ready') { 331 frameReady = true; 332 if (pendingMsg) { frame.contentWindow.postMessage(pendingMsg, '*'); pendingMsg = null; } 333 } else if (d.type === 'closed') { 334 hideFrame(); 335 var wasRefilling = !!pendingRefill; 336 pendingRefill = null; 337 if (wasRefilling) { 338 // User closed without picking a replacement â refresh sidebar so the removed 339 // child is no longer shown (applyCart in the remove flow may have injected 340 // stale html_cart; getCartHtml gives us a fresh render after session commits). 341 var refreshUrl = (window.AJAX_URL && window.AJAX_URL.getCartHtml) 342 || (window.OA_DEAL_POPUP_URL && window.OA_DEAL_POPUP_URL.replace(/deal-popup(\?.*)?$/, 'cart-html')); 343 if (refreshUrl) { 344 var rfBody = new URLSearchParams(); 345 rfBody.set(oaCsrfKey(), oaCsrf()); 346 fetch(refreshUrl, { 347 method: 'POST', credentials: 'same-origin', 348 headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' }, 349 body: rfBody.toString() 350 }) 351 .then(function(r) { return r.text(); }) 352 .then(function(t) { 353 var resp = parseJsonResponse(t); 354 if (resp && resp.html_cart) { var sb = document.querySelector('[data-ajax-ui="cartWrapper"]'); if (sb) setHtml(sb, resp.html_cart); } 355 }) 356 .catch(function() {}); 357 } 358 } 359 } else if (d.type === 'added') { 360 hideFrame(); 361 var refill = pendingRefill; 362 pendingRefill = null; 363 if (refill && String(refill.dealId) === String(d.dealId)) { 364 // Refill: only post the newly selected item â backend links it to the existing parent. 365 addMissingDealChildren(d, refill.remainingProductIds); 366 } else { 367 feedCart(d); 368 } 369 } 370 }); 371 372 // Capture-phase click intercepts â run before jQuery bubble handlers. 373 document.addEventListener('click', function (e) { 374 // ââ "Add to cart" for deals ââ 375 var btn = e.target && e.target.closest ? e.target.closest('[data-action="addToCartDeals"]') : null; 376 if (btn) { 377 e.preventDefault(); 378 e.stopImmediatePropagation(); 379 var dealId = btn.getAttribute('data-id'); 380 var real = (window.OA_DEALS && window.OA_DEALS[dealId]) ? window.OA_DEALS[dealId] : null; 381 if (real) { 382 openPopup(dealId, real); 383 } else { 384 var fetchUrl = window.AJAX_URL && window.AJAX_URL.getDealData; 385 if (fetchUrl) { 386 themeLoader(true); 387 fetch(fetchUrl + '?id=' + encodeURIComponent(dealId), { 388 credentials: 'same-origin', 389 headers: { 'X-Requested-With': 'XMLHttpRequest' } 390 }) 391 .then(function (r) { return r.json(); }) 392 .then(function (data) { 393 themeLoader(false); 394 if (data && !data.error) { 395 window.OA_DEALS = window.OA_DEALS || {}; 396 window.OA_DEALS[dealId] = data; 397 openPopup(dealId, data); 398 } else { 399 var fb = btn.closest('form') && btn.closest('form').querySelector('[id^="product_redirect_"]'); 400 if (fb && fb.getAttribute('href')) window.location.href = fb.getAttribute('href'); 401 } 402 }) 403 .catch(function () { 404 themeLoader(false); 405 var fb = btn.closest('form') && btn.closest('form').querySelector('[id^="product_redirect_"]'); 406 if (fb && fb.getAttribute('href')) window.location.href = fb.getAttribute('href'); 407 }); 408 } else { 409 var btns = Array.prototype.slice.call(document.querySelectorAll('[data-action="addToCartDeals"]')); 410 var idx = btns.indexOf(btn); 411 openPopup(SAMPLE_KEYS[(idx >= 0 ? idx : 0) % SAMPLE_KEYS.length]); 412 } 413 } 414 return; 415 } 416 417 // ââ "Add Item" link for an unfilled deal slot â open popup instead of old route ââ 418 var addItemLink = e.target && e.target.closest ? e.target.closest('[data-add-deal-item]') : null; 419 if (addItemLink) { 420 var dealContainer = addItemLink.closest('[data-deal-id]'); 421 if (!dealContainer) return; // not inside a known deal block; let the default handler run 422 423 e.preventDefault(); 424 e.stopImmediatePropagation(); 425 426 var dealId = String(dealContainer.getAttribute('data-deal-id')); 427 428 // Build remaining selections: prefer in-memory data (includes customizations), 429 // fall back to DOM child buttons (product-id + size-id only). 430 var remaining = []; 431 var remainingProductIds = {}; 432 var saved = window.OA_CART_DEALS && window.OA_CART_DEALS[dealId]; 433 if (saved && (saved.selection || []).length > 0) { 434 remaining = saved.selection; 435 remaining.forEach(function (s) { remainingProductIds[String(s.productId)] = true; }); 436 } else { 437 dealContainer.querySelectorAll('[data-is-deal-child][data-product-id]').forEach(function (btn) { 438 var pid = btn.getAttribute('data-product-id'); 439 if (pid && !remainingProductIds[pid]) { 440 remaining.push({ productId: pid, size: btn.getAttribute('data-size-id') || '' }); 441 remainingProductIds[pid] = true; 442 } 443 }); 444 } 445 446 pendingRefill = { dealId: dealId, remainingProductIds: remainingProductIds }; 447 448 var dealData = window.OA_DEALS && window.OA_DEALS[dealId]; 449 var targetCatId = String(addItemLink.getAttribute('data-step-category') || ''); 450 function findStepIdx(d) { 451 var idx = -1; 452 if (targetCatId && d.steps) { 453 d.steps.forEach(function (step, si) { 454 if (idx >= 0) return;
455 (step.tiers || []).forEach(function (t) { if (String(t.category) === targetCatId) idx = si; }); 456 }); 457 } 458 return idx; 459 } 460 if (dealData) { 461 openPopup(dealId, dealData, remaining, findStepIdx(dealData)); 462 } else { 463 // OA_DEALS not populated (different page) â fetch lazily then open popup 464 var fetchUrl = window.AJAX_URL && window.AJAX_URL.getDealData; 465 if (fetchUrl) { 466 themeLoader(true); 467 fetch(fetchUrl + '?id=' + encodeURIComponent(dealId), { 468 credentials: 'same-origin', 469 headers: { 'X-Requested-With': 'XMLHttpRequest' } 470 }) 471 .then(function (r) { return r.json(); }) 472 .then(function (data) { 473 themeLoader(false); 474 if (data && !data.error) { 475 window.OA_DEALS = window.OA_DEALS || {}; 476 window.OA_DEALS[dealId] = data; 477 openPopup(dealId, data, remaining, findStepIdx(data)); 478 } else { 479 window.location.href = addItemLink.getAttribute('data-fallback-url') || addItemLink.getAttribute('href'); 480 } 481 }) 482 .catch(function () { 483 themeLoader(false); 484 pendingRefill = null; 485 window.location.href = addItemLink.getAttribute('data-fallback-url') || addItemLink.getAttribute('href'); 486 }); 487 } else { 488 pendingRefill = null; 489 window.location.href = addItemLink.getAttribute('data-fallback-url') || addItemLink.getAttribute('href'); 490 } 491 } 492 return; 493 } 494 495 // ââ "Remove" for deal child items â delete it, reopen popup to pick a replacement ââ 496 var removeBtn = e.target && e.target.closest ? e.target.closest('[data-ajax="removeCartItem"][data-is-deal-child]') : null; 497 if (!removeBtn) return; 498 499 var dealContainer = removeBtn.closest('[data-deal-id]'); 500 if (!dealContainer) return; 501 502 e.preventDefault(); 503 e.stopImmediatePropagation(); // prevent products-page.998.js location.reload() 504 505 var dealId = String(dealContainer.getAttribute('data-deal-id')); 506 var deletedProductId = String(removeBtn.getAttribute('data-product-id') || ''); 507 508 // Build the set of product IDs that will remain in cart after this deletion. 509 // Prefer in-memory OA_CART_DEALS (includes full customisation state); fall back to 510 // DOM scan of the deal block (works after a page refresh when OA_CART_DEALS is unset). 511 var saved = window.OA_CART_DEALS && window.OA_CART_DEALS[dealId]; 512 var remaining; 513 var remainingProductIds = {}; 514 if (saved && (saved.selection || []).length > 0) { 515 remaining = (saved.selection || []).filter(function (s) { 516 return String(s.productId) !== deletedProductId; 517 }); 518 } else { 519 // Read sibling children from the live DOM BEFORE applyCart replaces it. 520 remaining = []; 521 dealContainer.querySelectorAll('[data-is-deal-child][data-product-id]').forEach(function (el) { 522 var pid = el.getAttribute('data-product-id'); 523 if (pid && pid !== deletedProductId && !remainingProductIds[pid]) { 524 remaining.push({ productId: pid, size: el.getAttribute('data-size-id') || '' }); 525 remainingProductIds[pid] = true; 526 } 527 }); 528 } 529 remaining.forEach(function (s) { remainingProductIds[String(s.productId)] = true; }); 530 531 // POST the removal. 532 var urlParams = new URLSearchParams(removeBtn.getAttribute('data-params') || ''); 533 urlParams.set(oaCsrfKey(), oaCsrf()); 534 535 themeLoader(true); 536 fetch(removeItemUrl(), { 537 method: 'POST', 538 credentials: 'same-origin', 539 headers: { 'X-Requested-With': 'XMLHttpRequest', 'Content-Type': 'application/x-www-form-urlencoded' }, 540 body: urlParams.toString() 541 }) 542 .then(function (r) { return r.text(); }) 543 .then(function (t) { 544 applyCart(parseJsonResponse(t)); 545 546 // Update saved selection to reflect the current cart state. 547 if (saved) { 548 window.OA_CART_DEALS[dealId] = { label: saved.label, selection: remaining }; 549 } 550 551 themeLoader(false); 552 553 // Store refill context: next "added" event posts only the new item, not a whole new deal. 554 pendingRefill = { dealId: dealId, remainingProductIds: remainingProductIds }; 555 556 var dealData = window.OA_DEALS && window.OA_DEALS[dealId]; 557 if (dealData) { 558 // Open at the step the deleted product belongs to so the user can pick a replacement 559 // for that specific slot instead of always starting at Step 1. 560 openPopup(dealId, dealData, remaining, findStepByProduct(dealData, deletedProductId)); 561 } else { 562 var fetchUrl = window.AJAX_URL && window.AJAX_URL.getDealData; 563 if (fetchUrl) { 564 fetch(fetchUrl + '?id=' + encodeURIComponent(dealId), { 565 credentials: 'same-origin', 566 headers: { 'X-Requested-With': 'XMLHttpRequest' } 567 }) 568 .then(function (r) { return r.json(); }) 569 .then(function (data) { 570 if (data && !data.error) { 571 window.OA_DEALS = window.OA_DEALS || {}; 572 window.OA_DEALS[dealId] = data; 573 } 574 var fetched = (data && !data.error) ? data : null; 575 openPopup(dealId, fetched, remaining, findStepByProduct(fetched, deletedProductId)); 576 }) 577 .catch(function () { openPopup(dealId, null, remaining); }); 578 } else { 579 openPopup(dealId, null, remaining); 580 } 581 } 582 }) 583 .catch(function (err) { 584 themeLoader(false); 585 pendingRefill = null; 586 if (window.console) console.error('[oaDeal] remove-child failed', err); 587 location.reload(); 588 }); 589 }, true); 590
591 function oaInit() { injectFrame(); fillDealPrices(); } 592 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', oaInit); 593 else oaInit(); 594})();
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.