@douglas.onsite.experimentation/douglas-ab-testing-toolkit
Version:
DOUGLAS A/B Testing Toolkit
1,014 lines (870 loc) • 39.7 kB
JavaScript
/**
* 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);
};