///////////////////////////////////////////////////////////
// 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
producer{
name
}
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: 3,
centeredSlides: true,
centeredSlidesBounds: true,
},
979: {
slidesPerView: 4,
centeredSlides: false,
},
}
}
// ============================
// DOMYŚLNE OPCJE HOTSPOTA
// ============================
static idmDefaultHotspotOptions = {
cssVariables: {
version: {
columnDesktop: 5,
columnTablet: 3,
columnMobile: 4,
},
nameClamp: null// 2,
},
options: {
limit: 8,
lazy: true,
devMode: false,
callbackFn: ()=>{},
omnibusTooltip: false,
// switchImage: false,
// POKAZANIE
showOpinions: false,
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 = {}, hotspotEl, products, cssVariables}){
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.hotspotEl = hotspotEl || null;
// Merge defaults
this.options = {
...IdmHotspot.idmDefaultHotspotOptions.options,
...options,
};
this.cssVariables = {
...IdmHotspot.idmDefaultHotspotOptions.cssVariables,
...cssVariables,
};
//
// 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.init();
// WCZYTANIE PONOWNIE DLA KOSZYKA
if(typeof app_shop.fn?.basket?.reloadForm === "function" && this.hotspotEl.closest("#content")){
app_shop.run(()=>{
this.init();
}, "all", "#Basket", true)
}
}
// ========================================================
// 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;
}
if(!products || !products.length) throw new Error(idmHotspotTextObject["Nie znaleziono produktów"]);
this.products = products;
this.title = this.title || data?.data?.hotspots?.name || "";
}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){
// IDM DO POPRAWKI
const prodExchangedData = app_shop?.fn?.getOmnibusDetails?.({productData: prod}) || app_shop.fn?.idmGetOmnibusDetails({productData: prod});
return `
${this.markupAdditional(prod)}
${this.markupImage(prod)}
${this.markupLabel(prod)}
${this.markupVersions(prod)}
${this.markupOpinions(prod)}
${prod.name}
${this.markupPrice(prod, prodExchangedData)}
${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") return "";
return `
`;
}
markupVersions(prod){
if(!this.options?.selectVersion || !prod.group?.versions || prod.group?.versions?.length === 1 ) return "";
// let MAX_VERSION_AMOUNT = 5;
// if(app_shop.vars.view > 2){
// MAX_VERSION_AMOUNT = this.cssVariables.version.columnDesktop;
// }else if(app_shop.vars.view === 2){
// MAX_VERSION_AMOUNT = this.cssVariables.version.columnTablet;
// }else if(app_shop.vars.view === 1){
// MAX_VERSION_AMOUNT = this.cssVariables.version.columnMobile;
// }
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)=>{
// if(index + 1 > MAX_VERSION_AMOUNT) return acc;
// if(index + 1 === MAX_VERSION_AMOUNT && prod.group.versions.length > MAX_VERSION_AMOUNT)
// return acc + `
// +${(prod.group.versions.length - MAX_VERSION_AMOUNT) + 1}
// `;
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
`
}
markupPrice(prod, prodExchangedData){
const price = prod.price.price[this.priceType];
const unit = prod.unit;
const pointsPrice = prod?.points;
const convertedPrice = prod.price?.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;
// link do produktu jak nie jest to zwykły produkt
if(prod.type !== "product" || prod.sizes[0].amount === 0) markup = `Zobacz produkt`;
else if(this.options.addToBasket === "range") // +-
markup = ``;
else // Zwykłe dodanie do koszyka
markup = `
`;
return markup;
}
markupSize(prod){
return ``;
// if(!this.options?.selectSize || prod.sizes.length === 1) return ``;
// const sizesName = `${this.id}-${prod.id}`;
// return `
//
// ${prod.sizes.reduce((acc, val)=>{
// const inputId = `${sizesName}-${val.id}`;
// return acc + `
//
// `
// }, "")}
//
// `;
}
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.addToBasket ? `
` : ""}
`
}
// ========================================================
// HANDLERY ZDARZEŃ
// ========================================================
/**
* Obsługuje dodanie produktu do koszyka (GraphQL).
*/
async handleAddToBasket(e){
const formEl = e.target.closest("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[name='product']")?.value;
const size = formEl.querySelector("input[type='hidden'][name='size']")?.value;
const number = formEl.querySelector("input[name='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.graphql.trackingEvents(res);
buttonEl.classList.add("--success");
// Dodawanie do koszyka na stronie basketedit.php będzie wymagał innego indywidualnego kodu!!!!!
app_shop.fn?.menu_basket_cache?.();
// 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 + 1;
// 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);
}
/**
* 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(){
this.cssSetVariable("--version-desktop-columns", this.cssVariables?.version?.columnDesktop || 5)
this.cssSetVariable("--version-tablet-columns", this.cssVariables?.version?.columnTablet || 3)
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
// ========================================================
/**
* 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)
}
/**
* Przeładowanie pojedynczego produktu
*/
async reloadProduct(prodEl, newProdId){
try{
prodEl.classList.add("--loading");
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();
const productData = data?.data?.product?.product;
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.setHeight({
selectors: [
`#${this.id} .product__prices`,
`#${this.id} .product__name`,
],
container: `#${this.id} .products__wrapper`,
});
}catch(err){
Alertek?.Error(idmHotspotTextObject["Błąd przy pobieraniu danych"]);
console.error(err);
}finally{
prodEl.classList.remove("--loading");
}
}
// ========================================================
// 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}
`);
}
await this.initSwiper();
// IDM setHeight
this.setHeight({
selectors: [
`#${this.id} .product__prices`,
`#${this.id} .product__name`,
],
container: `#${this.id} .products__wrapper`,
});
this.initEvents();
console.log(`Initialized hotspot #${this.id}`);
// funkcja wykonująca się po ramce rekomendacji
if(typeof this.options?.callbackFn === "function") this.options?.callbackFn();
}catch(err){
console.error(idmHotspotTextObject["Wystąpił błąd z inicjalizacją. Proszę odśwież stronę"], err);
}
}
initExternalFunctions(){
this.addToFavFn = app_shop.fn?.shoppingList?.addProductToList;
}
/**
* Pobiera dane, wypełnia markup i inicjuje Swipera.
*/
async fillHotspot(){
// Zdefiniowanie funkcji do dodawania do ulubionych
this.initExternalFunctions();
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"]);
}
this.hotspotEl.querySelector(".idm_hotspot__skeleton")?.remove();
this.hotspotEl.classList.remove("--hotspot-loading");
// 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
this.afterInit();
}catch(err){
console.error(idmHotspotTextObject["Wystąpił błąd"], err);
this.hotspotEl.remove();
}
}
/**
* Inicjuje instancję Swipera dla hotspotu.
*/
async initSwiper(){
try{
// swiper || slick
if(this.options?.swiper){
// Wywołanie swipera
const selectedSwiper = new HotspotSlider({
selector: `#${this.id} .swiper`,
hotspotName: `${this.id}`,
options: this.options.swiper,
});
await selectedSwiper.init();
if(this.options.swiperScrollbar) new IdmSwiperProgress(selectedSwiper, `#${this.id} .swiper`);
}
}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){
const addToBasketEl = prodEl.querySelector("form.add_to_basket");
addToBasketEl?.addEventListener("submit", this.handleAddToBasket);
// + -
if(this?.options?.addToBasket === "range"){
addToBasketEl?.querySelector(".idm-products-banner__qty")?.addEventListener("click",this.handleQuantityButtonClick);
addToBasketEl?.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);
}
/**
* 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 = IdmHotspot.idmDefaultSwiperConfig;
// 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();
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()});
}
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);
});
});
// new IdmHotspot({
// id: "idmTestHotspot1",
// title: "tescik",
// products: [] // Tablica produktów
// placement: {
// selector: "#content",
// insert: "beforeend",
// },
// source: {
// productsMenu: 1649,
// producersId: [],
// seriesId: [],
// parametersId: [],
// priceRange: {
// from: 0,
// to: 150,
// }
// }
// options: {
// lazy: true,
// addToBasket: "range",
// swiper: true,
// }
// });
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();
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.querySelectorAll(".hotspot__wrapper.idm__hotspot").forEach(currentHotspot=>{
idmPrepareHotspotObject(currentHotspot);
});