PageSourceSearch

https://pizzeria-planeta.pl/js/product-modal.js

js pizzeria-planeta.pl collected 2026-10-08 07:35:19 UTC 22,703 bytes, 672 lines download raw bytes

1import { getMenu, getConfig, formatPrice, getTagClass, getTagLabel } from './config.js';
2import { getIngredients, getIngredientPrice } from './ingredients.js';
3import { formatAllergenNumbersLabel, getItemAllergenNumbers } from './allergens.js';
4import { addToCart } from './cart.js';
5import { showToast } from './toast.js';
6import { applyDishOfTheDayToBase, getDishOfTheDay, formatDishDiscountLabel } from './pizza-of-the-day.js';
7import {
8  applyPercentOfferToPrice,
9  getBestPublicPercentOffer,
10  getDishOfTheDayDisplayPricing
11} from './promotions.js';
12import { t } from './i18n.js';
13import { resolvePublicPath, getLocale } from './locale.js';
14import { trackAbEvent, trackAbImpression } from './ab-tests.js';
15import {
16  applyFreeSaucesToExtras,
17  isSauceIngredientCategory,
18  resolveFreeSaucesPolicy
19} from './free-sauces.js';
20
21const POPUP_TRIGGER_KEYS = ['before_order', 'after_add_to_cart', 'open_cart'];
22
23let modalInitialized = false;
24let pendingOrderSubmit = null;
25let currentModalSession = null;
26
27function isCartDrawerOpen() {
28  return document.getElementById('cart-drawer')?.classList.contains('open') ?? false;
29}
30
31function restoreBodyScroll() {
32  document.body.style.overflow = isCartDrawerOpen() ? 'hidden' : '';
33}
34
35function clearModalSession() {
36  currentModalSession = null;
37}
38
39function closeModal() {
40  const { backdrop, modal, body, footer, title: titleEl } = getModalElements();
41  if (backdrop) backdrop.hidden = true;
42  if (modal) modal.hidden = true;
43  if (body) body.innerHTML = '';
44  if (footer) footer.innerHTML = '';
45  if (titleEl) titleEl.textContent = '';
46  restoreBodyScroll();
47}
48
49function dismissModalSession(action = 'abort') {
50  const session = currentModalSession;
51  if (!session || session.completed) {
52    closeModal();
53    clearModalSession();
54    return;
55  }
56
57  if (session.type === 'customize' && session.parentSession) {
58    const parent = session.parentSession;
59    parent.completed = false;
60    currentModalSession = parent;
61    renderUpsellModal(parent);
62    if (action === 'abort') {
63      session.onClose?.();
64    }
65    return;
66  }
67
68  session.completed = true;
69  currentModalSession = null;
70  closeModal();
71
72  if (session.type === 'upsell') {
73    if (action === 'continue') {
74      session.onContinue?.();
75    }
76    session.resolve?.(action === 'continue');
77    return;
78  }
79
80  session.onClose?.();
81}
82
83function restoreUpsellAfterCustomize(options = {}, event = 'cancelled') {
84  const parent = currentModalSession?.parentSession;
85  if (!parent || parent.type !== 'upsell') return false;
86
87  parent.completed = false;
88  currentModalSession = parent;
89  renderUpsellModal(parent);
90
91  if (event === 'added') {
92    options.onAdded?.();
93  } else {
94    options.onCancelled?.();
95  }
96  return true;
97}
98
99export function getPopupTriggers() {
100  return Object.fromEntries(
101    POPUP_TRIGGER_KEYS.map(key => [key, t(`modal.trigger.${key}`)])
102  );
103}
104
105function getAbMergedPopup(trigger) {
106  const config = getConfig();
107  const ab = config?._abProductPopup;
108  if (!ab || ab.trigger !== trigger) return null;
109  const base = (Array.isArray(config.popups) ? config.popups : []).find((p) => p.enabled !== false);
110  if (!base) return null;
111  const english = getLocale() === 'en';
112  return {
113    ...base,
114    trigger,
115    title: (english ? ab.titleEn : ab.titlePl) || base.title,
116    subtitle: (english ? ab.subtitleEn : ab.subtitlePl) || base.subtitle
117  };
118}
119
120export function getPopupsForTrigger(trigger) {
121  const abPopup = getAbMergedPopup(trigger);
122  if (abPopup) return [abPopup];
123  const popups = getConfig()?.popups;
124  if (!Array.isArray(popups)) return [];
125  return popups.filter(popup => popup.enabled !== false && popup.trigger === trigger);
126}
127
128export function findMenuItemById(itemId) {
129  const menu = getMenu();
130  for (const category of menu.categories || []) {
131    const found = (category.items || []).find(item => item.id === itemId);
132    if (found) return { ...found, categoryId: category.id, categoryName: category.name };
133  }
134  return null;
135}
136
137export function getAllMenuItemsFlat() {
138  return (getMenu().categories || [])
139    .filter(cat => cat.id !== 'bestsellery')
140    .flatMap(cat => (cat.items || []).map(item => ({
141      ...item,
142      categoryId: cat.id,
143      categoryName: cat.name
144    }
144)));
145}
146
147function escapeHtml(value) {
148  return String(value)
149    .replaceAll('&', '&')
150    .replaceAll('<', '&lt;')
151    .replaceAll('>', '&gt;')
152    .replaceAll('"', '&quot;');
153}
154
155function getModalElements() {
156  return {
157    backdrop: document.getElementById('product-modal-backdrop'),
158    modal: document.getElementById('product-modal'),
159    title: document.getElementById('product-modal-title'),
160    body: document.getElementById('product-modal-body'),
161    footer: document.getElementById('product-modal-footer'),
162    closeBtn: document.getElementById('product-modal-close')
163  };
164}
165
166function openModal(title, bodyHtml, footerHtml) {
167  const { backdrop, modal, title: titleEl, body, footer } = getModalElements();
168  if (!backdrop || !modal || !body || !footer) return;
169  if (titleEl) titleEl.textContent = title;
170  body.innerHTML = bodyHtml;
171  footer.innerHTML = footerHtml;
172  backdrop.hidden = false;
173  modal.hidden = false;
174  document.body.style.overflow = 'hidden';
175}
176
177function bindUpsellActions(session) {
178  const { popup, items } = session;
179
180  document.getElementById('product-modal-skip')?.addEventListener('click', (e) => {
181    e.preventDefault();
182    dismissModalSession('continue');
183  }, { once: true });
184
185  document.getElementById('product-modal-continue')?.addEventListener('click', (e) => {
186    e.preventDefault();
187    void trackAbEvent('product_popup', 'click');
188    dismissModalSession('continue');
189  }, { once: true });
190
191  document.querySelectorAll('[data-upsell-add]').forEach(btn => {
192    btn.addEventListener('click', () => {
193      const item = findMenuItemById(btn.dataset.upsellAdd);
194      if (!item) return;
195
196      if (item.allowExtras || item.allowSaucesOnly || item.categoryId === 'pizza' || item.sizes?.length) {
197        openCustomizeModal(item);
198        return;
199      }
200
201      addToCart({
202        id: item.id,
203        name: item.name,
204        size: null,
205        price: calculateItemCartPrice(item, null, []),
206        image: item.image,
207        extras: []
208      });
209      showToast(t('modal.addedShortToast', { name: item.name }));
210      renderUpsellModal(session);
211    });
212  });
213}
214
215function renderUpsellModal(session) {
216  const { popup, items } = session;
217  session.completed = false;
218  currentModalSession = session;
219
220  openModal(
221    popup.title || t('modal.upsellTitle'),
222    renderUpsellBody(popup, items),
223    `
224      <button type="button" class="btn btn-secondary" id="product-modal-skip">${escapeHtml(t('modal.skip'))}</button>
225      <button type="button" class="btn btn-primary" id="product-modal-continue">${escapeHtml(t('modal.continue'))}</button>
226    `
227  );
228
229  trackAbImpression('product_popup');
230  bindUpsellActions(session);
231}
232
233export function initProductModal() {
234  if (modalInitialized) return;
235  modalInitialized = true;
236
237  const { backdrop, closeBtn } = getModalElements();
238  if (closeBtn) closeBtn.setAttribute('aria-label', t('modal.close'));
239
240  backdrop?.addEventListener('click', () => {
241    if (currentModalSession?.type === 'customize' && currentModalSession.parentSession) {
242      dismissModalSession('abort');
243      return;
244    }
245    if (currentModalSession?.type === 'upsell') {
246      dismissModalSession(currentModalSession.trigger === 'before_order' ? 'continue' : 'abort');
247      return;
248    }
249    dismissModalSession('abort');
250  });
251
252  closeBtn?.addEventListener('click', (e) => {
253    e.preventDefault();
254    e.stopPropagation();
255    if (currentModalSession?.type === 'customize' && currentModalSession.parentSession) {
256      dismissModalSession('abort');
257      return;
258    }
259    if (currentModalSession?.type === 'upsell') {
260      dismissModalSession(currentModalSession.trigger === 'before_order' ? 'continue' : 'abort');
261      return;
262    }
263    dismissModalSession('abort');
264  });
265
266  document.addEventListener('keydown', (e) => {
267    const { modal } = getModalElements();
268    if (e.key !== 'Escape' || !modal || modal.hidden) return;
269    if (currentModalSession?.type === 'customize' && currentModalSession.parentSession) {
270      dismissModalSession('abort');
271      return;
272    }
273    if (currentModalSession?.type === 'upsell') {
274      dismissModalSession(currentModalSession.trigger === 'before_order' ? 'continue' : 'abort');
275      return;
276    }
277    dismissModalSession('abort');
278  });
279
280  closeModal();
281  clearModalSession();
282}
283
284function getCatalogUnitPrice(menuItem, sizeName) {
285  let price = Number(menuItem.price || 0);
286  const size = (menuItem.sizes || []).find(s => s.name === sizeName);
287  if (size) price += Number(size.priceModifier || 0);
288  return Math.round(price * 100) / 100;
289}
290
291/** Cena zapisywana w koszyku: danie dnia na pozycji; happy hours nie sumuje się z daniem dnia. */
292function calculateItemCartPrice(menuItem, sizeName, selectedExtras) {
293  let price = getCatalogUnitPrice(menuItem, sizeName);
294  price = applyDishOfTheDayToBase(
295    price,
296    menuItem.id,
297    getMenu(),
298    getConfig()?.promotions || {}
299  );
300  for (const extra of selectedExtras) {
301    price += Number(extra.price || 0);
302  }
303  return Math.round(price * 100) / 100;
304}
305
306/** Cena w modalu „Łącznie”: danie dnia zastępuje happy hours; inaczej publiczny %. */
307function calculateItemDisplayPrice(menuItem, sizeName, selectedExtras) {
308  const catalog = getCatalogUnitPrice(menuItem, sizeName);
309  const promotions = getConfig()?.promotions || {};
310  const menu = getMenu();
311  const dishDisplay = getDishOfTheDayDisplayPricing(catalog, menuItem.id, menu, promotions);
312  let unit = catalog;
313  if (dishDisplay.dishApplied) {
314    unit = dishDisplay.salePrice;
315  } else {
316    const autoOffer = getBestPublicPercentOffer(promotions, { subtotal: catalog });
317    unit = applyPercentOfferToPrice(catalog, autoOffer);
318  }
319  let price = unit;
320  for (const extra of selectedExtras) {
321    price += Number(extra.price || 0);
322  }
323  return Math.round(price * 100) / 100;
324}
325
326function resolveInitialSize(menuItem, initialSizeName) {
327  const sizes = menuItem.sizes || [];
328  if (!sizes.length) return null;
329  if (initialSizeName) {
330    const found = sizes.find(size => size.name === initialSizeName);
331    if (found) return found;
332  }
333  return sizes[1] || sizes[0];
334}
335
336function renderCustomizeBody(menuItem, initialSizeName = null) {
337  const ingredients = getIngredients();
338  const allowAllExtras = menuItem.allowExtras || menuItem.categoryId === 'pizza';
339  const allowSaucesOnly = !allowAllExtras && menuItem.allowSaucesOnly === true;
340  const allowAnyExtras = allowAllExtras || allowSaucesOnly;
341  const hasSizes = Array.isArray(menuItem.sizes) && menuItem.sizes.length > 0;
342  const defaultSize = hasSizes ? resolveInitialSize(menuItem, initialSizeName) : null;
343
344  const sizesHtml = hasSizes ? `
345    <div class="product-modal__section">
346      <p class="product-modal__label">${escapeHtml(t('modal.size'))}</p>
347      <div class="product-modal__sizes">
348        ${menuItem.sizes.map((size, i) => `
349          <button type="button" class="product-modal__size-btn ${size.name === defaultSize?.name ? 'is-active' : ''}"
350            data-size="${escapeHtml(size.name)}" data-modifier="${size.priceModifier ?? 0}">
351            ${escapeHtml(size.name)}
352          </button>
353        `).join('')}
354      </div>
355    </div>
356  ` : '';
357
358  const extrasHtml = allowAnyExtras ? (ingredients.categories || [])
359    .filter(cat => allowAllExtras || isSauceIngredientCategory(cat.id, cat.name))
360    .map(cat => ({
361      ...cat,
362      items: (cat.items || []).filter(extra => extra.enabled !== false)
363    }))
364    .filter(cat => cat.items.length)
365    .map(cat => `
366    <div class="product-modal__section">
367      <p class="product-modal__label">${escapeHtml(cat.name)}</p>
368      <div class="product-modal__extras">
369        ${cat.items.map(extra => {
370          const price = getIngredientPrice(extra, defaultSize?.name || null);
371          return `
372          <label class="product-modal__extra">
373            <input type="checkbox" name="extra" value="${escapeHtml(extra.id)}"
374              data-name="${escapeHtml(extra.name)}"
375              data-category-id="${escapeHtml(cat.id || '')}"
376              data-category-name="${escapeHtml(cat.name || '')}"
377              data-price="${price}"
378              data-price-30="${getIngredientPrice(extra, '30 cm')}"
379              data-price-40="${getIngredientPrice(extra, '40 cm')}"
380              data-price-50="${getIngredientPrice(extra, '50 cm')}">
381            <span>${escapeHtml(extra.name)}</span>
382            <strong class="product-modal__extra-price">+${formatPrice(price)}</strong>
383          </label>
384        `;
385        }).join('')}
386      </div>
387    </div>
388  `).join('') : '';
389
390  return `
391    <div class="product-modal__product">
392      ${menuItem.image ? `<img src="${resolvePublicPath(menuItem.image)}" alt="${escapeHtml(menuItem.name)}" class="product-modal__image">` : ''}
393      <div>
394        <h4>${escapeHtml(menuItem.name)}${(() => {
395          const dish = getDishOfTheDay(getMenu(), getConfig()?.promotions || {});
396          if (dish?.item?.id !== menuItem.id) return '';
397          const priced = getDishOfTheDayDisplayPricing(
398            getCatalogUnitPrice(menuItem, defaultSize?.name || null),
399            menuItem.id,
400            getMenu(),
401            getConfig()?.promotions || {}
402          );
403          const label = formatDishDiscountLabel(dish, getLocale(), priced.autoOffer?.value || 0);
404          return ` <span class="badge ${getTagClass('danie-dnia')}">${escapeHtml(getTagLabel('danie-dnia'))} ${escapeHtml(label)}</span>`;
405        })()}</h4>
406        ${menuItem.description ? `<p>${escapeHtml(menuItem.description)}</p>` : ''}
407        ${(() => {
408          const nums = getItemAllergenNumbers(menuItem.id);
409          if (!nums.length) return '';
410          return `<p class="product-modal__allergens">${escapeHtml(t('menu.allergens'))}: ${escapeHtml(formatAllergenNumbersLabel(nums))}</p>`;
411        })()}
412      </div>
413    </div>
414    ${sizesHtml}
415    ${extrasHtml}
416    <div class="product-modal__price-row">
417      <span>${escapeHtml(t('modal.total'))}</span>
418      <strong id="product-modal-total">${formatPrice(calculateItemDisplayPrice(menuItem, defaultSize?.name || null, []))}</strong>
419    </div>
420  `;
421}
422
423function bindCustomizeModal(menuItem, options = {}) {
424  const modal = document.getElementById('product-modal');
425  if (!modal) return;
426
427  const totalEl = modal.querySelector('#product-modal-total');
428  const hasSizes = Array.isArray(menuItem.sizes) && menuItem.sizes.length > 0;
429  let selectedSize = hasSizes ? resolveInitialSize(menuItem, options.initialSize)?.name : null;
430
431  function applyFreeSaucesPricing(extras) {
432    const policy = resolveFreeSaucesPolicy(getMenu(), menuItem.id, selectedSize);
433    return applyFreeSaucesToExtras(extras, {
434      maxFreeSauces: policy.maxFreeSauces,
435      isSauceExtra: (extra) => isSauceIngredientCategory(extra.categoryId, extra.categoryName)
436    });
437  }
438
439  function getSelectedExtras() {
440    const selected = Array.from(modal.querySelectorAll('input[name="extra"]:checked')).map(input => ({
441      id: input.value,
442      name: input.dataset.name || '',
443      price: Number(input.dataset.price) || 0,
444      categoryId: input.dataset.categoryId || '',
445      categoryName: input.dataset.categoryName || ''
446    }));
447    return applyFreeSaucesPricing(selected).map((extra) => ({
448      id: extra.id,
449      name: extra.name,
450      price: extra.price
451    }));
452  }
453
454  function isSauceInput(input) {
455    return isSauceIngredientCategory(input?.dataset?.categoryId || '', input?.dataset?.categoryName || '');
456  }
457
458  function refreshExtraPriceLabels() {
459    const allInputs = Array.from(modal.querySelectorAll('input[name="extra"]'));
460    if (!allInputs.length) return;
461
462    const policy = resolveFreeSaucesPolicy(getMenu(), menuItem.id, selectedSize);
463    let freeSlotsLeft = Math.max(0, Number(policy.maxFreeSauces) || 0);
464    const freeCheckedInputs = new Set();
465
466    const checkedSauceInputs = allInputs.filter((input) => input.checked && isSauceInput(input));
467    checkedSauceInputs.forEach((input) => {
468      if (freeSlotsLeft <= 0) return;
469      freeCheckedInputs.add(input);
470      freeSlotsLeft -= 1;
471    });
472
473    allInputs.forEach((input) => {
474      const priceEl = input.closest('.product-modal__extra')?.querySelector('.product-modal__extra-price');
475      if (!priceEl) return;
476
477      const basePrice = Math.max(0, Number(input.dataset.price) || 0);
478      let showFree = false;
479
480      if (isSauceInput(input)) {
481        if (input.checked) {
482          showFree = freeCheckedInputs.has(input);
483        } else {
484          showFree = freeSlotsLeft > 0;
485        }
486      }
487
488      priceEl.textContent = showFree ? t('cart.free') : `+${formatPrice(basePrice)}`;
489    });
490  }
491
492  function syncExtrasPricesForSize(sizeName) {
493    const key = String(sizeName || '').match(/(\d+)/)?.[1] || '40';
494    modal.querySelectorAll('input[name="extra"]').forEach(input => {
495      // data-price-30 nie mapuje się na dataset.price30 (cyfra po myślniku) — używaj getAttribute
496      const sizedAttr = input.getAttribute(`data-price-${key}`);
497      const nextPrice = Number(sizedAttr != null && sizedAttr !== '' ? sizedAttr : input.dataset.price) || 0;
498      input.dataset.price = String(nextPrice);
499    });
500    refreshExtraPriceLabels();
501  }
502
503  function updateTotal() {
504    const total = calculateItemDisplayPrice(menuItem, selectedSize, getSelectedExtras());
505    if (totalEl) totalEl.textContent = formatPrice(total);
506  }
507
508  modal.querySelectorAll('.product-modal__size-btn').forEach(btn => {
509    btn.addEventListener('click', () => {
510      modal.querySelectorAll('.product-modal__size-btn').forEach(b => b.classList.remove('is-active'));
511      btn.classList.add('is-active');
512      selectedSize = btn.dataset.size || null;
513      syncExtrasPricesForSize(selectedSize);
514      updateTotal();
515    });
516  });
517
518  syncExtrasPricesForSize(selectedSize);
519
520  modal.querySelectorAll('input[name="extra"]').forEach(input => {
521    input.addEventListener('change', () => {
522      refreshExtraPriceLabels();
523      updateTotal();
524    });
525  });
526
527  modal.querySelector('#product-modal-confirm')?.addEventListener('click', () => {
528    const extras = getSelectedExtras();
529    const price = calculateItemCartPrice(menuItem, selectedSize, extras);
530
531    addToCart({
532      id: menuItem.id,
533      name: menuItem.name,
534      size: selectedSize,
535      price,
536      image: menuItem.image,
537      extras
538    });
539
540    showToast(t('modal.addedToast', { name: menuItem.name }));
541    if (restoreUpsellAfterCustomize(options, 'added')) return;
542    dismissModalSession('abort');
543    options.onAdded?.();
544    maybeShowUpsell('after_add_to_cart');
545  }, { once: true });
546
547  modal.querySelector('#product-modal-cancel')?.addEventListener('click', (e) => {
548    e.preventDefault();
549    if (restoreUpsellAfterCustomize(options, 'cancelled')) return;
550    dismissModalSession('abort');
551    options.onCancelled?.();
552  }, { once: true });
553}
554
555export function openCustomizeModal(menuItem, options = {}) {
556  initProductModal();
557  const allowAllExtras = menuItem.allowExtras || menuItem.categoryId === 'pizza';
558  const allowAnyExtras = allowAllExtras || menuItem.allowSaucesOnly === true;
559  const needsModal = allowAnyExtras || (menuItem.sizes?.length > 0);
560  if (!needsModal) {
561    addToCart({
562      id: menuItem.id,
563      name: menuItem.name,
564      size: null,
565      price: calculateItemCartPrice(menuItem, null, []),
566      image: menuItem.image,
567      extras: []
568    });
569    showToast(t('modal.addedToast', { name: menuItem.name }));
570    options.onAdded?.();
571    maybeShowUpsell('after_add_to_cart');
572    return;
573  }
574
575  const parentSession = currentModalSession?.type === 'upsell' ? currentModalSession : null;
576
577  currentModalSession = {
578    type: 'customize',
579    completed: false,
580    parentSession,
581    onClose: options.onCancelled
582  };
583
584  openModal(
585    allowAnyExtras ? t('modal.chooseExtras') : t('modal.chooseOption'),
586    renderCustomizeBody(menuItem, options.initialSize),
587    `
588      <button type="button" class="btn btn-secondary" id="product-modal-cancel">${escapeHtml(t('modal.cancel'))}</button>
589      <button type="button" class="btn btn-primary" id="product-modal-confirm">${escapeHtml(t('modal.addToCart'))}</button>
590    `
591  );
592
593  bindCustomizeModal(menuItem, options);
594}
595
596function renderUpsellBody(popup, items) {
597  return `
598    <p class="product-modal__intro">${escapeHtml(popup.subtitle || t('modal.upsellSubtitle'))}</p>
599    <div class="product-modal__upsell-grid">
600      ${items.map(item => `
601        <article class="product-modal__upsell-card">
602          ${item.image ? `<img src="${resolvePublicPath(item.image)}" alt="${escapeHtml(item.name)}">` : '<div class="product-modal__upsell-card-thumb" aria-hidden="true"></div>'}
603          <div class="product-modal__upsell-card-body">
604            <strong class="product-modal__upsell-card-name">${escapeHtml(item.name)}</strong>
605            <span class="product-modal__upsell-card-price">${formatPrice(calculateItemDisplayPrice(item, null, []))}</span>
606          </div>
607          <button type="button" class="btn btn-primary btn-sm product-modal__upsell-add" data-upsell-add="${escapeHtml(item.id)}">${escapeHtml(t('modal.add'))}</button>
608        </article>
609      `).join('')}
610    </div>
611  `;
612}
613
614export function showUpsellPopup(trigger, onContinue) {
615  const popups = getPopupsForTrigger(trigger);
616  if (!popups.length) {
617    onContinue?.();
618    return Promise.resolve(false);
619  }
620
621  const popup = popups[0];
622  const items = (popup.itemIds || [])
623    .map(id => findMenuItemById(id))
624    .filter(item => item && item.enabled !== false);
625
626  if (!items.length) {
627    onContinue?.();
628    return Promise.resolve(false);
629  }
630
631  initProductModal();
632
633  if (currentModalSession?.type === 'upsell' && currentModalSession.trigger === trigger) {
634    dismissModalSession('abort');
635  }
636
637  return new Promise(resolve => {
638    const session = {
639      type: 'upsell',
640      trigger,
641      popup,
642      items,
643      onContinue,
644      resolve,
645      completed: false
646    };
647
648    currentModalSession = session;
649    renderUpsellModal(session);
650  });
651}
652
653export function maybeShowUpsell(trigger) {
654  const popups = getPopupsForTrigger(trigger);
655  if (!popups.length) return;
656
657  const popup = popups[0];
658  const items = (popup.itemIds || [])
659    .map(id => findMenuItemById(id))
660    .filter(item => item && item.enabled !== false);
661  if (!items.length) return;
662
663  showUpsellPopup(trigger);
664}
665
666export function interceptOrderSubmit(submitFn) {
667  pendingOrderSubmit = submitFn;
668  showUpsellPopup('before_order', () => {
669    pendingOrderSubmit?.();
670    pendingOrderSubmit = null;
671  });
672}

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.