UNPKG

@douglas.onsite.experimentation/douglas-ab-testing-toolkit

Version:

DOUGLAS A/B Testing Toolkit

1,014 lines (870 loc) 39.7 kB
/** * DOUGLAS A/B Testing Toolkit * * Feedback? * Max Vith <ma.vith@douglas.de> * Kristina Bekher <k.bekher@douglas.de> * */ /** * The logError function logs detailed information about an error to the console. * It includes the error message, stack trace, a prefix to identify the source, and optional context for additional debugging. * This function can also be extended to report errors to external monitoring tools like Grafana. * * @param {Error} error - The error object to log. * @param {string} prefix - A string used to identify the origin of the error (e.g., experiment ID or module). */ const logError = (error, prefix = '') => { window.console.error('>>> OE_ERROR', prefix, error); }; /** * The asyncSave function wraps an asynchronous function in a try-catch block and automatically logs any errors using the logError utility. * It helps prevent unhandled promise rejections and ensures consistent error reporting across async operations. * * @param {Function} fn - The asynchronous function to be executed safely. * @param {string} [prefix=''] - A label to identify the source of the function for error logging. * @returns {Function} A new async function that wraps the original and handles errors internally. */ export const asyncSave = (fn, prefix = '') => { return async function (...args) { try { return await fn(...args); } catch (error) { logError(error, prefix); } } }; /** * The qs function is a utility for selecting a single DOM element using a CSS selector. It simplifies element selection by * optionally allowing searches within a specific parent element. * * @param {string} selector - A CSS selector string used to identify the desired element. * @param {Element} parent - The element or document context within which the selector will be applied. * @returns Node - Returns the first element within the specified parent that matches the given selector. If no matching element * is found, it returns null. */ export const qs = (selector, parent) => { return (parent || document).querySelector(selector); }; /** * The qsa function is a utility for selecting multiple DOM elements using a CSS selector. It allows querying within a specific parent * element or the entire document. * * @param {string} selector - A CSS selector string used to identify the desired elements. * @param {Element | Document} [Optional] parent - The element or document context within which the selector will be applied. Defaults * to document if not provided. * @returns NodeList - Returns a NodeList containing all elements that match the given selector within the specified parent context. If * no elements match, an empty NodeList is returned. */ export const qsa = (selector, parent) => { return (parent || document).querySelectorAll(selector); }; /** * The elem function continuously checks for the presence of DOM elements matching a specified CSS selector or evaluates * a custom condition provided as a function. It executes a callback when the condition is met or times out after 10 seconds. * * @param {string | function} selector - A CSS selector string to query elements from the DOM. Alternatively, a function that * evaluates to true when the desired condition is met. * @param {function} callback - A function called when either: Matching elements are found, and the result is passed as an * argument or the timeout of 10 seconds is reached without finding any elements or satisfying the condition, in which case * false is passed to the callback. */ export const elem = (selector, callback) => { const endTime = Number(new Date()) + 10000; const selectorIsFunc = typeof selector === 'function'; if (!selectorIsFunc) { const tmp = selector; selector = () => qsa(tmp); } const iteration = () => { const result = selector(); if ((selectorIsFunc && result) || (!selectorIsFunc && result.length > 0)) { callback(result); } else if (Number(new Date()) < endTime) { const iterationTimeout = setTimeout(() => { clearTimeout(iterationTimeout); iteration(); }, 33); } else { callback(false); } }; iteration(); }; /** * elemSync is an asynchronous function that waits for DOM elements to be available by utilizing a provided elem function. * It returns a Promise that resolves with the elements matching the specified CSS selector. * * The function assumes that elem is a pre-existing function that accepts a CSS selector and a callback, providing * matched elements asynchronously. * * The code comment about resolving with false after 10 seconds. * * @param {string || function} selector - A CSS selector used to query the desired DOM elements. * @returns Promise - Returns a Promise that resolves to: The elements found by the elem function, based on the provided selector. */ export const elemSync = selector => { return new Promise((resolve) => { elem(selector, (response) => { resolve(response); }); }); }; /** * The addPrefix function is a utility that applies a CSS class to the root <html> element of a document. It is typically used to add * a global modifier or theme-related class for styling purposes. * * @param {string} selector - The name of the CSS class to be added to the <html> element. */ export const addPrefix = selector => addClass(document.documentElement, selector); /** * The removePrefix function removes a specified CSS class from the root <html> element of a document. It is typically used to reset or * revert global styles applied to the document. * * @param {string} selector - The name of the CSS class to be removed from the <html> element. */ export const removePrefix = selector => removeClass(document.documentElement, selector); /** * The hotjar function triggers a Hotjar event for tracking user interactions by sending a unique event key to the * Hotjar API. The function ensures that each event key is only triggered once per page load by maintaining a list of * already transmitted keys. * * @param {string} key - A unique identifier for the Hotjar event to be tracked. */ export const hotjar = key => { window.OE_HOTJAR = window.OE_HOTJAR || []; if (window.OE_HOTJAR.indexOf(key) === -1) { window.OE_HOTJAR.push(key); elem(() => { return window.hj; }, (hj) => { if (!hj) return; window.hj('event', key); }); } }; /** * The pushMetric function logs and optionally tracks unique conversion metrics during A/B testing experiments. It stores metric * IDs in a global array (window.OE_METRICS) and provides placeholders for integrating with third-party analytics tools. * * @param {string} key - The identifier for the metric or goal to be tracked. * @param {boolean} unique - If true, the function ensures the metric is only tracked once by checking if the id is already * present in window.OE_METRICS. Defaults to false. */ export const pushMetric = (id, unique = false) => { window.OE_METRICS = window.OE_METRICS || []; try { const metricTransmitted = window.OE_METRICS.indexOf(id) !== -1; if (unique && metricTransmitted) return; if (!metricTransmitted) window.OE_METRICS.push(id); Kameleoon.API.Goals.processConversion(id); console.log(">>> DOUGLAS Toolkit:", "Kameleoon API Goal", id); } catch (e) { console.log(">>> DOUGLAS Toolkit:", e); } }; /** * The pushData function sets a custom data value using Kameleoon’s JavaScript API. It takes a key and a value as parameters and updates the * associated custom data for the current user session. If the Kameleoon object or its API components are not available, the function gracefully * does nothing. * * @param {string} key - The name of the custom data to set. * @param {string | number} value - The value to associate with the custom data key. */ export const pushData = (key, value) => { Kameleoon?.API?.Data?.setCustomData(key, value); }; /** * The getWidth function calculates and returns the maximum width of the current document, accounting for different properties that * reflect the document's width under various scenarios. * * @returns number - The maximum width of the document in pixels. */ export const getWidth = () => { return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth, document.documentElement.clientWidth); }; /** * The isMobile function detects if the current user is accessing the website from a mobile device by examining the user agent string * of the browser. * * @returns boolean - true if the user agent string contains "android" or "mobile", indicating a mobile device, false otherwise. */ export const isMobile = () => { const userAgent = navigator.userAgent.toLowerCase(); return userAgent.indexOf('android') !== -1 || userAgent.indexOf('mobile') !== -1; }; /** * The getCookie function retrieves the value of a specified cookie from the browser's document. * * @description https://www.w3schools.com/js/js_cookies.asp * @param {string} cookieName - The name of the cookie whose value you want to retrieve. * @returns string - The value of the specified cookie. Returns an empty string ("") if the cookie does not exist. */ export const getCookie = cname => { const name = cname + "="; const decodedCookie = decodeURIComponent(document.cookie); const ca = decodedCookie.split(';'); for (let i = 0; i < ca.length; i++) { let c = ca[i]; while (c.charAt(0) == ' ') { c = c.substring(1); } if (c.indexOf(name) == 0) { return c.substring(name.length, c.length); } } return ""; }; /** * The setCookie function creates or updates a cookie with a specified name, value, and expiration date. It sets the cookie to be * accessible across the entire website by using a path of /. * * @param {string} cookieName - The name of the cookie to be set. * @param {string} cookieValue - The value to be assigned to the cookie. * @param {number} expiryDays - The number of days until the cookie expires. */ export const setCookie = (cname, cvalue, exdays) => { const d = new Date(); d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); let expires = "expires=" + d.toUTCString(); document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; console.log(">>> DOUGLAS Toolkit:", "Cookie created", cname, cvalue, expires); }; /** * The scrollTo function smoothly scrolls the page to a specified vertical position (pixel) over a given duration (duration). It * uses an easing function (easeInOutQuad) to create a smooth scrolling animation. * * @param {number} pixel - The target vertical scroll position (in pixels) to scroll to. This value can be any integer representing * the desired scroll position from the top of the page. * @param {number} duration - The duration (in milliseconds) over which the scroll animation should take place. This determines how * quickly the page will scroll to the specified position. */ export const scrollTo = (pixel, duration) => { Math.easeInOutQuad = (t, b, c, d) => { t /= d / 2; if (t < 1) { return (c / 2) * t * t + b; } else { t--; return (-c / 2) * (t * (t - 2) - 1) + b; } }; const start = window.document.documentElement.scrollTop + window.document.body.scrollTop; const change = pixel - start; const increment = 20; let currentTime = 0; const animateScroll = function () { currentTime += increment; var val = Math.easeInOutQuad(currentTime, start, change, duration); window.document.documentElement.scrollTop = val; window.document.body.scrollTop = val; if (currentTime < duration) { const animateScrollTimeout = setTimeout(() => { clearTimeout(animateScrollTimeout); animateScroll(); }, increment); } }; animateScroll(); }; /** * The share function adds a new method (func) to the global AB_TESTING_TOOLKIT object under the specified name (name). This * function is typically used for A/B testing scenarios to dynamically extend the toolkit with custom functions. After adding * the function, it logs a message to the console and executes an optional exec function, if defined. * * @param {string} name - The name of the method being added to the AB_TESTING_TOOLKIT object. This name will be used as a key * under which the func will be stored. * @param {function} func - The function that will be associated with the specified name and added to the AB_TESTING_TOOLKIT * object. This function can be any custom logic you wish to add to the toolkit. */ export const share = (name, func) => { window.OE_TOOLKIT = window.OE_TOOLKIT || new function () { }; window.OE_TOOLKIT.constructor.prototype[name] = func; console.log(">>> DOUGLAS Toolkit:", "Share function added", name); if (typeof exec === "function") { exec(); } return window.OE_TOOLKIT; }; /** * The exec function manages and executes functions stored in the AB_TESTING_TOOLKIT global object. It can immediately * execute a specified function if it exists or queue the function for execution later. The function also provides a * mechanism to execute all queued functions in the toolkit. * * @param {string} name - The name of the function in AB_TESTING_TOOLKIT to be executed. If name is omitted, all queued * functions are executed instead. * @param {*} params - Parameters to pass to the function when it is executed. This can be any type of data supported * by JavaScript, including objects, strings, numbers, or arrays. */ export const exec = (name, params) => { window.OE_TOOLKIT = window.OE_TOOLKIT || new function () { }; if (!window.OE_TOOLKIT.exec) window.OE_TOOLKIT.exec = []; if (name) { if (typeof window.OE_TOOLKIT[name] === "function") { window.OE_TOOLKIT[name](params); } else if (window.OE_TOOLKIT.exec.indexOf(name) === -1) { window.OE_TOOLKIT.exec.push({ name, params }); } } else { window.OE_TOOLKIT.exec.forEach(item => { if (typeof window.OE_TOOLKIT[item.name] === "function") { window.OE_TOOLKIT[item.name](item.params); console.log(">>> DOUGLAS Toolkit:", "Executed the function", item.name, item.params); } }); } }; /** * addTask * * Add a task to the DOUGLAS Toolkit Observer * * @param {string} name The unique name of the task. This name will be used to identify the task in the toolkit observer. * @param {function} init The initialization callback. This function will be invoked when the task is triggered by the * mutation observer. * @param {function | null} target The function that returns a boolean indicating whether the `init` function should be * executed. If `null`, the `init` function will always be triggered. * @param {function | null} remove The function that is invoked when the `target` evaluates to `false`. This function is * used to remove or clean up the task if necessary. If `null`, no removal will occur. */ export const addTask = (name, init, target, remove) => { const toolkitObserver = window.OE_TOOLKIT?.observ; if (!toolkitObserver) { console.log(">>> DOUGLAS Toolkit:", "No oberserver active"); return; } // log only if (window.OE_TOOLKIT.observ.tasks[name]) console.log(">>> DOUGLAS Toolkit:", "Task already exist"); toolkitObserver.add(name, asyncSave(init, name), target, remove); console.log(">>> DOUGLAS Toolkit:", "Task added", name); }; /** * removeTask * * Remove a task to the DOUGLAS Toolkit Observer * * @param {string} name * @returns */ export const removeTask = (name) => { const toolkitObserver = window.OE_TOOLKIT?.observ; if (!toolkitObserver) { console.log(">>> DOUGLAS Toolkit:", "No oberserver active"); return; } // log only if (!window.OE_TOOLKIT.observ.tasks[name]) console.log(">>> DOUGLAS Toolkit:", "Task does not exist"); toolkitObserver.remove(name); console.log(">>> DOUGLAS Toolkit:", "Task removed", name); }; /** * updateConfig * * Update the config for DOUGLAS Toolkit Observer * * @param {*} config * @returns */ export const updateConfig = config => { const toolkitObserver = window.OE_TOOLKIT?.observ; if (!toolkitObserver) { console.log(">>> DOUGLAS Toolkit:", "No oberserver active"); return; } toolkitObserver.update(config); console.log(">>> DOUGLAS Toolkit:", "Observer was updated", config); }; /** * The pushHistory function updates the browser's history stack by adding a new entry with a modified URL path that includes * a hash fragment (#step). This function is useful for tracking navigation steps or states within a single-page application * (SPA) without reloading the page. * * @param {string} step - A string representing the step or identifier to be appended as a hash fragment to the current URL. * If step is falsy (e.g., null, undefined, or an empty string), the function exits without making changes. */ export const pushHistory = step => { if (!step) return; const path = `${location.pathname}#${step}`; window.history.pushState({ urlPath: path }, "", path); }; /** * The setPreload function preloads a list of image assets by creating new Image objects and setting their src attributes to the * provided image URLs. This technique ensures that images are downloaded and cached by the browser before they are needed, * improving the loading speed and user experience when these images are displayed. * * @param {array} images - An array of image URLs to be preloaded. */ export const setPreload = images => { images.forEach(image => { new Image().src = image; }); }; /** * The setStore function saves or updates an object in client-side storage (localStorage or sessionStorage). It merges the * new key-value pair with existing data stored under the specified name key, ensuring structured data persistence across * user sessions. * * @param {string} name - The name of the storage key used to save or retrieve the data. * @param {string} key - The property name in the object to store or update. * @param {any} value - The value associated with the specified key. * @param {string} storage - localStorage (default) | sessionStorage */ export const setStore = (name, key, value, storage = 'localStorage') => { const data = getStore(name, null, storage); let object = {}; if (data !== null) { object = { ...data, [key]: value }; } else { object = { [key]: value }; } window[storage].setItem(name, JSON.stringify(object)); }; /** * The getStore function retrieves data from a specified storage (localStorage or sessionStorage) and optionally returns a specific * property of the stored data object. The function handles JSON parsing and gracefully manages errors if the stored data is not in * a valid JSON format.# * * @param {string} name - The key name under which the data is stored. * @param {string | null} key - The property to retrieve from the stored object. If omitted or null, the entire object is returned. * @param {string} storage - The storage type from which to retrieve the data. Options: "localStorage" (default) or "sessionStorage". * @returns any */ export const getStore = (name, key, storage = 'localStorage') => { const data = window[storage].getItem(name); if (data === null) return null; try { const json = JSON.parse(data); if (!key) return json; if (json[key]) return json[key]; else return null; } catch (err) { return null; } }; /** * * setClickEvent * * @param {string || Element} selector css selector, node or node list * @param {string} prefix prefix of your experiment * @param {string || function} callback a key or a callback function * @param {string} [eventType="click"] - The event type to listen for (e.g., "click", "mouseover"). Defaults to "click". */ export const setClickEvent = (selector, prefix, callback, eventType = "click") => { const addClickListeners = (elements) => { elements.forEach(element => { if (!element || element.dataset[prefix]) return; element.dataset[prefix] = true; element.addEventListener(eventType, event => { asyncSave(typeof callback === 'function' ? () => callback(event) : () => pushMetric(callback), prefix)(); }); }); }; if (typeof selector === 'string') { elem(selector, elements => { if (!elements) return; addClickListeners(elements) }); } else { if (selector instanceof NodeList === false) selector = [selector]; addClickListeners(selector) } }; /** * * setAccessabilityEvents * * @param {string || Element} selector css selector, node or node list * @param {string} prefix prefix of your experiment * @param {function} callback a callback function */ export const setAccessabilityEvents = (selector, prefix, callback) => { const a11yPrefix = prefix + 'a11y'; const a11yCallback = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); // prevent scroll for Space key asyncSave(() => callback(e), prefix)(); } } setClickEvent(selector, a11yPrefix, a11yCallback, 'keydown'); }; /** * The addClass function adds a specified CSS class to a given HTML element if the class is not already present. It ensures that the * operation only proceeds when the element and class name are valid. * * @param {HTMLElement} element - The target HTML element to which the class should be added. * @param {string} selector - The CSS class name to add to the element. * @returns {boolean} */ export const addClass = (el, selector) => { return hasClass(el, selector) === false && el.classList.add(selector); }; /** * The removeClass function removes a specified CSS class from an HTML element if the class is present. It ensures that the operation * only proceeds when valid input is provided. * * @param {HTMLElement} elem - The target HTML element from which the class should be removed. * @param {string} selector - The CSS class name to be removed from the element. * @returns {boolean} */ export const removeClass = (el, selector) => { return hasClass(el, selector) && el.classList.remove(selector); }; /** * The hasClass function checks whether a specified CSS class is present on a given HTML element. It ensures that the input parameters * are valid before performing the check. * * @param {node} elem - The target HTML element to be checked for the specified class. * @param {string} selector - The CSS class name to check for. * @returns {boolean} - Returns true if the element has the class, otherwise false. */ export const hasClass = (el, selector) => { return el && el.classList && selector && el.classList.contains(selector); }; /** * The toggleClass function toggles a specified CSS class on a given HTML element. * It ensures that the input parameters are valid before performing the toggle. * * @param {HTMLElement} el - The target HTML element to toggle the class on. * @param {string} selector - The CSS class name to toggle. * @returns {boolean} - Returns true if the class is now present, false if removed or invalid input. */ export const toggleClass = (el, selector) => { return el && el.classList && selector && el.classList.toggle(selector); }; /** * The setMutationObserver function sets up a MutationObserver on a DOM element matching the given selector. The observer watches * for mutations (changes) to the element, and when such changes occur, it triggers the provided callback function. This function * returns a promise, resolving once the observer is successfully set, or rejecting if any errors occur. * * @param {string} selector - The CSS selector used to find the target DOM element to observe. * @param {string} attribute - A unique identifier used as a data attribute (data-[prefix]) to ensure that the observer is not * added multiple times to the same element. * @param {function} callback - The callback function to be executed when mutations are detected on the target element. * @param {object} [options={ attributes: true, childList: true, subtree: true }] - An optional object specifying which mutations * should be observed. * @returns {Promise<void>} - A promise that resolves when the MutationObserver is set up. */ export const setMutationObserver = (selector, prefix, callback, options = { attributes: true, childList: true, subtree: true } ) => { return new Promise((resolve, reject) => { elemSync(selector) .then((element) => { if (!element || element[0].dataset[prefix]) return; element[0].dataset[prefix] = true; new MutationObserver(asyncSave(callback, prefix)).observe(element[0], options); resolve(); }) .catch((error) => { logError(error, prefix); }); }); }; /** * The isElementInViewport function checks if an HTML element is currently visible within the viewport. It determines whether the * element is fully or partially visible, and also checks if the element is positioned below the top of the viewport. * * @param {HTMLElement} element - The target HTML element to check for visibility within the viewport. * @returns Object */ export const isElementInViewport = el => { const rect = el.getBoundingClientRect(); if (rect.y > 0) return true; return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); }; /** * The inViewport function is used to monitor an HTML element's visibility in the viewport as the user scrolls. When the element enters * or exits the viewport, the provided callback function is invoked. This can be useful for triggering certain actions when an * element becomes visible, such as lazy loading content or triggering animations. * * @param {string | HTMLElement} selector - If a string is provided, it should be a CSS selector that will be used to find the element * within the document. If an HTMLElement is provided, the function directly uses it to check for visibility. * @param {string} prefix - A unique string used as a prefix to avoid duplicate event listeners. It is also used to mark elements with * a custom data attribute to track if the element has already been processed. * @param {function} callback - A function that will be invoked whenever the visibility status of the element changes (when it enters * or exits the viewport). It is passed the current visibility status (true if in the viewport, false if not). */ export const inViewport = (selector, prefix, callback) => { let status = false; const safeCallback = asyncSave(callback, prefix); const initScrollEvent = elem => { if (!elem || elem[prefix]) return; elem[prefix] = true; window.addEventListener('scroll', () => { if (!isElementInViewport(elem) && status) { status = false; safeCallback(status); console.log(">>> DOUGLAS Toolkit:", "Element is not in your viewport!"); } else if (isElementInViewport(elem) && !status) { status = true; safeCallback(status); console.log(">>> DOUGLAS Toolkit:", "Element is in your viewport!"); } }); }; if (typeof selector === "string") { elem(selector, elem => { if (!elem) return; initScrollEvent(elem[0]); }); } else { const elem = selector; initScrollEvent(elem); } }; /** * The getPage function determines the current page type based on the URL pathname. It optionally checks if the user is * in the checkout process and categorizes the page accordingly. The function supports various page types, including * home, product, search, brand, login, cart, and confirmation pages. * * @param {boolean} inCheckout - If set to true, the function checks if the current page is part of the checkout process * and returns checkout-related page types. * @returns {undefined|string} - Returns a string indicating the page type, such as "home", "pdp", "checkout", "cart", etc. * Returns undefined if no matching page type is found. */ export const getPage = (inCheckout = false) => { const pathname = location.pathname; if (inCheckout) { if (pathname.indexOf('/checkout/') === -1) return 'no-checkout' if (pathname.indexOf('/checkout/') !== -1) return 'checkout'; } let currentPage; if (new RegExp('^\\/(de|pl|it|fr|nl|be|at|ch|es)$').test(pathname)) { currentPage = 'home'; } else if (pathname.indexOf('/c/') !== -1) { currentPage = 'pop'; } else if (pathname.indexOf('/p/') !== -1) { currentPage = 'pdp'; } else if (pathname.indexOf('/b/') !== -1) { currentPage = 'brand'; } else if (pathname.indexOf('/search') !== -1) { currentPage = 'search'; } else if (pathname.indexOf('/login') !== -1) { currentPage = 'login'; } else if (pathname.indexOf('/cart') !== -1) { currentPage = 'cart'; } else if (pathname.indexOf('/checkout/customer') !== -1) { currentPage = 'customer'; } else if (pathname.indexOf('/checkout/payment') !== -1) { currentPage = 'payment'; } else if (pathname.indexOf('/checkout/overview') !== -1) { currentPage = 'overview'; } else if (pathname.indexOf('/checkout/thankyou') !== -1) { currentPage = 'confirm'; } return currentPage; }; /** * he pageChange function monitors changes in the browser's URL without requiring a full page reload. It listens for changes * triggered by pushState, replaceState, and popstate events, ensuring that a callback function is executed whenever the * URL changes. This is particularly useful in Single Page Applications (SPAs) where navigation happens dynamically. * * @param {string} PREFIX - A unique prefix to prevent multiple initializations of the same function. * @param {Function} callback - A function that gets executed whenever the page's URL changes. * @returns */ export const pageChange = (PREFIX, callback) => { const windowPrefix = PREFIX + 'locationchange'; // check that we only initialize the event once if (window[windowPrefix]) return; window[windowPrefix] = true; window.addEventListener('locationchange', asyncSave(callback, PREFIX)); const oldPushState = history.pushState; history.pushState = function pushState() { const ret = oldPushState.apply(this, arguments); window.dispatchEvent(new Event('pushstate')); window.dispatchEvent(new Event('locationchange')); return ret; }; const oldReplaceState = history.replaceState; history.replaceState = function replaceState() { const ret = oldReplaceState.apply(this, arguments); window.dispatchEvent(new Event('replacestate')); window.dispatchEvent(new Event('locationchange')); return ret; }; window.addEventListener('popstate', () => { window.dispatchEvent(new Event('locationchange')); }); }; /** * The goTo function is a utility designed to navigate to a specified URL using the System.history.history.push method, if available. * It ensures that navigation attempts do not result in errors if System or its nested properties are undefined. * * @param {string} url - The URL to navigate to. */ export const goTo = url => { System?.history?.history?.push(url); }; /** * The getState function asynchronously retrieves the application state from a global store object located in * window.System. The function returns a promise that resolves to the state if it is successfully retrieved or rejects * in case of an error. * * @returns {Promise<undefined|object>} - Resolves to the application state retrieved from window.System.store.getState(). * If the store is not available, the promise rejects with an error. */ export const getState = () => { return new Promise((resolve, reject) => { elemSync(() => { const store = window.System?.store; if (typeof store !== "undefined") { return store.getState(); } }) .then((state) => { resolve(state); }) .catch((error) => { logError(error); reject(error); }); }); }; /** * The getProdId function extracts and returns a product ID from the current URL. It dynamically determines whether the * product ID is found in the search query string or the pathname, depending on the presence of a "variant" parameter. * * @returns {undefined|string} - The product ID, if found. */ export const getProdId = () => { if (!window.location.pathname.includes("/p/")) return; return window.location[window.location.search.indexOf("variant") !== -1 ? "search" : "pathname"].replace("/p/", "").replace("?variant=", "").match(/[mM]*\d+/g)[0]; }; /** * The getProduct function asynchronously retrieves product information by dispatching a request * to the global System store using the product ID derived from the current page's URL or from a provided ID. * It returns a promise that resolves with the product data or rejects if the product is not found * or if an error occurs during the data retrieval process. * * @param {string} [productId] - Optional. The product ID to fetch. If omitted, the function will use getProdId() to extract the ID from the current page URL. * @returns {Promise<object|undefined>} - Resolves with the product data retrieved from the API. If the product is not found or an error occurs, the promise rejects. */ export const getProduct = (productId = '') => { return new Promise((resolve, reject) => { const prodId = productId && productId || getProdId(); if (!prodId) { const error = new Error("getProduct, product id not found"); logError(error); reject(error); return; } window.System.store.dispatch({ type: 'GET_PRODUCT', payload: {}, url: `v2/products/${prodId}`, method: 'GET', target: 'api' }).then(response => { const product = response?.payload; if (typeof product !== "undefined" && product !== null) { resolve(product); } }).catch((error) => { logError(error); reject(error); }); }); }; /** * The getProducts function retrieves product data from the store's cached search results. If the variations parameter is set to true, * it will fetch detailed product information for each product in the list by calling getProduct for each product code, and return an array * of detailed product objects. Otherwise, it returns the cached product list from the store. * * @param {boolean} [variations=false] - If true, fetches detailed product data for each product in the list. Defaults to false. * @returns {Promise<any[]>} - Resolves with an array of product objects. If variations is true, each object is a detailed product; otherwise, each object is from the cached search result. */ export const getProducts = (variations = false) => { return new Promise((resolve, reject) => { getState() .then((store) => { if (!store) { const error = new Error('getProducts, store is not available'); logError(error) reject(error); return; } const products = store?.search?.result.products; if (products !== undefined && products !== null) { if (variations) { // Fetch details for each product Promise.all(products.map(product => getProduct(product.code) )).then(productsWithVariations => { resolve(productsWithVariations); }).catch(error => { logError(error); reject(error); }); } else { resolve(products); } } }) .catch((error) => { logError(error); reject(error); }); }); }; /** * loadLibrary * * Dynamically loads an external JavaScript library from a given URL and ensures it is only loaded once. * If the script is already present in the document, it resolves immediately with the existing script element. * * @param {string} src - The URL of the CDN-hosted library to load. * @param {string} prefix - A unique attribute name to prevent multiple initializations of the same script. * @param {boolean} async - Define to load the script async or sync * @returns {Promise<HTMLScriptElement>} - Resolves with the script element once loaded, or rejects if the loading fails. */ export const loadLibrary = (src, prefix, async = true) => { return new Promise((resolve, reject) => { prefix = prefix.toLowerCase(); if (qs(`script[data-${prefix}="true"]`)) { resolve(); return; } const script = document.createElement('script'); script.dataset[prefix] = true; script.src = src; script.async = async; script.onload = () => resolve(); script.onerror = () => { const error = new Error(`loadLibrary, failed to load script: ${src}`); logError(error); reject(error); } document.head.appendChild(script); }); }; /** * The addStyleToBody function injects a CSS string into the document head, using a unique prefix as a data attribute to prevent duplicate style insertions. * If a style element with the given prefix already exists, it does nothing. Otherwise, it creates and appends a new style element. * The purpose of this function is to prevent flickering when page url changes. * @param {string} css - The CSS string to be injected. * @param {string} prefix - The unique prefix used as a data attribute to identify the style element. */ export const addStyleToBody = (css, prefix) => { if (qs(`style[data-${prefix}="true"]`)) return; const style = document.createElement('style'); style.setAttribute(`data-${prefix}`, 'true'); style.appendChild(document.createTextNode(css)); document.head.appendChild(style); };