const TapProductSample = {
settings: {
selectors: {
root: '.js__tap-product-sample',
button: '.js__tap-product-sample-button',
nativeBuyButton: 'buy-button',
basketProduct: '.basket-product',
basketStepper: 'h-input-stepper',
basketStepperButton: 'h-button-stepper',
basketQuantityInput: '.input-stepper__value_input'
},
classes: {
loading: 'tap-product-sample__button_is-loading',
modalActive: 'tap-product-sample-modal_is-active'
},
storefront: null,
basketUpdaterApi: null,
basketProductsApi: null,
basketProducts: [],
basketWatcherBound: false,
basketApisPromise: null,
basketDomObserver: null,
basketDomObserverQueued: false,
basketInteractionGuardBound: false,
protectionStyleInjected: false
},
init: (storefront) => {
TapProductSample.settings.storefront = storefront;
TapProductSample.injectProtectionStyles();
TapProductSample.bindBasketInteractionGuard();
TapProductSample.ensureBasketDomObserver();
TapProductSample.load();
// Prewarmujemy koszyk tylko tam, gdzie moduł może go potrzebować.
// Rejestracja dynamiczna zachowuje możliwość pierwszego dodania próbki
// również wtedy, gdy użytkownik nie ma jeszcze sesji koszyka.
if (TapProductSample.shouldPrepareBasketApis()) {
TapProductSample.ensureBasketApis().catch((error) => {
console.warn('[TapProductSample] Basket prewarm failed.', error);
});
}
if (storefront.eventBus && typeof storefront.eventBus.on === 'function') {
storefront.eventBus.on('basket.initialized', () => {
TapProductSample.ensureBasketApis().catch((error) => {
console.warn(
'[TapProductSample] Basket API resolve after basket.initialized failed.',
error
);
});
TapProductSample.scheduleBasketDomProtection();
});
}
},
shouldPrepareBasketApis: () => {
return Boolean(
document.querySelector(TapProductSample.settings.selectors.root) ||
window.location.pathname.includes('/basket')
);
},
ensureBasketApis: async () => {
if (
TapProductSample.settings.basketUpdaterApi &&
TapProductSample.settings.basketProductsApi
) {
return true;
}
if (TapProductSample.settings.basketApisPromise) {
return TapProductSample.settings.basketApisPromise;
}
TapProductSample.settings.basketApisPromise = (async () => {
const storefront = TapProductSample.settings.storefront;
if (!storefront) {
throw new Error('Storefront API is not available.');
}
let basketUpdaterApi =
typeof storefront.getApiSync === 'function'
? storefront.getApiSync('basketUpdaterApi')
: null;
let basketProductsApi =
typeof storefront.getApiSync === 'function'
? storefront.getApiSync('basketProductsApi')
: null;
// Gdy nie ma jeszcze sesji koszyka, feature "basket" może nie być
// zarejestrowany. Rejestrujemy go PRZED próbą pobrania API.
if (!basketUpdaterApi || !basketProductsApi) {
const featureSystemApi =
typeof storefront.getApiSync === 'function'
? storefront.getApiSync('FeatureSystemApi')
: null;
if (
!featureSystemApi ||
typeof featureSystemApi.registerDynamic !== 'function'
) {
throw new Error('FeatureSystemApi is not available.');
}
try {
await featureSystemApi.registerDynamic('basket', true);
} catch (error) {
// Inny moduł może zarejestrować basket w tym samym momencie.
// Błąd rejestracji ignorujemy i od razu ponawiamy pobranie API.
}
basketUpdaterApi =
typeof storefront.getApiSync === 'function'
? storefront.getApiSync('basketUpdaterApi')
: null;
basketProductsApi =
typeof storefront.getApiSync === 'function'
? storefront.getApiSync('basketProductsApi')
: null;
}
if (!basketUpdaterApi && typeof storefront.getApi === 'function') {
try {
basketUpdaterApi = await storefront.getApi('basketUpdaterApi');
} catch (error) {
console.warn(
'[TapProductSample] basketUpdaterApi getApi() failed.',
error
);
}
}
if (!basketProductsApi && typeof storefront.getApi === 'function') {
try {
basketProductsApi = await storefront.getApi('basketProductsApi');
} catch (error) {
console.warn(
'[TapProductSample] basketProductsApi getApi() failed.',
error
);
}
}
if (!basketUpdaterApi || !basketProductsApi) {
throw new Error('Basket APIs could not be initialized.');
}
TapProductSample.settings.basketUpdaterApi = basketUpdaterApi;
TapProductSample.settings.basketProductsApi = basketProductsApi;
TapProductSample.watchBasket();
return true;
})();
try {
return await TapProductSample.settings.basketApisPromise;
} catch (error) {
TapProductSample.settings.basketApisPromise = null;
throw error;
}
},
waitForBasket: async () => {
return TapProductSample.ensureBasketApis();
},
load: () => {
TapProductSample.injectProtectionStyles();
TapProductSample.bindBasketInteractionGuard();
TapProductSample.ensureBasketDomObserver();
document
.querySelectorAll(TapProductSample.settings.selectors.root)
.forEach((root) => {
if (root.dataset.tapProductSampleReady !== '1') {
root.dataset.tapProductSampleReady = '1';
const button = root.querySelector(
TapProductSample.settings.selectors.button
);
if (button) {
button.addEventListener('click', () => {
TapProductSample.handleClick(root, button);
});
}
}
TapProductSample.bindNativeSampleProductGuard(root);
TapProductSample.protectNativeSampleProductQuantity(root);
});
TapProductSample.scheduleBasketDomProtection();
},
injectProtectionStyles: () => {
if (TapProductSample.settings.protectionStyleInjected) {
return;
}
const style = document.createElement('style');
style.id = 'tap-product-sample-protection-styles';
style.textContent = `
.tap-product-sample-basket-item h-input-stepper h-button-stepper[type="increment"] {
display: none !important;
}
.tap-product-sample-basket-item_quantity-valid h-input-stepper h-button-stepper[type="decrement"] {
display: none !important;
}
.tap-product-sample-basket-item .input-stepper__value_input,
.tap-product-sample-native-quantity-locked .input-stepper__value_input {
pointer-events: none !important;
user-select: none !important;
caret-color: transparent !important;
}
.tap-product-sample-native-quantity-locked h-button-stepper {
display: none !important;
}
`;
document.head.appendChild(style);
TapProductSample.settings.protectionStyleInjected = true;
},
ensureBasketDomObserver: () => {
if (
TapProductSample.settings.basketDomObserver ||
!document.body ||
typeof MutationObserver !== 'function'
) {
return;
}
TapProductSample.settings.basketDomObserver = new MutationObserver(
(mutations) => {
const shouldProtect = mutations.some((mutation) => {
if (mutation.type === 'childList') {
return mutation.addedNodes && mutation.addedNodes.length > 0;
}
if (mutation.type === 'attributes') {
return Boolean(
mutation.target.closest?.(
TapProductSample.settings.selectors.basketProduct
)
);
}
return false;
});
if (shouldProtect) {
TapProductSample.scheduleBasketDomProtection();
}
}
);
TapProductSample.settings.basketDomObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['value']
});
},
scheduleBasketDomProtection: () => {
if (TapProductSample.settings.basketDomObserverQueued) {
return;
}
TapProductSample.settings.basketDomObserverQueued = true;
window.requestAnimationFrame(() => {
TapProductSample.settings.basketDomObserverQueued = false;
TapProductSample.protectBasketDom();
});
},
bindBasketInteractionGuard: () => {
if (TapProductSample.settings.basketInteractionGuardBound) {
return;
}
TapProductSample.settings.basketInteractionGuardBound = true;
document.addEventListener(
'click',
(event) => {
const stepperButton = event.target.closest(
TapProductSample.settings.selectors.basketStepperButton
);
if (!stepperButton) {
return;
}
const basketProduct = stepperButton.closest(
TapProductSample.settings.selectors.basketProduct
);
if (
!basketProduct ||
!TapProductSample.isBasketProductElementSample(basketProduct)
) {
return;
}
const buttonType = stepperButton.getAttribute('type');
const quantity = TapProductSample.getBasketProductDomQuantity(
basketProduct
);
// Jeżeli przed wdrożeniem próbka miała już błędnie ilość > 1,
// pozwalamy tylko ją zmniejszyć do 1. Zwiększanie jest zawsze blokowane.
if (buttonType === 'decrement' && quantity > 1) {
return;
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
},
true
);
document.addEventListener(
'keydown',
(event) => {
const input = event.target.closest(
TapProductSample.settings.selectors.basketQuantityInput
);
if (!input) {
return;
}
const basketProduct = input.closest(
TapProductSample.settings.selectors.basketProduct
);
if (
!basketProduct ||
!TapProductSample.isBasketProductElementSample(basketProduct)
) {
return;
}
event.preventDefault();
event.stopPropagation();
},
true
);
document.addEventListener(
'beforeinput',
(event) => {
const input = event.target.closest(
TapProductSample.settings.selectors.basketQuantityInput
);
if (!input) {
return;
}
const basketProduct = input.closest(
TapProductSample.settings.selectors.basketProduct
);
if (
!basketProduct ||
!TapProductSample.isBasketProductElementSample(basketProduct)
) {
return;
}
event.preventDefault();
event.stopPropagation();
},
true
);
},
watchBasket: () => {
const basketProductsApi = TapProductSample.settings.basketProductsApi;
if (!basketProductsApi || TapProductSample.settings.basketWatcherBound) {
return;
}
TapProductSample.settings.basketWatcherBound = true;
if (typeof basketProductsApi.get === 'function') {
try {
const products = basketProductsApi.get();
const items = TapProductSample.getBasketItemsFromBasketObject(products);
if (items.length || Array.isArray(products)) {
TapProductSample.settings.basketProducts = items;
}
} catch (error) {
console.warn(
'[TapProductSample] Initial basketProductsApi.get() failed.',
error
);
}
}
if (typeof basketProductsApi.select$ !== 'function') {
return;
}
const basketProducts$ = basketProductsApi.select$();
if (basketProducts$ && typeof basketProducts$.subscribe === 'function') {
basketProducts$.subscribe((products) => {
TapProductSample.settings.basketProducts =
TapProductSample.getBasketItemsFromBasketObject(products);
TapProductSample.scheduleBasketDomProtection();
});
}
},
splitIds: (value) => {
return String(value || '')
.split(/[\s,;]+/)
.map((item) => item.trim())
.filter(Boolean);
},
normalizeId: (value) => {
return String(value || '').trim();
},
normalizeText: (value) => {
return String(value || '')
.trim()
.toLocaleLowerCase('pl-PL')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '');
},
getKnownSampleProductIdsFromDom: () => {
const ids = new Set();
document
.querySelectorAll(TapProductSample.settings.selectors.root)
.forEach((root) => {
TapProductSample.splitIds(root.dataset.allSampleProductIds).forEach(
(id) => ids.add(id)
);
TapProductSample.getSampleCategoryProductIds(root).forEach(
(id) => ids.add(id)
);
});
return Array.from(ids);
},
getBasketProductOriginalProductId: (basketProduct) => {
const optionNodes = basketProduct.querySelectorAll(
'.basket-product__options [data-option-name]'
);
for (const optionNode of optionNodes) {
if (
TapProductSample.normalizeText(optionNode.dataset.optionName) ===
'id produktu'
) {
return TapProductSample.normalizeId(optionNode.dataset.optionValue);
}
}
return '';
},
getBasketProductDisplayName: (basketProduct) => {
const link = basketProduct.querySelector(
'.basket-product__link, .basket-product__name a, .basket-product__name'
);
return TapProductSample.normalizeText(link?.textContent || '');
},
isBasketProductElementSample: (basketProduct) => {
if (!basketProduct) {
return false;
}
// Próbki dodawane przez ten moduł mają w koszyku opcję "ID produktu".
if (TapProductSample.getBasketProductOriginalProductId(basketProduct)) {
return true;
}
const productId = TapProductSample.normalizeId(
basketProduct.dataset.productId
);
if (
productId &&
TapProductSample.getKnownSampleProductIdsFromDom().includes(productId)
) {
return true;
}
// Na stronie koszyka skrypt modułu jest ładowany globalnie, ale nie ma tam
// jego Twiga z konfiguracją. Produkty z kategorii /probniki rozpoznajemy
// dodatkowo po ich nazwach ("Próbka...", "Zestaw próbek...").
const name = TapProductSample.getBasketProductDisplayName(basketProduct);
return (
name.includes('probka') ||
name.includes('probek') ||
name.includes('probki')
);
},
getBasketProductDomQuantity: (basketProduct) => {
const stepper = basketProduct.querySelector(
TapProductSample.settings.selectors.basketStepper
);
const value = stepper?.getAttribute('value') || '1';
const quantity = parseInt(value, 10);
return Number.isFinite(quantity) && quantity > 0 ? quantity : 1;
},
countSamplesInDom: () => {
return Array.from(
document.querySelectorAll(
TapProductSample.settings.selectors.basketProduct
)
).reduce((sum, basketProduct) => {
if (!TapProductSample.isBasketProductElementSample(basketProduct)) {
return sum;
}
return sum + TapProductSample.getBasketProductDomQuantity(basketProduct);
}, 0);
},
hasDuplicateSampleInDom: (root) => {
const productId = TapProductSample.normalizeId(root?.dataset?.productId);
if (!productId) {
return false;
}
return Array.from(
document.querySelectorAll(
TapProductSample.settings.selectors.basketProduct
)
).some(
(basketProduct) =>
TapProductSample.getBasketProductOriginalProductId(basketProduct) ===
productId
);
},
hasNativeSampleProductInDom: (root) => {
const productId = TapProductSample.normalizeId(root?.dataset?.productId);
if (!productId) {
return false;
}
return Array.from(
document.querySelectorAll(
TapProductSample.settings.selectors.basketProduct
)
).some(
(basketProduct) =>
TapProductSample.normalizeId(basketProduct.dataset.productId) ===
productId
);
},
lockBasketProductQuantity: (basketProduct) => {
if (!basketProduct) {
return;
}
basketProduct.classList.add('tap-product-sample-basket-item');
const stepper = basketProduct.querySelector(
TapProductSample.settings.selectors.basketStepper
);
if (!stepper) {
return;
}
const quantity = TapProductSample.getBasketProductDomQuantity(basketProduct);
const isValidQuantity = quantity <= 1;
basketProduct.classList.toggle(
'tap-product-sample-basket-item_quantity-valid',
isValidQuantity
);
stepper.setAttribute('min', '1');
if (isValidQuantity) {
stepper.setAttribute('max', '1');
}
stepper
.querySelectorAll(TapProductSample.settings.selectors.basketStepperButton)
.forEach((button) => {
const buttonType = button.getAttribute('type');
const canRepairQuantity = buttonType === 'decrement' && quantity > 1;
if (canRepairQuantity) {
button.removeAttribute('aria-disabled');
button.setAttribute('tabindex', '0');
return;
}
button.setAttribute('aria-disabled', 'true');
button.setAttribute('tabindex', '-1');
});
const input = stepper.querySelector(
TapProductSample.settings.selectors.basketQuantityInput
);
if (input) {
input.readOnly = true;
input.setAttribute('aria-readonly', 'true');
input.setAttribute('tabindex', '-1');
}
},
protectBasketDom: () => {
document
.querySelectorAll(TapProductSample.settings.selectors.basketProduct)
.forEach((basketProduct) => {
if (TapProductSample.isBasketProductElementSample(basketProduct)) {
TapProductSample.lockBasketProductQuantity(basketProduct);
}
});
},
protectNativeSampleProductQuantity: (root) => {
if (root?.dataset?.isSampleCategoryProduct !== '1') {
return;
}
const productId = TapProductSample.normalizeId(root.dataset.productId);
if (!productId) {
return;
}
document
.querySelectorAll(`product-quantity[product-id="${productId}"]`)
.forEach((productQuantity) => {
productQuantity.classList.add(
'tap-product-sample-native-quantity-locked'
);
productQuantity.setAttribute('value', '1');
const stepper = productQuantity.querySelector(
TapProductSample.settings.selectors.basketStepper
);
if (stepper) {
stepper.setAttribute('min', '1');
stepper.setAttribute('max', '1');
stepper.setAttribute('value', '1');
stepper
.querySelectorAll(
TapProductSample.settings.selectors.basketStepperButton
)
.forEach((button) => {
button.setAttribute('aria-disabled', 'true');
button.setAttribute('tabindex', '-1');
});
const input = stepper.querySelector(
TapProductSample.settings.selectors.basketQuantityInput
);
if (input) {
input.readOnly = true;
input.setAttribute('aria-readonly', 'true');
input.setAttribute('tabindex', '-1');
}
}
});
document
.querySelectorAll(`buy-button[product-id="${productId}"]`)
.forEach((buyButton) => {
buyButton.setAttribute('quantity', '1');
});
},
getSampleLimit: (root) => {
const limit = parseInt(root?.dataset?.sampleLimit || '5', 10);
return Number.isFinite(limit) && limit > 0 ? limit : 5;
},
getSampleCategoryProductIds: (root) => {
return TapProductSample.splitIds(root?.dataset?.sampleCategoryProductIds);
},
getNativeBuyButtonQuantity: (buyButton) => {
const quantity = parseInt(buyButton?.getAttribute('quantity') || '1', 10);
return Number.isFinite(quantity) && quantity > 0 ? quantity : 1;
},
getQuantity: (item) => {
const quantity = item?.quantity ?? item?.count ?? item?.amount ?? item?.qty ?? 1;
const parsed = parseInt(quantity, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 1;
},
getItemProductId: (item) => {
return TapProductSample.normalizeId(
item?.productId ??
item?.product_id ??
item?.product?.id ??
item?.product?.productId ??
item?.id ??
''
);
},
getItemVariantId: (item) => {
return TapProductSample.normalizeId(
item?.variantId ??
item?.variant_id ??
item?.stockId ??
item?.stock_id ??
item?.variant?.id ??
item?.stock?.id ??
''
);
},
collectStrings: (value, output = [], depth = 0) => {
if (depth > 6 || value === null || value === undefined) {
return output;
}
if (typeof value === 'string' || typeof value === 'number') {
output.push(String(value));
return output;
}
if (Array.isArray(value)) {
value.forEach((item) => {
TapProductSample.collectStrings(item, output, depth + 1);
});
return output;
}
if (typeof value === 'object') {
Object.keys(value).forEach((key) => {
output.push(String(key));
TapProductSample.collectStrings(value[key], output, depth + 1);
});
}
return output;
},
getBasketItemsFromBasketObject: (basket) => {
if (!basket) {
return [];
}
if (Array.isArray(basket)) {
return basket;
}
const candidates = [
basket.products?.list,
basket.products,
basket.items,
basket.lines,
basket.positions,
basket.basket?.products?.list,
basket.basket?.products,
basket.basket?.items,
basket.basket?.lines,
basket.data?.products?.list,
basket.data?.products,
basket.data?.items
];
for (const candidate of candidates) {
if (Array.isArray(candidate)) {
return candidate;
}
}
return [];
},
getBasketItems: async () => {
await TapProductSample.waitForBasket();
const basketProductsApi = TapProductSample.settings.basketProductsApi;
const basketUpdaterApi = TapProductSample.settings.basketUpdaterApi;
// Przy każdym sprawdzeniu limitu próbujemy pobrać najświeższy stan koszyka.
// Poprzednio cache z subskrypcji mógł być jeszcze nieaktualny po szybkich zmianach.
if (basketUpdaterApi && typeof basketUpdaterApi.getCurrentBasket === 'function') {
try {
const basket = await basketUpdaterApi.getCurrentBasket();
const items = TapProductSample.getBasketItemsFromBasketObject(basket);
TapProductSample.settings.basketProducts = items;
return items;
} catch (error) {}
}
if (basketProductsApi && typeof basketProductsApi.get === 'function') {
try {
const products = basketProductsApi.get();
const items = TapProductSample.getBasketItemsFromBasketObject(products);
if (items.length || Array.isArray(products)) {
TapProductSample.settings.basketProducts = items;
return items;
}
} catch (error) {}
}
return TapProductSample.settings.basketProducts;
},
isSampleItem: (
item,
sampleProductIds,
sampleCategoryProductIds,
sampleProductId,
sampleVariantId
) => {
const itemProductId = TapProductSample.getItemProductId(item);
const itemVariantId = TapProductSample.getItemVariantId(item);
const strings = TapProductSample.collectStrings(item);
if (itemProductId && sampleProductIds.includes(itemProductId)) {
return true;
}
if (itemProductId && sampleCategoryProductIds.includes(itemProductId)) {
return true;
}
if (sampleProductId && itemProductId === sampleProductId) {
return true;
}
if (sampleVariantId && itemVariantId === sampleVariantId) {
return true;
}
if (sampleProductIds.some((id) => strings.includes(id))) {
return true;
}
return sampleCategoryProductIds.some((id) => strings.includes(id));
},
hasDuplicateSample: (items, root) => {
const productId = TapProductSample.normalizeId(root.dataset.productId);
const sampleProductId = TapProductSample.normalizeId(root.dataset.sampleProductId);
const sampleVariantId = TapProductSample.normalizeId(root.dataset.sampleVariantId);
const sampleProductIds = TapProductSample.splitIds(root.dataset.allSampleProductIds);
const sampleCategoryProductIds = TapProductSample.getSampleCategoryProductIds(root);
return items.some((item) => {
if (
!TapProductSample.isSampleItem(
item,
sampleProductIds,
sampleCategoryProductIds,
sampleProductId,
sampleVariantId
)
) {
return false;
}
const strings = TapProductSample.collectStrings(item);
return productId && strings.includes(productId);
});
},
countSamples: (items, root) => {
const sampleProductId = TapProductSample.normalizeId(root.dataset.sampleProductId);
const sampleVariantId = TapProductSample.normalizeId(root.dataset.sampleVariantId);
const sampleProductIds = TapProductSample.splitIds(root.dataset.allSampleProductIds);
const sampleCategoryProductIds = TapProductSample.getSampleCategoryProductIds(root);
return items.reduce((sum, item) => {
if (
TapProductSample.isSampleItem(
item,
sampleProductIds,
sampleCategoryProductIds,
sampleProductId,
sampleVariantId
)
) {
return sum + TapProductSample.getQuantity(item);
}
return sum;
}, 0);
},
hasNativeSampleProductDuplicate: (items, root) => {
const productId = TapProductSample.normalizeId(root?.dataset?.productId);
if (!productId) {
return false;
}
return items.some((item) => {
if (TapProductSample.getItemProductId(item) === productId) {
return true;
}
return TapProductSample.collectStrings(item).includes(productId);
});
},
bindNativeSampleProductGuard: (root) => {
if (root.dataset.isSampleCategoryProduct !== '1') {
return;
}
const currentProductId = TapProductSample.normalizeId(root.dataset.productId);
document
.querySelectorAll(TapProductSample.settings.selectors.nativeBuyButton)
.forEach((buyButton) => {
if (
TapProductSample.normalizeId(buyButton.getAttribute('product-id')) !==
currentProductId ||
buyButton.dataset.tapProductSampleGuardReady === '1'
) {
return;
}
buyButton.dataset.tapProductSampleGuardReady = '1';
buyButton.setAttribute('quantity', '1');
buyButton.addEventListener(
'click',
async (event) => {
if (buyButton.dataset.tapProductSampleGuardBypass === '1') {
delete buyButton.dataset.tapProductSampleGuardBypass;
return;
}
const nativeButton = event.target.closest('button');
if (!nativeButton || !buyButton.contains(nativeButton)) {
return;
}
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
try {
const items = await TapProductSample.getBasketItems();
const sampleLimit = TapProductSample.getSampleLimit(root);
const apiSamplesCount = TapProductSample.countSamples(items, root);
const domSamplesCount = TapProductSample.countSamplesInDom();
const currentSamplesCount = Math.max(
apiSamplesCount,
domSamplesCount
);
if (
TapProductSample.hasNativeSampleProductDuplicate(items, root) ||
TapProductSample.hasNativeSampleProductInDom(root)
) {
TapProductSample.showModal(
root.dataset.duplicateMessage ||
'Już dodałeś próbkę tego produktu'
);
return;
}
if (currentSamplesCount + 1 > sampleLimit) {
TapProductSample.showModal(
root.dataset.limitMessage ||
'Możesz zamówić maksymalnie 5 próbek!'
);
return;
}
TapProductSample.protectNativeSampleProductQuantity(root);
buyButton.setAttribute('quantity', '1');
buyButton.dataset.tapProductSampleGuardBypass = '1';
nativeButton.click();
} catch (error) {
console.error(
'[TapProductSample] Native sample limit check failed.',
error
);
TapProductSample.showModal(
root.dataset.errorMessage ||
'Wystąpił błąd podczas dodawania próbki'
);
}
},
true
);
});
},
createModal: () => {
let modal = document.querySelector('.tap-product-sample-modal');
if (modal) {
return modal;
}
modal = document.createElement('div');
modal.className = 'tap-product-sample-modal';
modal.innerHTML = `
`;
document.body.appendChild(modal);
modal.addEventListener('click', (event) => {
if (event.target === modal) {
TapProductSample.closeModal();
}
});
modal.querySelector('.tap-product-sample-modal__close').addEventListener('click', () => {
TapProductSample.closeModal();
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
TapProductSample.closeModal();
}
});
return modal;
},
showModal: (message, type = 'info') => {
const modal = TapProductSample.createModal();
const messageNode = modal.querySelector('.tap-product-sample-modal__message');
const iconNode = modal.querySelector('.tap-product-sample-modal__icon');
if (messageNode) {
messageNode.textContent = message;
}
if (iconNode) {
if (type === 'success') {
iconNode.innerHTML = `
`;
} else {
iconNode.textContent = 'i';
}
}
modal.classList.add(TapProductSample.settings.classes.modalActive);
},
closeModal: () => {
const modal = document.querySelector('.tap-product-sample-modal');
if (modal) {
modal.classList.remove(TapProductSample.settings.classes.modalActive);
}
},
addSampleToBasket: async (root) => {
await TapProductSample.waitForBasket();
const basketUpdaterApi = TapProductSample.settings.basketUpdaterApi;
if (!basketUpdaterApi || typeof basketUpdaterApi.addItem !== 'function') {
throw new Error('Basket updater API is not available.');
}
const variantOptions = {};
variantOptions[root.dataset.productNameOptionId] = root.dataset.productName || '';
variantOptions[root.dataset.productIdOptionId] = root.dataset.productId || '';
const item = {
variantId: Number(root.dataset.sampleVariantId),
quantity: 1,
variantOptions,
showAddedModal: false,
bundleItems: []
};
const basket = await basketUpdaterApi.addItem(item);
const returnedItems = TapProductSample.getBasketItemsFromBasketObject(basket);
if (returnedItems.length) {
TapProductSample.settings.basketProducts = returnedItems;
}
if (basketUpdaterApi.refreshBasket && typeof basketUpdaterApi.refreshBasket === 'function') {
try {
await basketUpdaterApi.refreshBasket();
} catch (error) {}
}
return basket;
},
handleClick: async (root, button) => {
if (button.classList.contains(TapProductSample.settings.classes.loading)) {
return;
}
button.classList.add(TapProductSample.settings.classes.loading);
button.disabled = true;
try {
const items = await TapProductSample.getBasketItems();
const sampleLimit = TapProductSample.getSampleLimit(root);
const apiSamplesCount = TapProductSample.countSamples(items, root);
const domSamplesCount = TapProductSample.countSamplesInDom();
const currentSamplesCount = Math.max(apiSamplesCount, domSamplesCount);
if (
TapProductSample.hasDuplicateSample(items, root) ||
TapProductSample.hasDuplicateSampleInDom(root)
) {
TapProductSample.showModal(
root.dataset.duplicateMessage ||
'Już dodałeś próbkę tego produktu'
);
return;
}
if (currentSamplesCount + 1 > sampleLimit) {
TapProductSample.showModal(root.dataset.limitMessage || 'Możesz zamówić maksymalnie 5 próbek!');
return;
}
await TapProductSample.addSampleToBasket(root);
TapProductSample.scheduleBasketDomProtection();
if (root.dataset.afterAddAction === 'cart') {
window.location.href = root.dataset.basketUrl || '/basket';
return;
}
TapProductSample.showModal(
root.dataset.addedMessage || 'Próbka została dodana do koszyka',
'success'
);
} catch (error) {
console.error('[TapProductSample] Sample add failed.', error);
TapProductSample.showModal(root.dataset.errorMessage || 'Wystąpił błąd podczas dodawania próbki');
} finally {
button.classList.remove(TapProductSample.settings.classes.loading);
button.disabled = false;
}
}
};
useStorefront(async (storefront) => {
TapProductSample.init(storefront);
storefront.eventBus.on('PageManager.loaded', () => {
TapProductSample.load();
if (TapProductSample.shouldPrepareBasketApis()) {
TapProductSample.ensureBasketApis().catch((error) => {
console.warn(
'[TapProductSample] Basket resolve after PageManager.loaded failed.',
error
);
});
}
});
});