Files
kupshop/web/common/static/wpj/wpj.blocekRuntime.ts
2025-08-02 16:30:27 +02:00

640 lines
23 KiB
TypeScript

import { autoPlacement, autoUpdate, computePosition, offset } from '@floating-ui/react-dom';
let intervalIds: any[] = [];
const getIntersectionObserver = (loadCallback: (target: any) => void, observerOptions?: IntersectionObserverInit) => {
observerOptions = observerOptions ?? {
rootMargin: '300px 0px 300px 0px'
};
const intersectionCallback = (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
entries.forEach((entry) => {
if (entry.isIntersecting && (!entry.target.children.length || entry.target.children[0].textContent === 'Načítám')) {
loadCallback(entry.target);
observer.unobserve(entry.target);
}
});
};
return new IntersectionObserver(intersectionCallback, observerOptions);
};
const decodeHtmlEntities = (value: string) => {
if(!value) {
return value;
}
let txt = document.createElement("textarea");
txt.innerHTML = value;
return txt.value;
}
enum Resolution {
DEFAULT = 'default',
MD = 'mqdefault',
HD = 'hqdefault',
SD = 'sddefault',
MAX = 'maxresdefault',
}
const resolutions: string[] = [
'0',
'default',
'mqdefault',
'hqdefault'
];
type Mode = 'youtube' | 'vimeo' | 'cdn';
const blocekRuntime = {
config: {},
onReady: function() {
wpj.blocekRuntime.initProductsLazyload();
wpj.blocekRuntime.initComponentLazyload();
wpj.blocekRuntime.initializeSliders();
wpj.blocekRuntime.initializeAccordionClickAction();
wpj.blocekRuntime.initTabs();
wpj.blocekRuntime.initImap();
wpj.blocekRuntime.initFaq();
wpj.blocekRuntime.initVideoLazy();
},
insertProducts: function(filter: any, callback: any) {
// @ts-ignore
if (MODULES.COMPONENTS) {
fetch(wpj.graphqlRoute, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
body: JSON.stringify({
query: `query ($data: String){
productsBlock(data: $data)
}
`,
variables: {
data: filter
}
})
}).then((response) => {
// Examine the text in the response
response.json().then((content: any) => {
callback({ html: content.data.productsBlock, result: true });
fireProductsLoadedEvent();
wpj.blocekRuntime.initializeSliders();
});
});
} else {
fetch('/_blocek/ProductsBlock/', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
body: filter
}).then(function(response) {
response.json().then(function(data) {
callback(data);
fireProductsLoadedEvent();
wpj.blocekRuntime.initializeSliders();
});
});
}
},
insertComponent: (body: string, callback: any) => {
if (!MODULES.COMPONENTS) {
return;
}
fetch(wpj.graphqlRoute, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
body: JSON.stringify({
query: `query ($data: String){
componentBlock(data: $data)
}`,
variables: {
data: body
}
})
}).then((response) => {
// Examine the text in the response
response.json().then((content: any) => {
callback({ html: content.data.componentBlock, result: true });
});
});
},
loadComponent: function(component: any) {
if (!component || !MODULES.COMPONENTS) {
return;
}
let body = component.getAttribute('data-block-component');
// @ts-ignore
window.wpj.blocekRuntime.insertComponent(body, function(data: any) {
component.innerHTML = data.html;
// @ts-ignore
if (typeof window.wpj !== 'undefined' && typeof window.wpj.tracking !== 'undefined') {
// @ts-ignore
window.wpj.tracking.handleImpressions();
}
});
},
initializeSliders: () => {
for (let i = 0; i <= intervalIds.length; i++) {
clearInterval(intervalIds[i]);
}
intervalIds = [];
const containers = document.querySelectorAll('[data-slide-settings]') as NodeListOf<HTMLElement>;
containers.forEach((container) => {
const containerSettings = JSON.parse(decodeHtmlEntities(container.getAttribute('data-slide-settings') ?? '{}'));
const slideBox = container.querySelector('[data-slide-init=\'true\']') as HTMLElement;
if (containerSettings['buttons']) {
const prevButton = container.querySelector('[data-slide-controls=\'prev\']') as HTMLElement;
prevButton.classList.add('disabled');
const nextButton = container.querySelector('[data-slide-controls=\'next\']') as HTMLElement;
let sliderOriginalX: number[] = [];
const slideBoxX: number = slideBox.getBoundingClientRect().x;
const slideBoxViewport: number = slideBox.getBoundingClientRect().width;
let sliderElements: HTMLElement[] = Array.from(container.querySelectorAll('[data-blocek-col]') as NodeListOf<HTMLElement>);
// X, kupkolo fallback na productList
if (sliderElements.length <= 0 && container.querySelector('.catalog-row')) {
sliderElements = Array.prototype.slice.call(container.querySelector('.catalog-row').children);
}
if (sliderElements.length <= 0) {
sliderElements = Array.from(container.querySelectorAll('[class^="wpj-col-"]') as NodeListOf<HTMLElement>).filter((element: HTMLElement) => {
return !(element.querySelector('[class^="wpj-col-"]') !== null);
});
}
sliderOriginalX.push(0); // start slideru
sliderElements.forEach((element, index) => {
if ((element.getBoundingClientRect().x + element.getBoundingClientRect().width) >= (slideBoxX + slideBoxViewport)) {
const el_offset = (element.getBoundingClientRect().x + element.getBoundingClientRect().width) - (slideBoxX + slideBoxViewport);
sliderOriginalX.push(el_offset);
}
});
let currentIndex = 0;
sliderOriginalX = [...new Set(sliderOriginalX)];
function scrollCarousel(items: number) {
currentIndex += items;
if (currentIndex < 0) {
currentIndex = 0;
} else if (currentIndex >= sliderOriginalX.length) {
currentIndex = sliderOriginalX.length - 1;
}
if (currentIndex == 0) {
prevButton.classList.add('disabled');
} else {
prevButton.classList.remove('disabled');
}
if (currentIndex >= (sliderOriginalX.length - 1)) {
nextButton.classList.add('disabled');
} else {
nextButton.classList.remove('disabled');
}
let offset = 0;
if (currentIndex > 0) {
offset = sliderOriginalX[currentIndex];
}
slideBox.style.transform = `translateX(${-1 * offset}px)`;
}
prevButton.addEventListener('click', function() {
scrollCarousel(-1);
});
nextButton.addEventListener('click', function() {
scrollCarousel(1);
});
} else if (containerSettings['speed'] > 0) {
slideBox.style.width = (2 * container.getBoundingClientRect().width) + 'px';
slideBox.style.transition = 'unset';
const elements = container.querySelectorAll('[data-blocek-col]') as NodeListOf<HTMLElement>;
let elementsWidth = 0;
elements.forEach((element, index) => {
elements[0]?.parentNode?.appendChild(element.cloneNode(true));
elementsWidth += element.getBoundingClientRect().width;
if (index == 0 && elements.length > 1) {
elementsWidth += (elements[index + 1].getBoundingClientRect().x - element.getBoundingClientRect().x - element.getBoundingClientRect().width) * elements.length;
}
});
const id = setInterval(frame, 50 - containerSettings['speed']);
intervalIds.push(id);
let pos = 0;
function frame() {
if (pos >= elementsWidth) {
pos = 0;
} else {
pos++;
slideBox.style.transform = `translateX(${(-pos)}px)`;
}
}
}
});
},
initializeAccordionClickAction: () => {
document.addEventListener('click', (e) => {
const container = (e.target as HTMLElement)
.closest('[data-accordion-click]') as HTMLElement;
if (!container) return;
const value = container.getAttribute('data-accordion-click');
if (value === 'disabled') return;
const body = container.parentElement
?.querySelector('[data-accordion-body]') as HTMLElement;
if (!body) return;
const icon = container
.querySelector('[data-accordion-open-icon]') as HTMLElement;
if (body.style.maxHeight && body.style.maxHeight !== '0px') {
icon?.classList.remove('wpj-icon-active');
body.style.maxHeight = '0px';
} else {
icon?.classList.add('wpj-icon-active');
body.style.maxHeight = getTotalScrollHeight(body) + 'px';
const speed = getAnimationSpeed(body);
body.style.transition = `max-height ${speed}s ease`;
}
const onEnd = () => {
if (body.style.maxHeight !== '0px') {
body.style.maxHeight = 'auto';
body.style.transition = 'max-height .3s ease';
}
body.removeEventListener('transitionend', onEnd);
};
body.addEventListener('transitionend', onEnd);
});
function getAnimationSpeed(element) {
const childrenBodies = element.querySelectorAll('[data-accordion-body]') as NodeListOf<HTMLElement>;
const closedBodies = Array.from(childrenBodies).filter(body => !body.style.maxHeight || body.style.maxHeight === '0px');
return closedBodies.length ? closedBodies.length * 0.3 : 0.3;
}
function getTotalScrollHeight(element) {
let totalHeight = element.scrollHeight;
const childrenBodies = element.querySelectorAll('[data-accordion-body]');
childrenBodies.forEach((body) => {
totalHeight += body.scrollHeight;
});
return totalHeight;
}
},
initComponentLazyload: function() {
if (!MODULES.COMPONENTS) {
return;
}
const observer = getIntersectionObserver((target) => {
wpj.blocekRuntime.loadComponent(target);
});
const targets = document.querySelectorAll('[data-block-lazy]');
targets.forEach(target => {
observer.observe(target);
});
},
loadProducts: function(products: any) {
if (!products) {
return;
}
var filter = decodeHtmlEntities(products.getAttribute('data-products-filter'));
try {
// Find closest "data-products-listId"
let element = products;
while (element.parentElement) {
element = element.parentElement;
if (element.hasAttribute('data-products-listType')) {
let filter_decoded = JSON.parse(filter);
filter_decoded.listType = element.getAttribute('data-products-listType');
filter_decoded.listId = element.getAttribute('data-products-listId');
filter = JSON.stringify(filter_decoded);
break;
}
}
} catch (e) {
// Ignore listId detection exceptions
}
// @ts-ignore
window.wpj.blocekRuntime.insertProducts(filter, function(data: any) {
products.innerHTML = data.html;
// Trigger imagesLoaded event to notify lazy load
products.dispatchEvent(new CustomEvent('imagesLoaded', { bubbles: true }));
// @ts-ignore
if (typeof window.wpj !== 'undefined' && typeof window.wpj.tracking !== 'undefined') {
// @ts-ignore
window.wpj.tracking.handleImpressions();
}
});
},
initProductsLazyload: function() {
const observer = getIntersectionObserver((target) => {
wpj.blocekRuntime.loadProducts(target);
});
const targets = document.querySelectorAll('[data-products-filter]');
targets.forEach(target => {
observer.observe(target);
});
},
initImap: function() {
document.addEventListener('click', (e) => {
// @ts-ignore
const selector = e.target.closest('[data-imap="points-wrapper"] [data-imap="icon"]');
if (selector) {
const point = selector.closest('[data-imap="point"]');
point.classList.toggle('active');
if (point.classList.contains('active')) {
const content = point.querySelector('.w-imap-content');
autoUpdate(point, content, () => {
computePosition(point, content, {
strategy: 'absolute',
middleware: [
autoPlacement({
crossAxis: true,
allowedPlacements: ['bottom-start', 'bottom-end', 'top-end', 'top-start', 'bottom', 'top']
}),
offset({
mainAxis: -20,
alignmentAxis: 20
})
]
}).then(({ x, y }) => {
Object.assign(content.style, {
left: `${x}px`,
top: `${y}px`,
height: 'fit-content'
});
});
}, {
ancestorScroll: false,
layoutShift: false
});
}
[...point.parentNode.querySelectorAll('.active')].filter((child) => child !== point)[0]?.classList.remove('active');
}
});
document.addEventListener('click', (e) => {
// @ts-ignore
const selector = e.target.closest('a.imap-close-button');
if (selector) {
const point = selector.closest('[data-imap="point"]');
point.classList.toggle('active');
[...point.parentNode.querySelectorAll('.active')].filter((child) => child !== point)[0]?.classList.remove('active');
}
});
},
initFaq: function() {
document.addEventListener('click', (e) => {
// @ts-ignore
const selector = e.target.closest('.wpj-faq-wrapper h4');
if (selector) {
if (selector.classList.contains('active')) {
selector.classList.remove('active');
selector.closest('.wpj-faq-wrapper').classList.remove('opened');
} else {
selector.classList.add('active');
selector.closest('.wpj-faq-wrapper').classList.add('opened');
}
}
});
},
initTabs: function() {
document.addEventListener('click', (e) => {
// @ts-ignore
const trigger = e.target.closest('[data-tabs-open]');
if (trigger) {
const id = trigger.getAttribute('data-tabs-open');
const content = document.querySelector(`[data-tabs-content="${id}"]`);
const parent = trigger.closest('.wpj-tabs');
if (!content || !parent) return;
const allHeaders = parent.querySelectorAll('[data-tabs-open]');
const allContents = parent.querySelectorAll('[data-tabs-content]');
allHeaders.forEach(header => header.classList.remove('active'));
allContents.forEach(content => content.classList.remove('active'));
trigger.classList.add('active');
content.classList.add('active');
}
});
},
setYoutubeImages: function(url: string, code: string, imageThumb: HTMLImageElement) {
const fallbackUrl = `https://i.ytimg.com/vi/${code}/sddefault.jpg`;
fetch(`https://www.youtube.com/oembed?format=json&url=${url}`)
.then((res) => res.json())
.then((res) => {
if (!res?.thumbnail_url) {
imageThumb.setAttribute('src', fallbackUrl);
}
imageThumb.setAttribute('src', res.thumbnail_url);
})
.catch((err) => {
imageThumb.setAttribute('src', fallbackUrl);
});
},
getVimeoImages: async function(code: string, imageThumb: HTMLImageElement): Promise<string> {
const fallbackUrl = `https://i.ytimg.com/vi/${code}/sddefault.jpg`;
try {
const res = await fetch(`https://vimeo.com/api/oembed.json?url=https://player.vimeo.com/video/${code}&width=1280&height=720`);
if (!res.ok) {
imageThumb.setAttribute('src', fallbackUrl);
return;
}
const data = await res.json();
const url = data ? data.thumbnail_url : fallbackUrl;
imageThumb.setAttribute('src', url);
} catch (error) {
imageThumb.setAttribute('src', fallbackUrl);
}
},
getImages: function(url: string, code: string, mode: Mode, imageThumb: HTMLImageElement) {
switch (mode) {
case 'youtube':
url = url.replace('embed/', 'watch?v=').replace('?autoplay=1&mute=0', '');
this.setYoutubeImages(url, code, imageThumb);
return;
case 'vimeo':
this.getVimeoImages(code, imageThumb);
return;
}
},
initVideoLazy: function () {
document.addEventListener('click', (e: Event) => {
// @ts-ignore
const selector = e.target.closest('.w-lazy-play-btn');
if (!selector) {
return;
}
e.preventDefault();
selector.style.display = 'none';
if (selector.hasAttribute('data-i-frame-loaded')) {
return false;
}
const wrapper = selector.closest('.w-lazy-video-wrapper');
const url = wrapper.getAttribute('data-url');
const mode = wrapper.getAttribute('data-mode') as Mode;
if (mode === "cdn" && wrapper.getAttribute('data-autoplay') !== "true") {
injectVideoElement(wrapper, false);
} else {
let iframeElement: HTMLIFrameElement = document.createElement('iframe');
iframeElement.src = url;
iframeElement.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture';
iframeElement.frameBorder = '0';
iframeElement.allowFullscreen = true;
wrapper.append(iframeElement);
}
selector.setAttribute('data-i-frame-loaded', true);
});
document.querySelectorAll('.w-lazy-video-wrapper').forEach((el) => {
const imageThumb: HTMLImageElement = el.querySelector('.w-lazy-video-poster-img');
let imgSrc: string = imageThumb?.getAttribute('src');
const url = el.getAttribute('data-url');
const mode = el.getAttribute('data-mode') as Mode;
if (mode === 'cdn' && el.getAttribute('data-autoplay') === 'true') {
getIntersectionObserver(() => {
injectVideoElement(el as HTMLDivElement, true);
}).observe(el.querySelector('.w-lazy-video-poster-img'));
return;
}
getIntersectionObserver(() => {
if (!imgSrc || (imageThumb.naturalWidth <= 120 && imageThumb.naturalHeight <= 90)) {
const code = el.getAttribute('data-code');
this.getImages(url, code, mode, imageThumb);
}
}).observe(imageThumb);
el.addEventListener('mouseenter', (e: Event) => {
// @ts-ignore
const wrapper: any = e.target;
if (!wrapper) {
return;
}
const mode = wrapper.getAttribute('data-mode') as Mode;
const url = wrapper.getAttribute('data-url');
if (wrapper.getAttribute('data-warm') || !mode) {
return false;
}
wrapper.setAttribute('data-warm', true);
let preconnectLinksTemplate;
if (mode === 'youtube') {
preconnectLinksTemplate = `<link rel="preconnect" ` + `href="` + url + `"/>
<link rel="preconnect" href="https://www.google.com"/>
<link rel="preconnect" href="https://static.doubleclick.net"/>
<link rel="preconnect" href="https://googleads.g.doubleclick.net"/>`;
} else if (mode === 'vimeo') {
preconnectLinksTemplate = `<link rel="preconnect" href="https://player.vimeo.com"/>
<link rel="preconnect" href="https://i.vimeocdn.com"/>
<link rel="preconnect" href="https://f.vimeocdn.com"/>
<link rel="preconnect" href="https://fresnel.vimeocdn.com"/>`;
}
if (preconnectLinksTemplate) {
wrapper.insertAdjacentHTML('beforeend', preconnectLinksTemplate);
}
});
});
}
};
const injectVideoElement = (wrapper: HTMLDivElement, autoplay: boolean) => {
const mobileUrl = wrapper.getAttribute('data-mobile-url');
let videoElement: HTMLVideoElement = document.createElement('video');
videoElement.loop = autoplay;
videoElement.autoplay = true;
videoElement.playsInline = autoplay;
videoElement.preload = 'auto';
videoElement.muted = autoplay;
videoElement.controls = !autoplay;
videoElement.className = 'video';
videoElement.style.zIndex = '1';
videoElement.className = 'w-lazy-video-poster-img';
const sourceMP4 = document.createElement('source');
sourceMP4.src = wrapper.getAttribute('data-url');
sourceMP4.type = 'video/mp4';
videoElement.appendChild(sourceMP4);
const sourceHLS = document.createElement('source');
sourceHLS.src = mobileUrl;
sourceHLS.type = 'application/x-mpegURL';
videoElement.appendChild(sourceHLS);
wrapper.append(videoElement);
};
const fireProductsLoadedEvent = () => {
// event for js-shop - so favorites/compare icons can be rendered
const loadedEvent = new CustomEvent('wpj-products-loaded', {
detail: { type: 'blocek' }
});
document.dispatchEvent(loadedEvent);
};
declare global {
interface Wpj {
blocekRuntime: typeof blocekRuntime;
}
}
wpj.blocekRuntime = blocekRuntime;
if (MODULES.COMPONENTS) {
window.onload = () => {
wpj.blocekRuntime.onReady();
};
} else {
wpj.onReady.push(wpj.blocekRuntime.onReady);
}