///////////////////////////////////////////////////////////
// TEXT
// LITERAŁY
const idmHotspotTextObject = {
["Kod rabatowy"]: ,
["Okazja"]: ,
["Promocja"]: ,
["Bestseller"]: ,
["Nowość"]: ,
["Ilość"]: ,
["Porównaj"]: ,
["Dodaj do ulubionych"]: ,
["Najniższa cena"]: ,
["Najniższa cena z 30 dni przed obniżką"]: (
),
["Zwiększ ilość"]: ,
["Zmniejsz ilość"]: ,
["Najniższa cena produktu w okresie 30 dni przed wprowadzeniem obniżki"]: (
),
["Cena regularna"]: ,
["Cena bez kodu"]: ,
["Cena nadchodząca od"]: ,
["Coś poszło nie tak podczas dodawania do koszyka. Spróbuj ponownie lub odśwież stronę"]:
(
),
["Nie znaleziono produktów"]: ,
["Błąd przy pobieraniu danych"]: (
),
["Kliknij, by przejść do formularza kontaktu"]: (
),
["Cena na telefon"]: ,
["Dodany"]: ,
["Wystąpił błąd"]: ,
["Do koszyka"]: ,
["Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]: (
),
["Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]: (
),
["Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"]: (
),
["Nie znaleziono kontenera"]: ,
["Nie znaleziono metody graphql"]: (
),
["Drugie Zdjęcie"]: ,
["Zobacz więcej"]: ,
};
// STRING
// const idmHotspotTextObject = {
// "Kod rabatowy": "Kod rabatowy",
// "Okazja": "Okazja",
// "Promocja": "Promocja",
// "Bestseller": "Bestseller",
// "Nowość": "Nowość",
// "Ilość": "Ilość",
// "Porównaj": "Porównaj",
// "Dodaj do ulubionych": "Dodaj do ulubionych",
// "Najniższa cena": "Najniższa cena",
// "Najniższa cena z 30 dni przed obniżką": "Najniższa cena z 30 dni przed obniżką",
// "Zwiększ ilość": "Zwiększ ilość",
// "Zmniejsz ilość": "Zmniejsz ilość",
// "Najniższa cena produktu w okresie 30 dni przed wprowadzeniem obniżki":
// "Najniższa cena produktu w okresie 30 dni przed wprowadzeniem obniżki",
// "Cena regularna": "Cena regularna",
// "Cena bez kodu": "Cena bez kodu",
// "Cena nadchodząca od": "Cena nadchodząca od",
// "Coś poszło nie tak podczas dodawania do koszyka. Spróbuj ponownie lub odśwież stronę":
// "Coś poszło nie tak podczas dodawania do koszyka. Spróbuj ponownie lub odśwież stronę",
// "Nie znaleziono produktów": "Nie znaleziono produktów",
// "Błąd przy pobieraniu danych": "Błąd przy pobieraniu danych",
// "Kliknij, by przejść do formularza kontaktu":
// "Kliknij, by przejść do formularza kontaktu",
// "Cena na telefon": "Cena na telefon",
// "Dodany": "Dodany",
// "Wystąpił błąd": "Wystąpił błąd",
// "Do koszyka": "Do koszyka",
// "Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:":
// "Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:",
// "Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:":
// "Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:",
// "Wystąpił błąd z inicjalizacją. Proszę odśwież stronę":
// "Wystąpił błąd z inicjalizacją. Proszę odśwież stronę",
// "Nie znaleziono kontenera": "Nie znaleziono kontenera",
// "Nie znaleziono metody graphql": "Nie znaleziono metody graphql",
// "Drugie Zdjęcie": "Drugie Zdjęcie",
// "Zobacz więcej": "Zobacz więcej",
// };
///////////////////////////////////////////////
// GraphQL
// ogolne
const IDM_PRICE_QUERY = (priceType) => `price {
rebateCodeActive
price {
${priceType} {
value
formatted
}
}
omnibusPrice {
${priceType} {
value
formatted
}
}
omnibusPriceDetails {
unit {
${priceType} {
value
formatted
}
}
youSavePercent
omnibusPriceIsHigherThanSellingPrice
newPriceEffectiveUntil {
formatted
}
}
max {
${priceType} {
value
formatted
}
}
unit {
${priceType} {
value
formatted
}
}
unitConvertedPrice {
${priceType} {
value
formatted
}
}
youSavePercent
beforeRebate {
${priceType} {
value
formatted
}
}
beforeRebateDetails {
youSavePercent
unit {
${priceType} {
value
formatted
}
}
}
advancePrice {
${priceType} {
value
formatted
}
}
suggested {
${priceType} {
value
formatted
}
}
rebateNumber {
number
${priceType} {
value
formatted
}
}
}`;
const IDM_PRODUCT_QUERY = (priceType) => `id
type
name
icon
iconSecond
iconSmall
iconSmallSecond
link
zones
series{
name, link
}
producer{
name, link,
searchIcons{
icon
}
}
category{
name
}
sizes{
id
amount
name
${IDM_PRICE_QUERY(priceType)}
}
group{
id
name
link
versions{
id
name
icon
iconSecond
iconSmall
iconSmallSecond
link
}
}
opinion{
rating,
count
}
awardedParameters {
name
id
description
values {
name
id
}
}
enclosuresImages {
position
url
}
points
unit{
id, name, singular, plural, fraction, sellBy, precision, unitConvertedFormat
}
${IDM_PRICE_QUERY(priceType)}`;
// 1. products
const IDM_PRODUCTS_GQL = (args, priceType = "gross") =>
JSON.stringify({
query: `{
products(${args}){
took
products{
${IDM_PRODUCT_QUERY(priceType)}
}
}
}`,
});
// 2. hotspots
const IDM_HOTSPOTS_GQL = (args, priceType = "gross") =>
JSON.stringify({
query: `{
hotspots(${args}){
took
name
url
products{
${IDM_PRODUCT_QUERY(priceType)}
}
}
}`,
});
// 3. single product
const IDM_PRODUCT_GQL = (args, priceType = "gross") =>
JSON.stringify({
query: `{
product(${args}){
product{
${IDM_PRODUCT_QUERY(priceType)}
}
}
}`,
});
// ADD TO BASKET
const IDM_HOTSPOT_ADD_TO_BASKET = (t, e, a) =>
JSON.stringify({
query: `mutation {
addProductsToBasket(ProductInput: {id: ${t}, size: "${e}", quantity: ${a}}) {
status
results {
status
error {
code
message
}
}
}
}`,
});
/////////////////////////////////////////
// JS
app_shop.fn.idmGetOmnibusDetails = (options) => {
const {
productData,
sizeId,
priceType = app_shop.vars.priceType,
} = options || {};
if (!productData) return false;
const sizeData =
productData.sizes.find((size) => size.id === sizeId) || productData;
if (!sizeData?.price) return false;
const classes = {
add: [],
remove: [
"--omnibus",
"--omnibus-short",
"--omnibus-code",
"--omnibus-code-short",
"--omnibus-new-price",
"--omnibus-higher",
],
};
const activeLabel = {};
const omnibusPrice =
sizeData.price?.omnibusPriceDetails?.unit?.[priceType]?.formatted ||
sizeData.price.omnibusPrice[priceType]?.formatted;
if (!omnibusPrice) {
return {
classes,
};
}
// Omnibus
classes.add.push("--omnibus");
classes.remove = classes.remove.filter((item) => item !== "--omnibus");
const sellBy = productData?.unit?.sellBy;
const unitMaxPrice =
sizeData?.price?.unit?.[priceType]?.formatted &&
sizeData.price.max?.[priceType]?.value
? format_price(
parseFloat(sizeData.price.max?.[priceType]?.value) *
parseFloat(sellBy),
{
mask: app_shop.vars.currency_format,
currency: app_shop.vars?.currency?.symbol,
currency_space: app_shop.vars.currency_space,
currency_before_price: app_shop.vars.currency_before_value,
}
)
: false;
const maxPrice = unitMaxPrice || sizeData.price.max?.[priceType]?.formatted;
// Skrócona wersja omnibusa
if (!maxPrice || maxPrice === omnibusPrice) {
classes.add.push("--omnibus-short");
classes.remove = classes.remove.filter(
(item) => item !== "--omnibus-short"
);
}
// Aktywny kod rabatowy
if (
app_shop.vars.omnibus?.rebateCodeActivate &&
sizeData.price?.rebateCodeActive
) {
classes.add.push("--omnibus-code");
activeLabel.rebateCodeActive = `${omnibusDetailsTxt?.["Kod rabatowy"]} `;
classes.remove = classes.remove.filter((item) => item !== "--omnibus-code");
}
// Skrócona wersja omnibusa, gdy aktywny kod rabatowy
const beforeRebatePrice =
sizeData.price.beforeRebateDetails?.unit?.[priceType]?.formatted ||
sizeData.price.beforeRebate[priceType]?.formatted;
if (
app_shop.vars.omnibus?.rebateCodeActivate &&
beforeRebatePrice === omnibusPrice &&
sizeData.price?.rebateCodeActive
) {
classes.add.push("--omnibus-code-short");
classes.remove = classes.remove.filter(
(item) => item !== "--omnibus-code-short"
);
}
// Nadchodząca cena
const newDate =
sizeData.price.omnibusPriceDetails?.newPriceEffectiveUntil?.formatted;
if (newDate && maxPrice) {
classes.add.push("--omnibus-new-price");
classes.remove = classes.remove.filter(
(item) => item !== "--omnibus-new-price"
);
}
// Cena omnibusa wyższa niż cena sprzedaży
const higher =
sizeData.price.omnibusPriceDetails?.omnibusPriceIsHigherThanSellingPrice;
if (higher) {
classes.add.push("--omnibus-higher");
classes.remove = classes.remove.filter(
(item) => item !== "--omnibus-higher"
);
}
// label okazja
if ((!higher || newDate) && !activeLabel?.rebateCodeActive) {
activeLabel.bargain = `${omnibusDetailsTxt?.["Okazja"]} `;
}
// label promocja
if (Object.keys(activeLabel)?.length === 0) {
activeLabel.bargain = `${omnibusDetailsTxt?.["Promocja"]} `;
}
let omnibusPercentSign = "";
if (higher) {
omnibusPercentSign = "-";
} else if (sizeData.price.omnibusPriceDetails?.youSavePercent !== 0) {
omnibusPercentSign = "+";
}
const omnibusPercent = `${omnibusPercentSign}${sizeData.price.omnibusPriceDetails?.youSavePercent}%`;
const omnibus = {
price: omnibusPrice,
visible: true,
percent: omnibusPercent,
html: `${omnibusDetailsTxt["Najniższa cena produktu w okresie 30 dni przed wprowadzeniem obniżki"]}: ${omnibusPrice}${omnibusPercent} `,
};
const max = maxPrice
? {
max: {
price: maxPrice,
visible: true,
percent: `-${sizeData.price.youSavePercent}%`,
html: `${omnibusDetailsTxt["Cena regularna"]}:
${maxPrice}-${sizeData.price.youSavePercent}% `,
},
}
: {};
const beforeRebate = beforeRebatePrice
? {
beforeRebate: {
price: beforeRebatePrice,
visible: !!classes.add.includes("--omnibus-code"),
percent: `-${sizeData.price.beforeRebateDetails?.youSavePercent}%`,
html: `${omnibusDetailsTxt["Cena bez kodu"]}:
${beforeRebatePrice}
-${sizeData.price.beforeRebateDetails?.youSavePercent}% `,
},
}
: {};
const newPriceEffectiveUntil = newDate
? {
newPriceEffectiveUntil: {
date: newDate,
price: maxPrice,
visible: !!classes.add.includes("--omnibus-new-price"),
html: `${omnibusDetailsTxt["Cena nadchodząca od"]}
${newDate}:
${maxPrice} `,
},
}
: {};
const omnibusLabel = {
omnibusLabel: {
name: Object.keys(activeLabel)?.[0] || "",
html: activeLabel[Object.keys(activeLabel)?.[0]],
},
};
return {
classes,
omnibus,
...max,
...beforeRebate,
...newPriceEffectiveUntil,
...omnibusLabel,
};
};
/**
* Klasa IdmHotspot
* ============================
* Odpowiada za tworzenie, renderowanie i obsługę dynamicznych hotspotów produktowych.
* Pobiera dane przez GraphQL, renderuje produkty, obsługuje dodawanie do koszyka,
* inicjuje Swipera, ustawia wysokości, nasłuchuje zdarzeń i wykonuje lazy loading.
*/
class IdmHotspot {
// ============================
// DOMYŚLNE USTAWIENIA SWIPERA
// ============================
static idmDefaultSwiperConfig = {
// true, false, obiekt z opcjami swipera
loop: false,
autoHeight: false,
spaceBetween: 16,
slidesPerView: 1.4,
centeredSlides: true,
centeredSlidesBounds: true,
breakpoints: {
550: {
slidesPerView: 2,
centeredSlides: true,
centeredSlidesBounds: true,
},
757: {
slidesPerView: 3,
},
979: {
slidesPerView: 4,
centeredSlides: false,
},
},
// navigation: {
// nextEl: `#${this.id} .swiper-button-next`,
// prevEl: `#${this.id} .swiper-button-prev`,
// },
};
// ============================
// DOMYŚLNE OPCJE HOTSPOTA
// ============================
static idmDefaultHotspotOptions = {
cssVariables: {
version: {
columnDesktop: 5,
columnTablet: 4,
columnMobile: 4,
},
nameClamp: null, // 2,
},
options: {
limit: 8,
lazy: true,
devMode: false,
callbackFn: () => {},
omnibusTooltip: false,
// switchImage: false,
// POKAZANIE
showOpinions: false,
showProducer: false, // false, true, "img", "link", "img-link"
showSeries: false, // false, true, "link"
showSecondImage: false,
// DODAWANIE
addToBasket: true, // true, false, "range"
addToFavorites: false, // Wymaga zmian szablonowych
addToCompare: false,
// WYBÓR
selectSize: false,
selectVersion: false,
// SWIPER
swiper: true,
swiperScrollbar: false,
},
};
/**
* Konstruktor
* @param {object} object - Dane konfiguracyjne hotspotu
*/
constructor({
id,
title,
classes,
placement,
source,
query,
options = {},
banner,
hotspotEl,
products,
cssVariables,
}) {
const defaultOptions = this.getDefaultHotspotOptions();
this.id = id || "";
this.title = title || "";
this.classes = classes || "";
this.placement = placement || {};
this.source = source || {};
this.query = query || {};
// this.type = type;
this.products = products || null;
this.productsVersions = [];
this.hotspotEl = hotspotEl || null;
// Merge defaults
this.options = {
...defaultOptions.options,
...options,
};
this.cssVariables = {
...defaultOptions.cssVariables,
...cssVariables,
};
this.banner = banner || false;
// this.hotspots = {};
this.priceType = app_shop?.vars?.priceType || "gross";
// bind this do funkcji eventowych
this.handleAddToBasket = this.handleAddToBasket.bind(this);
this.handleQuantityButtonClick = this.handleQuantityButtonClick.bind(this);
this.handleQuantityInputChange = this.handleQuantityInputChange.bind(this);
this.handleAddToCompare = this.handleAddToCompare.bind(this);
this.handleAddToFav = this.handleAddToFav.bind(this);
this.handleShowSecondImage = this.handleShowSecondImage.bind(this);
this.handleHideSecondImage = this.handleHideSecondImage.bind(this);
this.handleSelectVersion = this.handleSelectVersion.bind(this);
this.handleSelectSize = this.handleSelectSize.bind(this);
this.init();
}
// ========================================================
// GET DEFAULTS
// ========================================================
getDefaultSwiperOptions() {
return IdmHotspot.idmDefaultSwiperConfig;
}
getDefaultHotspotOptions() {
return IdmHotspot.idmDefaultHotspotOptions;
}
// ========================================================
// ASYNC – POBIERANIE DANYCH Z GRAPHQL
// ========================================================
/**
* Przygotowuje funkcję i zapytanie GraphQL w zależności od typu danych.
*/
getQueryData() {
let graphFn, query;
let queryMarkup = "";
let additionalQuery = "";
if (this.source?.hotspotsType) {
graphFn = IDM_HOTSPOTS_GQL;
queryMarkup += `hotspot: ${this.source.hotspotsType}, limit: ${this.options.limit}`;
} else {
graphFn = IDM_PRODUCTS_GQL;
if (this.source?.productsId) {
queryMarkup += `productsId: [${this.source.productsId}],`;
}
if (this.source?.productsMenu) {
queryMarkup += `navigation: ${this.source.productsMenu},`;
}
if (this.source?.producersId) {
queryMarkup += `producers: [${this.source.producersId}],`;
}
if (this.source?.seriesId) {
queryMarkup += `series: [${this.source.seriesId}],`;
}
if (this.source?.parametersId) {
queryMarkup += `parameters: [${this.source.parametersId.reduce(
(acc, val) => acc + `{id: ${val}}`,
""
)}],`;
}
if (this.source?.priceRange) {
queryMarkup += `priceRange: {from: ${
+this.source.priceRange?.from || 0
}, to: ${+this.source.priceRange?.to || 0}},`;
}
if (this.options.limit)
additionalQuery += `, settingsInput: {limit: ${this.options.limit}}`;
}
query = `searchInput: { ${queryMarkup} }${additionalQuery}`;
return [graphFn, query];
}
/**
* Ustawia dane zapytania GraphQL wewnątrz instancji.
*/
setQueryData() {
const [graphFn, queryString] = this.getQueryData();
this.query.graphFn = graphFn;
this.query.string = queryString;
}
/**
* Pobiera dane hotspotu z API GraphQL.
*/
async getHotspotData() {
if (this.products) return;
try {
let products;
if (this.source?.link) {
products = await this.xmlGetData();
} else {
const res = await fetch(`/graphql/v1/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: this.query.graphFn(this.query.string, this.priceType),
});
const data = await res.json();
products =
data?.data?.[
this.query.graphFn === IDM_HOTSPOTS_GQL ? "hotspots" : "products"
]?.products;
this.title = this.title || data?.data?.hotspots?.name || "";
}
if (!products || !products.length)
throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]);
this.products = products;
} catch (err) {
console.error(idmHotspotTextObject["Błąd przy pobieraniu danych"], err);
return null;
}
}
// ========================================================
// MARKUP – TWORZENIE HTML PRODUKTÓW
// ========================================================
/**
* Tworzy markup dla wszystkich produktów w hotspotcie.
*/
markup() {
let markup = "";
this.products.forEach((prod) => {
markup += this.markupProduct(prod);
});
return markup;
}
/**
* Tworzy markup dla pojedynczego produktu.
*/
markupProduct(prod) {
// markup pojedynczego produktu
let singleMarkup = "";
singleMarkup += `
${this.markupProductInnerHTML(prod)}
`;
return singleMarkup;
}
markupProductInnerHTML(prod) {
return `
${this.markupAdditional(prod)}
${this.markupImage(prod)}
${this.markupLabel(prod)}
${this.markupVersions(prod)}
${this.markupProducer(prod)}
${this.markupSeries(prod)}
${this.markupOpinions(prod)}
${prod.name}
${this.markupPrice(prod)}
${this.markupAddToBasket(prod)}
`;
}
markupAdditional(prod) {
return `
${this.markupCompare(prod)}
${this.markupFavorite(prod)}
`;
}
markupCompare(prod) {
if (!this.options?.addToCompare) return "";
return `
`;
}
markupFavorite(prod) {
if (
!this.options?.addToFavorites ||
typeof this.addToFavFn !== "function" ||
this.hotspotEl.closest(".modal")
)
return "";
return `
`;
}
markupVersions(prod) {
if (!this.options?.selectVersion) return "";
if (!prod.group?.versions || prod.group?.versions?.length < 2)
return `
`;
const sortedVersions = prod.group.versions.sort(function (a, b) {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
});
let productVersionsMarkup = sortedVersions.reduce((acc, val, index) => {
return (
acc +
` `
);
}, "");
const desktopMoreLength =
prod.group.versions.length - this.cssVariables.version.columnDesktop + 1;
const tabletMoreLength =
prod.group.versions.length - this.cssVariables.version.columnTablet + 1;
const mobileMoreLength =
prod.group.versions.length - this.cssVariables.version.columnMobile + 1;
return ``;
}
markupImage(prod) {
let markup = "";
if (prod.iconSmallSecond && prod.iconSecond)
markup += `
`;
else if (prod?.iconSmall)
markup += `
`;
else
markup += ` `;
if (this.options?.showSecondImage && prod.enclosuresImages?.[1]?.url)
markup += ` `;
return markup;
}
markupLabel(prod) {
let labelMarkup = "";
// labele zones
if (prod.zones?.find((zone) => zone === "bestseller"))
labelMarkup += `${idmHotspotTextObject["Bestseller"]} `;
if (prod.zones?.find((zone) => zone === "news"))
labelMarkup += `${idmHotspotTextObject["Nowość"]} `;
const omnibusPrice =
prod.price?.omnibusPriceDetails?.unit?.[this.idmPriceType]?.formatted ||
prod.price.omnibusPrice[this.idmPriceType]?.formatted;
if (omnibusPrice) {
const newDate =
prod.price.omnibusPriceDetails?.newPriceEffectiveUntil?.formatted;
const higher =
prod.price.omnibusPriceDetails?.omnibusPriceIsHigherThanSellingPrice;
if (
app_shop.vars.omnibus?.rebateCodeActivate &&
prod.price?.rebateCodeActive
) {
labelMarkup += `${idmHotspotTextObject["Kod rabatowy"]} `;
} else if ((!higher || newDate) && !activeLabel?.rebateCodeActive) {
labelMarkup += `${idmHotspotTextObject["Okazja"]} `;
} else {
labelMarkup += `${idmHotspotTextObject["Promocja"]} `;
}
}
return labelMarkup;
}
markupOpinions(prod) {
if (!this.options?.showOpinions) return "";
return `
${prod.opinion.rating.toFixed(
2
)} / 5.00
`;
}
markupProducer(prod) {
if (!this.options.showProducer) return "";
const isImgIncluded =
typeof this.options.showProducer === "string" &&
this.options.showProducer?.includes("img");
const isLinkIncluded =
typeof this.options.showProducer === "string" &&
this.options.showProducer?.includes("link");
if (
(!prod?.producer?.name &&
(typeof this.options.showProducer === "boolean" || isLinkIncluded)) ||
(!prod?.producer?.searchIcons?.icon && isImgIncluded)
)
return `
`;
let producerMarkup =
isLinkIncluded && prod.producer?.link
? ``
: ``;
if (isImgIncluded && prod?.producer?.searchIcons?.icon) {
producerMarkup += `
`;
} else {
producerMarkup += `
${prod.producer.name} `;
}
producerMarkup += producerMarkup =
isLinkIncluded && prod.producer?.link ? `` : `
`;
return producerMarkup;
}
markupSeries(prod) {
if (!this.options.showSeries) return "";
if (!prod?.series?.name) return `
`;
let seriesMarkup = "";
if (this.options.showSeries === "link" && prod.series.link) {
seriesMarkup = `
${prod.series.name}
`;
} else if (
typeof this.options.showSeries === "boolean" ||
!prod.series.link
) {
seriesMarkup = `
${prod.series.name}
`;
}
return seriesMarkup;
}
markupPrice(prod, sizeId = false) {
let priceRoot = prod.price;
const getOmnibusDetailsObject = { productData: prod };
// Znalezienie ceny wybranego rozmiaru
if (this.options?.selectSize) {
const currentSizeId =
sizeId ||
prod.sizes?.find((size) => size.amount !== 0)?.id ||
prod.sizes?.size?.[0]?.id;
priceRoot =
prod.sizes.find((size) => size.id === currentSizeId)?.price ||
priceRoot;
getOmnibusDetailsObject.sizeId = currentSizeId;
}
const prodExchangedData =
app_shop?.fn?.getOmnibusDetails?.(getOmnibusDetailsObject) ||
app_shop.fn?.idmGetOmnibusDetails(getOmnibusDetailsObject);
const price = priceRoot.price[this.priceType];
const unit = prod.unit;
const pointsPrice = prod?.points;
const convertedPrice =
priceRoot?.unitConvertedPrice?.[this.priceType]?.formatted;
return `
${price.formatted}
/
${unit?.sellBy}
${
unit?.sellBy > 1 ? unit?.plural : unit?.singular
}
${
convertedPrice && prod.unit?.unitConvertedFormat
? `(${convertedPrice} / ${prod.unit?.unitConvertedFormat}) `
: ""
}
${
pointsPrice
? `
${pointsPrice} pkt. `
: ""
}
${
price.value === 0
? `
${idmHotspotTextObject["Cena na telefon"]} `
: ""
}
${
prodExchangedData?.beforeRebate?.visible
? `
${prodExchangedData?.beforeRebate?.html} `
: ""
}
${
prodExchangedData?.newPriceEffectiveUntil?.visible
? `
${prodExchangedData?.newPriceEffectiveUntil?.html} `
: ""
}
${this.markupOmnibus(prodExchangedData?.omnibus)}
${
prodExchangedData?.max?.visible
? `
${prodExchangedData?.max?.html} `
: ""
}
`;
}
/**
* Tworzy znacznik HTML dla omnibusa.
*
* @param {object} omnibus - dane omnibusa
* @param {string} omnibus.html - kod html zwykłego omnibusa
* @param {string} omnibus.percent - % przeceny omnibusa
* @param {string} omnibus.price - cena omnibusa
* @param {boolean} omnibus.visible - czy omnibus jest widoczny
* @returns {string} Zwraca HTML dla sekcji omnibusa lub pusty string, jeśli niewidoczny.
*/
markupOmnibus(omnibus) {
if (!omnibus?.visible) return "";
if (!this.options.omnibusTooltip)
return `${omnibus?.html} `;
else
return `
${idmHotspotTextObject["Najniższa cena"]}:
${omnibus.price}
${omnibus.percent}
${idmHotspotTextObject["Najniższa cena z 30 dni przed obniżką"]}
`;
}
markupAddToBasket(prod) {
let markup = "";
if (!this.options.addToBasket) return markup;
const prodCurrentSizeAmount = this.options.selectSize
? prod.sizes.find((size) => size.amount !== 0).amount
: prod.sizes[0].amount;
const prodTotalAmount = this.options.selectSize
? this.getProdTotalAmount(prod)
: prodCurrentSizeAmount;
/* Sprawdzanie czy hotpsot znajduje się w formularzu */
const addToBasketHTMLTag = this.isClosestForm ? "div" : "form";
// link do produktu jak nie jest to zwykły produkt
const buttonMarkup = `
${idmHotspotTextObject["Do koszyka"]}
`;
if (prod.type !== "product" || prodTotalAmount === 0)
markup = `Zobacz produkt `;
else if (this.options.addToBasket === "range")
// +-
markup = `<${addToBasketHTMLTag} class="add_to_basket --range" ${
this.isClosestForm ? "" : `action="/basketchange.php" type="post"`
}>
${this.markupSize(prod)}
−
+
${buttonMarkup}
${addToBasketHTMLTag}>`;
// Zwykłe dodanie do koszyka
else
markup = `
<${addToBasketHTMLTag} class="add_to_basket" ${
this.isClosestForm ? "" : `action="/basketchange.php" type="post"`
}>
${this.markupSize(prod)}
${buttonMarkup}
${addToBasketHTMLTag}>`;
return markup;
}
markupSize(prod) {
// return ` `;
if (!this.options?.selectSize || prod.sizes.length === 1)
return ` `;
const sizesName = `${this.id}-${prod.id}`;
let selectedDefault = false;
return `
${prod.sizes.reduce((acc, val, index) => {
const inputId = `${sizesName}-${val.id}`;
const isSelected = !selectedDefault && val?.amount !== 0 ? true : false;
if (isSelected) selectedDefault = true;
return (
acc +
`
${val.name}
`
);
}, "")}
`;
}
markupHotspotContainer() {
return `
${this.markupHotspotInnerDiv()}
`;
}
markupHotspotInnerDiv() {
return `
${
this?.title
? `
${this.title}
`
: ""
}
${this.markupHotspotSwiperContainer()}
`;
}
markupHotspotSwiperContainer(productsHTML = "") {
return `
${productsHTML || this.markupSkeleton()}
`;
}
// SKELETONS MARKUPS
markupSkeleton() {
let skeletonMarkup = "";
// Tworzenie skeletonow
for (let i = 0; i <= this.options.limit; i++) {
skeletonMarkup += this.markupSingleSkeleton();
}
return `
${skeletonMarkup}
`;
}
markupSingleSkeleton() {
return `
${
this.options.showSeries
? `
`
: ""
}
${
this.options.showOpinions
? `
`
: ""
}
${
this.options.addToBasket
? `
`
: ""
}
`;
}
// Banner
markupBannerContainer({ html, position }) {
return `${html}
`;
}
// ========================================================
// HANDLERY ZDARZEŃ
// ========================================================
/**
* Obsługuje dodanie produktu do koszyka (GraphQL).
*/
async handleAddToBasket(e) {
const formEl = e.target.closest(
`${this.isClosestForm ? "div" : "form"}.add_to_basket`
);
if (!formEl) return;
try {
// pobieranie danych i elementów
formEl.classList.add("--loading");
const buttonEl = formEl.querySelector(".add_to_basket__button");
e.preventDefault();
const id = formEl.querySelector("input.product__add_id")?.value;
const size = formEl.querySelector("input.product__add_size")?.value;
const number = formEl.querySelector("input.product__add_number")?.value;
// dodanie do koszyka
const res = await fetch(`/graphql/v1/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: IDM_HOTSPOT_ADD_TO_BASKET(id, size, number),
});
const data = await res.json();
// Błąd
if (data?.data?.addProductsToBasket?.status !== "success")
throw new Error(data);
else {
localStorage.setItem("addedtoBasket", true);
// Obsługiwanie sukcesu
app_shop.fn?.menu_basket_cache();
await app_shop.graphql.trackingEvents(res);
buttonEl.classList.add("--success");
// STRONA KOSZYKA
if (typeof app_shop.fn?.basket?.reloadForm === "function") {
const existingBasketBlockQuantity = document.querySelector(
`.basket__block[data-product-id="${id}"][data-product-size="${size}"] input.buy__more_input.quantity__input[type="number"]`
);
// Dodanie do ilości produktu jeśli już był dodany do koszyka
if (existingBasketBlockQuantity)
existingBasketBlockQuantity.value =
+existingBasketBlockQuantity.value + number;
// Przeładowanie koszyka na stronie basketedit.html
app_shop.fn?.basket?.reloadForm();
}
buttonEl.innerHTML = `${buttonEl.dataset.success} `;
setTimeout(() => {
buttonEl.innerHTML = `${buttonEl.dataset.text} `;
buttonEl.classList.remove("--success");
}, 3000);
}
} catch (err) {
console.error(err);
Alertek.Error(
idmHotspotTextObject[
"Coś poszło nie tak podczas dodawania do koszyka. Spróbuj ponownie lub odśwież stronę"
]
);
buttonEl.innerHTML = `${buttonEl.dataset.error} `;
buttonEl.classList.add("--error");
setTimeout(() => {
buttonEl.classList.remove("--error");
buttonEl.innerHTML = `${buttonEl.dataset.text} `;
}, 3000);
} finally {
formEl.classList.remove("--loading");
}
}
/**
* Obsługuje dodanie produktu do Porównania
*/
async handleAddToCompare(e) {
const compareBtnEl = e.target.closest(".idm-products-banner__compare-btn");
const compareId = compareBtnEl?.dataset?.compareId;
if (!compareBtnEl || !compareId) return;
e.preventDefault();
try {
compareBtnEl.classList.add("--loading");
const compareUrl = `/${
app_shop?.vars?.language?.symbol || "pl"
}/settings.html?comparers=add&product=${compareId}`;
const res = await fetch(compareUrl);
console.log(res);
if (!res.ok) throw new Error(`${idmHotspotTextObject["Wystąpił błąd"]}`);
compareBtnEl.classList.add("--success");
const compareContainerQuery = "#menu_compare_product";
if (document.querySelector(compareContainerQuery)) {
app_shop.fn?.load(
window.location.pathname,
[[compareContainerQuery, compareContainerQuery]],
function () {},
"?set_render=content"
);
}
setTimeout(() => {
compareBtnEl.classList.remove("--success");
}, 2000);
} catch (err) {
console.error(err);
Alertek.Error(
`${idmHotspotTextObject["Coś poszło nie tak. Spróbuj ponownie później"]}`
);
} finally {
compareBtnEl.classList.remove("--loading");
}
}
/**
* Obsługuje dodanie produktu do Listy zakupowej
*/
handleAddToFav(e) {
const favEl = e.target.closest(".product__favorite");
if (!favEl) return;
this.addToFavFn([[favEl.dataset.productId, favEl.dataset.productSize]]);
}
/**
* Obsługuje kliknięcia w przyciski +/− przy wyborze ilości.
*/
handleQuantityButtonClick(e) {
if (e.target.classList.contains("idm-products-banner__qty-input"))
return e.target.select();
const wrapper = e.target.closest(".idm-products-banner__qty");
const input = wrapper.querySelector(".idm-products-banner__qty-input");
const step = parseFloat(wrapper.dataset.sellBy || "1");
const precision = parseInt(wrapper.dataset.precision || "0");
const max = parseFloat(wrapper.dataset.max || "999999");
let current = parseFloat(input.value) || 0;
if (e.target.classList.contains("idm-products-banner__qty-increase")) {
current += step;
if (current > max) {
current = max;
this.rangeMaxAlert(max);
}
} else if (
e.target.classList.contains("idm-products-banner__qty-decrease")
) {
current -= step;
if (current < step) {
current = step;
this.rangeMinAlert(step);
}
}
input.value = current.toFixed(precision);
}
/**
* Obsługuje Pokazywanie zdjęcia na hover
*/
handleShowSecondImage(e) {
const prodIconEl = e.target.closest(".product__icon");
if (!prodIconEl) return;
prodIconEl.classList.add("--toggle-icon");
}
handleHideSecondImage(e) {
const prodIconEl = e.target.closest(".product__icon");
if (!prodIconEl) return;
prodIconEl.classList.remove("--toggle-icon");
}
/**
* Walidacja zmian ilości w polu input.
*/
handleQuantityInputChange(e) {
if (e.target.value > +e.target.max) {
this.rangeMaxAlert(e.target.max);
e.target.value = +e.target.max;
} else if (e.target.value < +e.target.min) {
this.rangeMinAlert(e.target.min);
e.target.value = +e.target.min;
}
}
handleSelectVersion(e) {
if (e.target.closest(".product__version_more")) return;
e.preventDefault();
const closestVersion = e.target.closest(
".product__version_single:not(.--active)"
);
const prodEl = e.target.closest(".product");
if (!closestVersion || !prodEl) return;
this.reloadProduct(prodEl, closestVersion.dataset.productId);
}
handleSelectSize(e) {
const inputEl = e.target.closest("input[type='radio']");
if (!inputEl) return;
//1. Zmiana Wybranego inputa
const sizeId = inputEl?.dataset?.value;
const hiddenSizeInputEl = e.target
.closest("form.add_to_basket")
?.querySelector("input.product__add_size");
if (!hiddenSizeInputEl || !sizeId) return (inputEl.checked = false);
hiddenSizeInputEl.value = sizeId;
// 2. Zmiana ceny
const productEl = e.target.closest(".product.hotspot__product");
const productData = this.searchForProductData(+productEl?.dataset.id);
const currSizeData = productData?.sizes.find((size) => size.id === sizeId);
if (!currSizeData || !productData) return;
const priceEl = productEl.querySelector(".product__prices");
if (!priceEl) return;
priceEl.outerHTML = this.markupPrice(productData, sizeId);
this.setHeightDefault();
// 3. Zmiana maksymalnej ilości produktu dozwolonego do kupienia dla range
if (this.options.addToBasket === "range") {
const qtyContainer = productEl.querySelector(".idm-products-banner__qty");
const qtyInput = qtyContainer?.querySelector(
"input.idm-products-banner__qty-input"
);
if (!qtyContainer || !qtyInput)
return console.error("Brak elementów qty");
qtyContainer.dataset.max = currSizeData.amount;
qtyInput.max = currSizeData.amount;
if (qtyInput.value > currSizeData.amount)
qtyInput.value =
currSizeData.amount - (currSizeData.amount % +qtyInput.step);
}
}
/**
* Lazy-load hotspotu – wczytuje dane dopiero, gdy element pojawi się w viewportcie.
*/
handleObserveHotspotOnce() {
if (!this.hotspotEl) return;
const observer = new IntersectionObserver(
(entries, obs) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.fillHotspot(); // run your callback
obs.disconnect(); // stop observing after first trigger
}
});
},
{ root: null, rootMargin: "0px", threshold: 0.1 }
);
observer.observe(this.hotspotEl);
}
// ========================================================
// USTAWIANIE ZMIENNYCH CSS DLA RAMKI
// ========================================================
// Ustawia się je w dwa sposoby - jako zmienne css przypisywane do ramki + jako dodaktowy tag style
cssSetAllVariables() {
this.cssVariableVersionColumnCount();
this.cssVariableNameClamp();
}
cssSetAllTags() {
this.cssInsertStyleTag();
}
cssVariableNameClamp() {
if (this.cssVariables?.nameClamp)
this.cssSetVariable("--hotspot-name-clamp", this.cssVariables.nameClamp);
}
cssVariableVersionColumnCount() {
if (!this.options?.selectVersion) return false;
this.cssSetVariable(
"--version-desktop-columns",
this.cssVariables?.version?.columnDesktop || 5
);
this.cssSetVariable(
"--version-tablet-columns",
this.cssVariables?.version?.columnTablet || 4
);
this.cssSetVariable(
"--version-mobile-columns",
this.cssVariables?.version?.columnMobile || 4
);
}
cssSetVariable(name, value) {
this.hotspotEl.style.setProperty(name, value);
}
cssVersionColumnStyle() {
return `
@media (max-width: 756px){
#${this.hotspotEl.id}.hotspot__wrapper.idm__hotspot .product__version_single:nth-of-type(n + ${this.cssVariables?.version?.columnMobile}){
display: none;
}
}
@media (min-width: 757px) and (max-width: 978px){
#${this.hotspotEl.id}.hotspot__wrapper.idm__hotspot .product__version_single:nth-of-type(n + ${this.cssVariables?.version?.columnTablet}){
display: none;
}
}
@media (min-width: 979px){
#${this.hotspotEl.id}.hotspot__wrapper.idm__hotspot .product__version_single:nth-of-type(n + ${this.cssVariables?.version?.columnDesktop}){
display: none;
}
}
`;
}
cssSkeletonStyle() {
let skeletonStyles = "";
const skeletonStylesObj = {};
// Budowanie skeletona w zależności od ustawień hotspota
if (this.options.swiper) {
skeletonStylesObj[0] = this.options.swiper?.slidesPerView;
for (const [key, value] of Object.entries(
this.options.swiper.breakpoints
)) {
if (value.slidesPerView) skeletonStylesObj[key] = value.slidesPerView;
}
} else {
// mobile 2
skeletonStylesObj[0] = 2;
// desktop + tablet 4
skeletonStylesObj[757] = 4;
}
for (const [key, value] of Object.entries(skeletonStylesObj)) {
let style = `
#${
this.hotspotEl.id
}.hotspot__wrapper.idm__hotspot.--hotspot-loading .idm_hotspot__skeleton_product{
width: ${(1 / value) * 100}%
}
`;
skeletonStyles +=
key === 0
? style
: `
@media (min-width: ${key}px){
${style}
}
`;
}
return skeletonStyles;
}
cssInsertStyleTag() {
this.hotspotEl.insertAdjacentHTML(
"beforeend",
`
`
);
}
cssSetAll() {
this.cssSetAllVariables();
this.cssSetAllTags();
}
// ========================================================
// FUNKCJE POMOCNICZE
// ========================================================
getProdTotalAmount(prod) {
return prod.sizes.reduce(
(acc, val) => (val.amount === -1 || acc === -1 ? -1 : acc + val.amount),
0
);
}
/**
* Wyświetla alert o maksymalnej ilości produktu.
*/
rangeMaxAlert(max) {
Alertek.Error(
`${idmHotspotTextObject["Maksymalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]} ${max}`
);
}
/**
* Wyświetla alert o minimalnej ilości produktu.
*/
rangeMinAlert(min) {
Alertek.Error(
`${idmHotspotTextObject["Minimalna liczba sztuk tego towaru które możesz dodać do koszyka to:"]} ${min}`
);
}
/**
* Ustawia jednakową wysokość elementów (np. nazw lub cen).
*/
setHeight(options) {
const { selector, selectors, container } = options || {};
if ((!selector && !selectors) || !container) return;
const containerElement = document.querySelector(container);
if (!containerElement) return;
const adjustAllHeights = (itemSelector) => {
const targets = containerElement.querySelectorAll(itemSelector);
if (!targets.length) return;
targets.forEach((el) => (el.style.minHeight = ""));
const max = Math.max(...[...targets].map((el) => el.offsetHeight || 0));
targets.forEach((el) => (el.style.minHeight = `${max}px`));
};
if (selector) adjustAllHeights(selector);
if (selectors?.length) selectors.forEach(adjustAllHeights);
}
setHeightDefault() {
this.setHeight({
selectors: [
`#${this.id} .product__prices`,
`#${this.id} .product__name`,
`#${this.id} .product__series`,
`#${this.id} .product__producer`,
],
container: `#${this.id} .products__wrapper`,
});
}
/**
* Przeładowanie pojedynczego produktu
*/
async reloadProduct(prodEl, newProdId) {
try {
prodEl.classList.add("--loading");
let productData = this.searchForProductData(newProdId);
if (!productData) {
const res = await fetch(`/graphql/v1/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: IDM_PRODUCT_GQL(`productId: ${newProdId}`, this.priceType),
});
const data = await res.json();
productData = data?.data?.product?.product;
if (productData) this.productsVersions.push(productData);
}
if (!productData)
throw new Error("Nie udało się pobrać danych o produkcie");
const prodHTML = this.markupProductInnerHTML(productData);
prodEl.dataset.id = newProdId;
prodEl.innerHTML = prodHTML;
if (productData.price.price[this.priceType].value === 0)
prodEl.classList.add("--phone");
else prodEl.classList.remove("--phone");
this.initSingleEvent(prodEl);
this.setHeightDefault();
} catch (err) {
Alertek?.Error(idmHotspotTextObject["Błąd przy pobieraniu danych"]);
console.error(err);
} finally {
prodEl.classList.remove("--loading");
}
}
searchForProductData(searchedId) {
let productData;
productData =
this.products.find((prod) => prod.id === +searchedId) ||
this.productsVersions.find((prod) => prod.id === +searchedId) ||
null;
return productData;
}
// ========================================================
// XML to GraphQL
// ========================================================
xmlGetGrossNetPrices({ priceNode, name }) {
return {
gross: {
value:
priceNode?.getAttribute(`${name}`) !== null
? +priceNode.getAttribute(`${name}`)
: undefined,
formatted: priceNode?.getAttribute(`${name}_formatted`),
},
net: {
value:
priceNode?.getAttribute(`${name}_net`) !== null
? +priceNode.getAttribute(`${name}_net`)
: undefined,
formatted: priceNode?.getAttribute(`${name}_net_formatted`),
},
};
}
xmlGetPriceFromNode(priceNode) {
const priceObj = {
price: {
gross: {
value:
priceNode?.getAttribute("value") !== null
? +priceNode.getAttribute("value")
: undefined,
formatted: priceNode?.getAttribute("price_formatted"),
},
net: {
value:
priceNode?.getAttribute("price_net") !== null
? +priceNode.getAttribute("price_net")
: undefined,
formatted: priceNode?.getAttribute("price_net_formatted"),
},
},
rebateCodeActive:
priceNode?.getAttribute("rebate_code_active") === "y" ? true : false,
omnibusPrice: {
...this.xmlGetGrossNetPrices({ priceNode, name: "omnibus_price" }),
},
// depositPrice: {},
// depositPriceUnit: {},
// totalDepositPrice: {},
// totalDepositPriceUnit: {},
omnibusPriceDetails: {
// unit: {},
youSave: {
...this.xmlGetGrossNetPrices({ priceNode, name: "omnibus_yousave" }),
},
youSavePercent:
priceNode?.getAttribute("price_net") !== null
? +priceNode.getAttribute("omnibus_yousave_percent")
: undefined,
omnibusPriceIsHigherThanSellingPrice:
priceNode?.getAttribute(
"omnibus_price_is_higher_than_selling_price"
) === "true"
? true
: false,
// newPriceEffectiveUntil: {},
},
tax: {
// worth: {},
vatPercent:
priceNode?.getAttribute("tax") !== null
? +priceNode.getAttribute("tax")
: undefined,
// vatString: ""
},
beforeRebate: {
...this.xmlGetGrossNetPrices({ priceNode, name: "beforerebate" }),
},
// beforeRebateDetails: {
// youSave: {
// },
// youSavePercent: "",
// unit: {}
// },
// crossedPrice: {},
youSave: {
...this.xmlGetGrossNetPrices({ priceNode, name: "yousave" }),
},
youSavePercent:
priceNode?.getAttribute("yousave_percent") !== null
? +priceNode.getAttribute("yousave_percent")
: undefined,
// unit: {},
max: {
...this.xmlGetGrossNetPrices({ priceNode, name: "maxprice" }),
},
// maxPriceUnit: {},
// suggested: {},
unitConvertedPrice: {
...this.xmlGetGrossNetPrices({
priceNode,
name: "unit_converted_price",
}),
},
// rebateNumber: {},
lastPriceChangeDate: priceNode?.getAttribute("last_price_change_date"),
// advancePrice: {},
promotionDuration: {
promotionTill: {
date: {
date: priceNode
?.getAttribute("last_price_change_date")
?.split("-")?.[2],
month: priceNode
?.getAttribute("last_price_change_date")
?.split("-")?.[1],
year: priceNode
?.getAttribute("last_price_change_date")
?.split("-")?.[0],
// weekDay: "",
// formatted: ""
},
time: {
hour: priceNode?.getAttribute("promotiontillhour")?.split(":")?.[0],
minutes: priceNode
?.getAttribute("promotiontillhour")
?.split(":")?.[1],
seconds: priceNode
?.getAttribute("promotiontillhour")
?.split(":")?.[2],
},
// timestamp: ""
},
// discountTill: {},
// distinguishedTill: {},
// specialTill: {},
},
// subscriptionPrice: {},
};
return priceObj;
}
xmlGetTraitsFromNode(node) {
const awardedParameters = [];
node.querySelectorAll(":scope > trait").forEach((trait) => {
const currParameter = awardedParameters.find(
(param) => param.id === trait.getAttribute("groupid")
) || {
id: trait.getAttribute("groupid"),
name: trait.getAttribute("groupdescription"),
// description: "",
values: [],
// contextValue: "",
// search: {
// icon: "",
// },
};
currParameter.values.push({
id: trait.getAttribute("traitid"),
name: trait.getAttribute("traitid"),
link: trait.getAttribute("link"),
// description: "",
// search: {
// icon: ""
// }
});
if (currParameter.values.length === 1)
awardedParameters.push(currParameter);
});
return awardedParameters;
}
async xmlGetData() {
if (!this.source?.link) return null;
try {
const res = await fetch(
`${this.source?.link}${
this.source?.link.includes("?")
? "&getProductXML=t"
: "?getProductXML=t"
}`
);
const str = await res.text();
const xml = new window.DOMParser().parseFromString(str, "text/xml");
const now = new Date();
const allProducts = xml.querySelectorAll("product");
if (allProducts.length === 0) throw new Error("Nie znaleziono produktów");
const data = [];
allProducts.forEach((prod, index) => {
if (this.options.limit <= index) return;
// NODES
const priceNode = prod.querySelector(":scope >price");
const firmNode = prod.querySelector(":scope > firm");
const categoryNode = prod.querySelector(":scope > category");
const seriesNode = prod.querySelector(":scope > series");
const commentsNode = prod.querySelector(":scope > comments");
const sizesNode = prod.querySelector(":scope > sizes");
const versionsNode = prod.querySelector(":scope > versions");
// STREFY
const zones = [];
if (prod.getAttribute("promo") === "yes") zones.push("promotion");
if (prod.getAttribute("discount") === "yes") zones.push("discount");
if (prod.getAttribute("distinguished") === "yes")
zones.push("distinguished");
if (prod.getAttribute("special") === "yes") zones.push("special");
if (prod.getAttribute("new") === "yes") zones.push("new");
if (prod.getAttribute("bestseller") === "yes") zones.push("bestseller");
if (prod.getAttribute("subscription") === "yes")
zones.push("subscription");
// ZDJĘCIA
const enclosuresImages = [];
prod
.querySelectorAll(":scope > enclosures > images > enclosure")
.forEach((img) => {
enclosuresImages.push({
position: img.getAttribute("position"),
type: img.getAttribute("type"),
typeSecond: img.getAttribute("type_second"),
url: img.getAttribute("url"),
urlSecond: img.getAttribute("url_second"),
width: img.getAttribute("width"),
height: img.getAttribute("height"),
iconUrl: img.getAttribute("icon"),
iconUrlSecond: img.getAttribute("icon_second"),
iconWidth: img.getAttribute("icon_width"),
iconHeight: img.getAttribute("icon_height"),
mediumUrl: img.getAttribute("medium"),
mediumUrlSecond: img.getAttribute("medium_second"),
mediumWidth: img.getAttribute("medium_width"),
mediumHeight: img.getAttribute("medium_height"),
});
});
// SIZES
const sizes = [];
sizesNode.querySelectorAll(":scope > size").forEach((size) => {
const availabilityNode = size.querySelector(":scope > availability");
const shippingTimeNode = availabilityNode.querySelector(
":scope > shipping_time"
);
const sizePriceNode = size.querySelector(":scope > price");
const weightNode = size.querySelector(":scope > weight");
sizes.push({
id: size?.getAttribute("type"),
name: size?.getAttribute("name"),
code: size?.getAttribute("code"),
codeProducer: size?.getAttribute("code_producer"),
codeExtern: size?.getAttribute("code_extern"),
amount:
size?.getAttribute("amount") !== null
? +size.getAttribute("amount")
: undefined,
amount_mo:
size?.getAttribute("amount_mo") !== null
? +size.getAttribute("amount_mo")
: undefined,
amount_mw:
size?.getAttribute("amount_mw") !== null
? +size.getAttribute("amount_mw")
: undefined,
amount_mp:
size?.getAttribute("amount_mp") !== null
? +size.getAttribute("amount_mp")
: undefined,
weight:
weightNode?.getAttribute("g") !== null
? +weightNode.getAttribute("g")
: undefined,
availability: {
visible:
availabilityNode?.getAttribute("visible") === "y"
? true
: false,
description: availabilityNode?.getAttribute("status_description"),
status: availabilityNode?.getAttribute("status"),
icon: availabilityNode?.getAttribute("status_gfx"),
deliveryDate:
shippingTimeNode?.getAttribute("today") === "true"
? `${now.getFullYear()}-${now.getMonth() + 1}-${now
.getDate()
.toString()
.padStart(2, 0)}`
: availabilityNode?.getAttribute("delivery_date"),
minimumStockOfProduct: availabilityNode?.getAttribute(
"minimum_stock_of_product"
),
// descriptionTel: "",
// iconTel: "",
},
shipping: {
today:
shippingTimeNode?.getAttribute("today") === "true"
? true
: false,
},
price: this.xmlGetPriceFromNode(sizePriceNode),
// amountWholesale: "",
});
});
// VERSIONS
const versions = [];
versionsNode.querySelectorAll(":scope > version").forEach((ver) => {
versions.push({
id:
ver?.getAttribute("id") !== null
? +ver?.getAttribute("id")
: undefined,
name: ver?.getAttribute("name"),
icon: ver?.getAttribute("gfx"),
iconSecond: ver?.getAttribute("gfx_second"),
iconSmall: ver?.getAttribute("gfx_small"),
iconSmallSecond: ver?.getAttribute("gfx_small_second"),
productIcon: ver?.getAttribute("icon"),
productIconSecond: ver?.getAttribute("icon_second"),
productIconSmall: ver?.getAttribute("icon_small"),
productIconSmallSecond: ver?.getAttribute("icon_small_second"),
link: ver?.getAttribute("link"),
// parameterValues: [],
});
});
// DATA
const typeParts = prod?.getAttribute("product_type").split("_");
data.push({
id:
prod?.getAttribute("id") !== null
? +prod?.getAttribute("id")
: undefined,
type: typeParts[1] === "item" ? typeParts[0] : typeParts[1],
code: prod?.getAttribute("code"),
name: prod.querySelector(":scope > name")?.textContent,
versionName: prod
.querySelector(":scope > versions")
?.getAttribute("name"),
description: prod.querySelector(":scope > description")?.textContent,
// longDescription: prod.querySelector("vlongdescription")?.textContent,
// longDescriptionSections: "",
link: prod?.getAttribute("link"),
zones,
icon: prod.querySelector(":scope > icon")?.textContent,
iconSecond: prod.querySelector(":scope > icon_second")?.textContent,
iconSmall: prod.querySelector(":scope > icon_small")?.textContent,
iconSmallSecond: prod.querySelector(":scope > icon_small_second")
?.textContent,
price: this.xmlGetPriceFromNode(priceNode),
unit: {
id: sizesNode?.getAttribute("unit_id"),
name: sizesNode?.getAttribute("unit"),
singular: sizesNode?.getAttribute("unit_single"),
plural: sizesNode?.getAttribute("unit_plural"),
fraction: sizesNode?.getAttribute("unit_fraction"),
sellBy:
sizesNode?.getAttribute("unit_sellby") !== null
? +sizesNode?.getAttribute("unit_sellby")
: undefined,
precision:
sizesNode?.getAttribute("unit_precision") !== null
? +sizesNode?.getAttribute("unit_precision")
: undefined,
unitConvertedFormat: sizesNode?.getAttribute(
"unit_converted_format"
),
},
producer: {
id:
firmNode?.getAttribute("id") !== null
? +firmNode?.getAttribute("id")
: undefined,
name: firmNode?.getAttribute("name"),
link: firmNode?.getAttribute("productslink"),
searchIcons: {
icon: firmNode?.getAttribute("icon"),
},
// projectorIcons: {},
},
category: {
id:
categoryNode?.getAttribute("id") !== null
? +categoryNode?.getAttribute("id")
: undefined,
name: categoryNode?.getAttribute("name"),
link: categoryNode?.getAttribute("productslink"),
},
group: {
id:
versionsNode?.getAttribute("id") !== null
? +versionsNode?.getAttribute("id")
: undefined,
name: versionsNode?.getAttribute("name"),
displayAll:
versionsNode?.getAttribute("display_all") === "true"
? true
: false,
link: versionsNode?.getAttribute("link"),
versions,
groupParameters: this.xmlGetTraitsFromNode(
versionsNode.querySelector(":scope > groupParameters")
),
},
opinion: {
rating:
commentsNode?.getAttribute("avg") !== null
? +commentsNode?.getAttribute("avg")
: undefined,
count:
commentsNode?.getAttribute("count") !== null
? +commentsNode?.getAttribute("count")
: undefined,
link: commentsNode?.getAttribute("link"),
},
enclosuresImages,
// enclosuresAttachments: [],
series: {
id:
seriesNode?.getAttribute("id") !== null
? +seriesNode?.getAttribute("id")
: undefined,
name: seriesNode?.getAttribute("name"),
link: seriesNode?.getAttribute("link"),
},
awardedParameters: this.xmlGetTraitsFromNode(
prod.querySelector(":scope > traits")
),
// parameteresWithContext: [],
sizes,
points:
priceNode?.getAttribute("points") !== null
? +priceNode?.getAttribute("points")
: undefined,
pointsReceive:
priceNode?.getAttribute("points_recive") !== null
? +priceNode?.getAttribute("points_recive")
: undefined,
// subscription: {},
// bundled: [],
// responsibleEntity: {},
});
});
return data;
} catch (err) {
console.error(err);
return null;
}
}
// ========================================================
// INICJALIZACJA
// ========================================================
/**
* Wykonywana po pełnej inicjalizacji hotspotu (Swiper, eventy, wysokości).
*/
async afterInit() {
try {
if (!this.hotspotEl) throw new Error("Nie znaleziono elementu");
if (
this.title &&
!this.hotspotEl.querySelector(".hotspot__name.headline__wrapper")
) {
this.hotspotEl
.querySelector(".hotspot.--initialized")
?.insertAdjacentHTML(
"afterbegin",
`
${this.title}
`
);
}
if (this.banner) this.placeBanners();
await this.initSwiper();
// IDM setHeight
this.setHeightDefault();
this.initEvents();
console.log(`Initialized hotspot #${this.id}`);
// funkcja wykonująca się po ramce rekomendacji
if (typeof this.options?.callbackFn === "function")
this.options?.callbackFn(this);
} catch (err) {
console.error(
idmHotspotTextObject[
"Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"
],
err
);
}
}
placeBanners() {
if (!this.placeBanners) return;
const productsWrapperEl =
this.hotspotEl.querySelector(".products__wrapper");
for (let [key, value] of Object.entries(this.banner)) {
const currBannerMarkup = this.markupBannerContainer({
html: value?.html || "",
position: key.split("-")[0],
});
if (key === "top") {
productsWrapperEl.insertAdjacentHTML("beforebegin", currBannerMarkup);
} else if (key === "left") {
productsWrapperEl.insertAdjacentHTML("beforebegin", currBannerMarkup);
} else if (key === "right") {
productsWrapperEl.insertAdjacentHTML("afterend", currBannerMarkup);
} else if (key === "bottom") {
productsWrapperEl.insertAdjacentHTML("afterend", currBannerMarkup);
} else {
const slidePosition = Number(key.split("-")[1]) - 1;
if (!Number.isInteger(slidePosition)) return;
const allProducts = productsWrapperEl.querySelectorAll(".product");
let productIndex = slidePosition;
if (slidePosition > allProducts.length - 1)
productIndex = allProducts.length - 1;
if (slidePosition < 0) productIndex = 0;
const bannerPosition = slidePosition < 0 ? "beforebegin" : "afterend";
allProducts[productIndex]?.insertAdjacentHTML(
bannerPosition,
currBannerMarkup
);
}
value?.callbackFn?.(this);
}
}
afterInitOnce() {
if (this.initialized) return;
// WCZYTANIE PONOWNIE DLA KOSZYKA
if (
typeof app_shop.fn?.basket?.reloadForm === "function" &&
this.hotspotEl.closest("#content")
) {
app_shop.run(
() => {
this.init();
console.log("test", this.hotspotEl);
},
"all",
"#Basket",
true
);
}
this.initialized = true;
}
initExternalFunctions() {
this.addToFavFn = app_shop.fn?.shoppingList?.addProductToList;
}
/**
* Pobiera dane, wypełnia markup i inicjuje Swipera.
*/
async fillHotspot() {
// Zdefiniowanie funkcji do dodawania do ulubionych
try {
if (!this.products) {
if (
(!this?.query?.graphFn || !this?.query?.string) &&
!this.source?.link
)
this.setQueryData();
// pobranie danych o produktach
await this.getHotspotData();
if (!this.products)
throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]);
}
// Skeleton
this.hotspotEl.querySelector(".idm_hotspot__skeleton")?.remove();
this.hotspotEl.classList.remove("--hotspot-loading");
this.initExternalFunctions();
// Wstawienie markupa na strone
if (this.hotspotEl.querySelector(".products.hotspot__products"))
this.hotspotEl
.querySelector(".products.hotspot__products")
.insertAdjacentHTML("beforeend", this.markup());
else if (this.hotspotEl.querySelector(".hotspot")) {
this.hotspotEl
.querySelector(".hotspot")
?.insertAdjacentHTML(
"beforeend",
this.markupHotspotSwiperContainer(this.markup())
);
} else {
throw new Error("Nie udało się wstawić produktów! Zła struktura HTML");
}
// init swiper + add to basket
await this.afterInit();
this.afterInitOnce();
} catch (err) {
console.error(idmHotspotTextObject["Wystąpił błąd"], err);
this.hotspotEl.remove();
}
}
/**
* Inicjuje instancję Swipera dla hotspotu.
*/
async initSwiper(isReinitialized) {
try {
// swiper || slick
if (this.options?.swiper) {
// Wywołanie swipera
this.swiperFn = new HotspotSlider({
selector: `#${this.id} .swiper`,
hotspotName: `${this.id}`,
options: this.options.swiper,
});
await this.swiperFn.init();
// // //Wywołanie swipera v2 (nie działa jeszcze)
// const { Slider } = await appModules?.load("Slider");
// if(!Slider) throw new Error("Brak modułu slidera")
// const idmSwiper = new Slider();
// await idmSwiper.init(this.options.swiper);
// if(!this.hotspotEl.querySelector(".swiper")) throw new Error("Brak elementu swipera")
// this.swiperFn = await idmSwiper.setupSlider({ element: this.hotspotEl.querySelector(".swiper") });
if (this.options.swiperScrollbar && !isReinitialized)
this.swiperFnProgress = new IdmSwiperProgress(
this.swiperFn,
`#${this.id} .swiper`
);
else if (this.options.swiperScrollbar && isReinitialized)
this.swiperFnProgress.reattachSwiperFn(this.swiperFn);
}
} catch (err) {
console.error(
idmHotspotTextObject[
"Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"
],
err
);
}
}
async reInitSwiper(differentOptions) {
try {
if (differentOptions) this.options.swiper = differentOptions;
this.swiperFn?.slider?.slider?.destroy?.();
this.initSwiper(true);
} catch (err) {
console.error(
idmHotspotTextObject[
"Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"
],
err
);
}
}
/**
* Inicjuje eventy dla produktów w hotspotcie.
*/
initEvents() {
this.hotspotEl.querySelectorAll(".product").forEach((prodEl) => {
this.initSingleEvent(prodEl);
});
}
initSingleEvent(prodEl) {
// DODAWANIE DO KOSZYKA
if (this.options?.addToBasket) {
// Warunek this.isClosestForm w przypadku gdy hotspot znajduje się już w środku formularza
const addToBasketEl = prodEl.querySelector(
`${
this.isClosestForm
? "button.add_to_basket__button"
: "form.add_to_basket"
}`
);
addToBasketEl?.addEventListener(
`${this.isClosestForm ? "click" : "submit"}`,
this.handleAddToBasket
);
// + -
if (this?.options?.addToBasket === "range") {
addToBasketEl
?.closest(".add_to_basket")
?.querySelector(".idm-products-banner__qty")
?.addEventListener("click", this.handleQuantityButtonClick);
addToBasketEl
?.closest(".add_to_basket")
?.querySelector(".idm-products-banner__qty-input")
?.addEventListener("input", this.handleQuantityInputChange);
}
}
// Dodaj do ulubionych
if (this.options?.addToFavorites && typeof this.addToFavFn === "function")
prodEl
.querySelector(".product__favorite")
?.addEventListener("click", this.handleAddToFav);
// Porównanie
if (this.options?.addToCompare)
prodEl
.querySelector(".idm-products-banner__compare-btn")
?.addEventListener("click", this.handleAddToCompare);
// Hover drugie zdjęcie
if (this.options?.showSecondImage) {
const prodIconEl = prodEl.querySelector(".product__icon");
if (prodIconEl.querySelector(".product__image.--second")) {
prodIconEl?.addEventListener("mouseover", this.handleShowSecondImage);
prodIconEl?.addEventListener("mouseleave", this.handleHideSecondImage);
}
}
// Wybór wersji
if (this.options?.selectVersion)
prodEl
.querySelector(".product__versions")
?.addEventListener("click", this.handleSelectVersion);
// Wybór rozmiaru
if (this.options?.selectSize)
prodEl
.querySelector(".product__select_sizes")
?.addEventListener("click", this.handleSelectSize);
}
/**
* Inicjuje kontener hotspotu w określonym miejscu DOM.
*/
initHotspotContainer() {
const selectedEl = document.querySelector(this?.placement.selector);
if (!selectedEl)
throw new Error(idmHotspotTextObject["Nie znaleziono kontenera"]);
const markup = this.markupHotspotContainer();
selectedEl.insertAdjacentHTML(this.placement.insert || "afterend", markup);
this.hotspotEl = document.getElementById(this.id);
}
/**
* Główna metoda inicjalizująca hotspot (lazy lub natychmiast).
*/
init() {
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const dev = urlParams.get("dev");
if (this.options?.devMode && dev !== "true")
return console.error(
`Brak włączonego devMode. Ramka ${this.id} nie mogła zostać utworzona!`
);
// Opcje swipera
if (typeof this?.options?.swiper === "boolean")
this.options.swiper = this.getDefaultSwiperOptions();
// Wstawienie kontenera
if (!this.hotspotEl || !document.contains(this.hotspotEl))
this.initHotspotContainer();
else if (!this.hotspotEl.querySelector(".hotspot"))
this.hotspotEl.innerHTML = this.markupHotspotInnerDiv();
else if (!this.hotspotEl.querySelector(".products__wrapper"))
this.hotspotEl
.querySelector(".hotspot")
.insertAdjacentHTML("beforeend", this.markupHotspotSwiperContainer());
// Ustawienie wszystkich zmiennych CSS
this.cssSetAll();
// Sprawdzenie czy hotspot znajduje się w formularzu(dla doawania do koszyka)
this.isClosestForm = !!this.hotspotEl.closest("form");
if (this.options?.lazy) this.handleObserveHotspotOnce();
else this.fillHotspot();
}
}
/*
==============================================================
SWIPER PASEK
==============================================================
*/
class IdmSwiperProgress {
constructor(swiper, selector) {
this.swiper = swiper?.slider?.slider ?? swiper?.slider ?? swiper;
this.selector = selector;
this.scrollbarEl = null;
this.progressEl = null;
this.isDragging = false;
this.init();
}
init() {
const el = document.querySelector(this.selector);
if (!el || el.querySelector(".idm-scrollbar")) return;
el.insertAdjacentHTML(
"beforeend",
``
);
this.scrollbarEl = el.querySelector(".idm-scrollbar");
this.progressEl = this.scrollbarEl.querySelector(".idm-progress");
this.updateBarWidth();
this.addDragTracking();
this.swiper.on("progress", () => this.updateProgress());
this.swiper.on("breakpoint", () => {
this.updateBarWidth();
});
}
reattachSwiperFn(newSwiperFn) {
this.swiper =
newSwiperFn?.slider?.slider ?? newSwiperFn?.slider ?? newSwiperFn;
this.progressEl.style.width = "";
this.progressEl.style.left = "";
this.updateBarWidth();
}
updateBarWidth() {
const { slidesPerGroup, slidesPerView } = this.swiper.params;
const totalSlides = this.swiper.slides.length;
const progressWidth =
100 / ((totalSlides - slidesPerView) / slidesPerGroup + 1);
this.progressEl.style.width = `${progressWidth}%`;
if (progressWidth >= 100 || progressWidth <= 0)
this.scrollbarEl.style.display = "none";
else this.scrollbarEl.style.display = "";
}
updateProgress() {
const progress = this.swiper.progress;
const { slidesPerGroup, slidesPerView } = this.swiper.params;
const totalSlides = this.swiper.slides.length;
const progressWidth =
100 / ((totalSlides - slidesPerView) / slidesPerGroup + 1);
const newLeft = (100 - progressWidth) * progress;
this.progressEl.style.left = `${Math.min(
100 - progressWidth,
Math.max(0, newLeft)
)}%`;
}
addDragTracking() {
const handle = this.progressEl;
let grabOffset = 0;
let scrollbarWidth = 0;
let handleWidth = 0;
const startDrag = (e) => {
this.isDragging = true;
this.scrollbarEl.classList.add("--drag-start");
const rect = this.scrollbarEl.getBoundingClientRect();
const handleRect = handle.getBoundingClientRect();
scrollbarWidth = rect.width;
handleWidth = handleRect.width;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
grabOffset = clientX - handleRect.left;
document.addEventListener("mousemove", handleDrag);
document.addEventListener("mouseup", stopDrag);
document.addEventListener("touchmove", handleDrag);
document.addEventListener("touchend", stopDrag);
};
const handleDrag = (e) => {
if (!this.isDragging) return;
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const rect = this.scrollbarEl.getBoundingClientRect();
let newLeftPx = clientX - rect.left - grabOffset;
const maxLeft = scrollbarWidth - handleWidth;
newLeftPx = Math.max(0, Math.min(maxLeft, newLeftPx));
const progress = newLeftPx / maxLeft;
this.swiper.setProgress(progress, 0);
};
const stopDrag = () => {
if (!this.isDragging) return;
this.isDragging = false;
document.removeEventListener("mousemove", handleDrag);
document.removeEventListener("mouseup", stopDrag);
document.removeEventListener("touchmove", handleDrag);
document.removeEventListener("touchend", stopDrag);
this.scrollbarEl.classList.remove("--drag-start");
this.swiper.slideReset(400);
};
handle.addEventListener("mousedown", startDrag);
handle.addEventListener("touchstart", startDrag);
}
}
// ========================================================
// TOOLTIP
// ========================================================
function idmShowTooltip(tooltipEl) {
const tooltipContentEl = tooltipEl.querySelector(".idm_tooltip__content");
if (!tooltipContentEl) return;
tooltipContentEl.classList.add("--visible");
// Logika pokazywania się i chowania tooltipa
let timeoutVar;
function onMouseLeave() {
timeoutVar = idmHideTooltipTimer(tooltipEl);
}
function onMouseEnter() {
clearTimeout(timeoutVar);
}
function onScroll() {
idmHideTooltip(tooltipEl);
}
// Store references for later removal
tooltipEl._onMouseLeave = onMouseLeave;
tooltipEl._onMouseEnter = onMouseEnter;
tooltipEl._onScroll = onScroll;
tooltipEl.addEventListener("mouseleave", onMouseLeave);
tooltipEl.addEventListener("mouseenter", onMouseEnter);
document.addEventListener("scroll", onScroll);
}
function idmHideTooltipTimer(tooltipEl) {
return setTimeout(() => idmHideTooltip(tooltipEl), 1500);
}
function idmHideTooltip(tooltipEl) {
const tooltipContentEl = tooltipEl.querySelector(".idm_tooltip__content");
if (!tooltipContentEl) return;
tooltipContentEl.classList.remove("--visible");
tooltipEl.removeEventListener("mouseleave", tooltipEl._onMouseLeave);
tooltipEl.removeEventListener("mouseenter", tooltipEl._onMouseEnter);
document.removeEventListener("scroll", tooltipEl._onScroll);
delete tooltipEl._onMouseLeave;
delete tooltipEl._onMouseEnter;
delete tooltipEl._onScroll;
}
document.addEventListener("DOMContentLoaded", () => {
document.body.addEventListener("click", (e) => {
const tooltipEl = e.target.closest(".idm_tooltip");
if (!e.target.closest(".idm_tooltip__info_icon") || !tooltipEl) return;
e.preventDefault();
idmShowTooltip(tooltipEl);
});
});
async function idmPrepareHotspotObject(selectedContainerEl) {
selectedContainerEl.classList.add("--init");
const source = {};
if (selectedContainerEl.dataset?.link)
source.link = selectedContainerEl.dataset.link;
else if (selectedContainerEl.dataset?.hotspotsType)
source.hotspotsType = selectedContainerEl.dataset.hotspotsType;
else {
if (selectedContainerEl.dataset?.productsId)
source.productsId = selectedContainerEl.dataset.productsId.split(",");
if (selectedContainerEl.dataset?.productsMenu)
source.productsMenu = selectedContainerEl.dataset.productsMenu;
if (selectedContainerEl.dataset?.producersId)
source.producersId = selectedContainerEl.dataset.producersId;
if (selectedContainerEl.dataset?.seriesId)
source.seriesId = selectedContainerEl.dataset.seriesId;
if (selectedContainerEl.dataset?.parametersId)
source.seriesId = selectedContainerEl.dataset.parametersId;
if (
selectedContainerEl.dataset?.priceFrom &&
selectedContainerEl.dataset?.priceTo
)
source.priceRange = {
from: +selectedContainerEl.dataset.priceFrom,
to: +selectedContainerEl.dataset.priceTo,
};
}
if (Object.keys(source).length === 0) {
console.error("Brak metody pobrania ramek rekomendacji");
selectedContainerEl?.remove();
return;
}
const idmHotspotObj = {
id: selectedContainerEl?.id,
source,
hotspotEl: selectedContainerEl,
};
if (selectedContainerEl?.dataset?.lazy)
idmHotspotObj.options = {
lazy: selectedContainerEl?.dataset?.lazy === "true" ? true : false,
};
new IdmHotspot(idmHotspotObj);
}
document.addEventListener("DOMContentLoaded", () => {
document
.querySelectorAll(".hotspot__wrapper.idm__hotspot:not(.--init)")
.forEach((currentHotspot) => {
idmPrepareHotspotObject(currentHotspot);
});
});