function elementExists(el) {
return document.querySelectorAll(el).length > 0;
}
function isFunction(x) {
return typeof x === "function";
}
function isString(x) {
return typeof x === "string";
}
/**
* @callback pollForCallback
* @param {string|function|array} assertion - The assertion that was passed to pollFor
*/
/**
* A poller which allows you to wait for specific criteria before running
* a callback function.
* @param {string|function|array} assertion - Either a CSS selector, a function that returns a boolean, or an array of functions
* @param {pollForCallback} onSuccess - The function to run when the assertion has returned true
* @param {number|null} [timeout=10] - How many seconds should we poll for before giving up
* @param {pollForCallback|null} [onTimeout] - An optional function to run when polling has timed out
*/
function pollFor(assertion, onSuccess) {
var timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 10;
var onTimeout = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
var test,
// Holds the function that will be tested on each loop
expired = false,
// A flag that will be set to true on timeout
timeoutInSeconds = timeout * 1000; // Converts the seconds passed in to milliseconds
// Convert the assertion into a testable function
if (isFunction(assertion)) {
test = assertion;
} else if (isString(assertion)) {
test = function test() {
return elementExists(assertion);
};
} else if (Array.isArray(assertion)) {
test = function test() {
return assertion.reduce(function (o, n) {
if (typeof n !== "function" && typeof n !== "string") {
throw new Error("assertion is not a string or function");
}
o.push(typeof n === "function" ? n() : elementExists(n));
return o;
}, []).indexOf(false) === -1; // All assertions need to evaluate to true
};
} else {
throw new Error("assertion must be a Function, String, or Array");
} // Ensure backwards compatability for requestAnimationFrame;
var requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
window.setTimeout(callback, 1000 / 60);
}; // This will repeatedly test the assertion until expired is true
function loop() {
if (expired === true) {
// If onTimeout exists, call it
if (isFunction(onTimeout)) {
onTimeout(assertion);
}
} else {
if (test() === true) {
onSuccess(assertion);
} else {
requestAnimationFrame(loop);
}
}
} // Kick off the loop
if (typeof test === "function") {
loop(); // Set the expired flag to true after the elapsed timeout
window.setTimeout(function () {
expired = true;
}, timeoutInSeconds);
}
}
/**
* A poller which allows you to wait for multiple elements to exist before running
* a callback function.
* @param {function} callback - The function to run when the element(s) have been found
* @param {string} arg - Pass multiple elements that you want to poll for. There is no limit to the amount of elements you can check, but the more you check for, the longer the poller will take to run.
*/
function pollElements(callback) {
for (var _len = arguments.length, arg = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
arg[_key - 1] = arguments[_key];
}
var poll_limit = 100,
poll_count = 0;
var waitForLoad = function waitForLoad() {
if (poll_conditions(arg)) {
callback();
} else if (poll_count < poll_limit) {
poll_count++;
window.setTimeout(waitForLoad, 5);
}
};
window.setTimeout(waitForLoad, 5);
function poll_conditions(arg) {
var cond = []; // stores element exists as true/false
arg.map(function (elementToCheck) {
cond.push(elementExists(elementToCheck));
});
if (cond.indexOf(false) !== -1) {
// one or more elements do not exist
return false;
} else {
// all elements exist
return true;
}
}
}
/**
* Function to initialise the mouse leave detection function.
* @param {string} conditions - Conditions to be met for the function to run. Has to return true.
* @param {function} callback - The function to run when the conditions have returned true
* @param {number} [threshold=5] - Threshold set for mouse y position
*/
function onMouseLeave(conditions, callback) {
var threshold = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 5;
// declare conditionCheck function as variable
// pass the event to the function
var conditionCheck = function conditionCheck(event) {
// check if conditions are met and that mouse y position is less than threshold (defaults to 5)
if (conditions && event.y < threshold) {
// call callback function
callback();
}
},
// declare listenerController function to easily handle adding and removing the event listent
listenerController = function listenerController(method) {
switch (method) {
case "add":
document.body.addEventListener("mouseleave", conditionCheck, false);
break;
case "remove":
document.body.removeEventListener("mouseleave", conditionCheck, false);
break;
}
},
select = document.getElementsByTagName("select"); // attach mouse leave event to body, call conditionCheck when mouse leave event detected
document.body.addEventListener("mouseleave", conditionCheck, false); // the following focusin and focusout event functions are for cross-browser compatibility. They ensure the condition check is not called on Edge, IE and possibly Firefox
// when a select element is focused, remove the event listener, when it is blurred, add the event listenr
for (var i = 0, length1 = select.length; i < length1; i++) {
select[i].addEventListener("focus", listenerController("remove"), false);
select[i].addEventListener("blur", listenerController("add"), false);
}
}
/**
* Call a function after there has been no interaction for a set period
* @param {function} callback - The function to run after no interaction
* @param {function} [time=7000] - How long before calling the callback in milliseconds
*/
function onNoInteraction(callback, time) {
var t,
delay = time || 7000;
function timeout() {
t = window.setTimeout(function () {
window.removeEventListener("scroll", reset);
window.removeEventListener("keyup", reset);
callback();
}, delay);
}
function clear(timer) {
window.clearTimeout(timer);
}
function reset() {
clear(t);
timeout();
}
window.addEventListener("scroll", reset);
window.addEventListener("keyup", reset);
timeout();
}
/**
* A trigger which runs a callback function based on whether an element on the page has been viewed by the user.
* @param {string} elementToCheck - A CSS selector
* @param {function} callback - The function to run when the elementToCheck has returned true
* @param {string} [elementIndex] - If there are multiple of the same CSS selectors available on the page, specify the index number of the element you want to target
*/
function onElementIsVisible(elementToCheck, callback, elementIndex) {
checkNumberOfElements();
var trackedElement;
var doneArray = [];
var check_if_elements_in_viewport = debounce(check_function, 50);
function checkNumberOfElements() {
if (document.querySelectorAll(elementToCheck).length > 1) {
if (elementIndex !== undefined) {
trackedElement = document.querySelectorAll(elementToCheck)[elementIndex];
} else {
throw new Error("There are multiple elements on page with the same selector name. Please specify the one you want to target");
}
} else {
trackedElement = document.querySelector(elementToCheck);
}
}
function elementInViewport(el) {
var top = el.offsetTop;
var left = el.offsetLeft;
var width = el.offsetWidth;
var height = el.offsetHeight;
while (el.offsetParent) {
el = el.offsetParent;
top += el.offsetTop;
left += el.offsetLeft;
}
return top >= window.pageYOffset && left >= window.pageXOffset && top + height <= window.pageYOffset + window.innerHeight && left + width <= window.pageXOffset + window.innerWidth;
}
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this,
args = arguments;
var later = function later() {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
}
function elementChecker(element) {
if (doneArray.indexOf(element) > -1) {
return true;
} else {
// element has not been seen before
return false;
}
}
function check_function() {
var is_in_viewport = elementInViewport(trackedElement),
already_seen = elementChecker(trackedElement);
if (!already_seen && is_in_viewport) {
//Element has been seen
doneArray.push(trackedElement);
callback();
}
}
if (trackedElement !== null) {
window.addEventListener("scroll", check_if_elements_in_viewport);
} else {
throw new Error("Element Not Found");
}
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
/**
* Function for GA tracking. Use ga.sendEvent() to call the function.
* @param {Object} gaPayload - Used to pass through specific properties for the GA tracking
* @param {string} gaPayload.trackingId - UA Tracking ID for test tracking to be sent to
* @param {string} gaPayload.dimensionNumber - Dimension index for test tracking to be sent to
* @param {string} gaPayload.campaignName - Campaign name for CRO test
* @param {boolean} gaPayload.notInteractive - Specify whether the event should NOT count as an interaction
* @param {string} [gaPayload.category=DDL CRO] - Event category to be sent to GA
* @param {string} gaPayload.action - Event action to be sent to GA
* @param {string} gaPayload.label - Event label to be sent to GA
* @param {number} [gaPayload.value=0] - Event value to be sent to GA
*/
function sendEvent(gaPayload) {
var trackerObject = "undefined";
gaPayload.trackingId = gaPayload.trackingId.toLocaleUpperCase();
if (gaPayload.trackingId.startsWith('G')) {
initialiseGA4Tracking(gaPayload);
} else {
initialiseUATracking(gaPayload);
}
function initialiseUATracking(data) {
if (typeof ga === "function" && ga.loaded) {
// for debugging only
// console.log('ga loaded')
return getUATrackerName(data);
} else {
// for debugging only
// console.log('ga not loaded')
setLooper(initialiseUATracking, data, 750, 15000); // stop looping if GA or tracker are not found after 15s
}
}
function initialiseGA4Tracking(data) {
window.dataLayer = window.dataLayer || [];
window.gtag = window.gtag || function () {
dataLayer.push(arguments);
};
getGA4TrackerName(data);
}
function getUATrackerName(data) {
var allTrackers; // use ga object methods inside a readyCallback as they're guaranteed to be available
ga(function () {
allTrackers = ga.getAll();
trackerObject = allTrackers.reduce(function (trackers, tracker) {
if (tracker.get("trackingId") === data.trackingId) {
return tracker;
}
return trackers; // accumulator always has to be returned in the reduce method
});
if (data.dimensionNumber) {
setUAdimension(data);
} else {
sendToUA(data);
}
});
}
function getGA4TrackerName(data) {
// use ga object methods inside a readyCallback as they're guaranteed to be available
if (data.dimensionNumber) {
setGA4dimension(data);
} else {
sendToGA4(data);
}
}
function setUAdimension(data) {
trackerObject.set("dimension" + data.dimensionNumber, data.campaignName); // for debugging only
// console.log('Set dimension' + data.dimensionNumber, data.campaignName)
return sendToUA(data);
}
function setGA4dimension(data) {
gtag('config', data.trackingId, _defineProperty({}, "cro_slot_" + data.dimensionNumber, data.campaignName)); // for debugging only
console.log('Set dimension' + data.dimensionNumber, data.campaignName);
return sendToGA4(data);
}
function sendToUA(data) {
trackerObject.send("event", {
nonInteraction: data.notInteractive,
eventCategory: data.category || "DDL CRO",
eventAction: data.action,
eventLabel: data.label,
eventValue: data.value || 0
}); // for debugging only
// console.log('event set', data.notInteractive, data.category, data.action, data.label, data.value)
}
function sendToGA4(data) {
gtag('event', data.action, {
'event_category': data.category || "DDL CRO",
'event_label': data.label,
'value': data.value || 0,
'non_interaction': data.notInteractive,
'send_to': data.trackingId
}); // for debugging only
// console.log('event set', data.notInteractive, data.category, data.action, data.label, data.value)
}
function setLooper(functionToLoop, params, timeToLoop, timeToStop) {
var initialiserLoop = setTimeout(function () {
functionToLoop(params);
}, timeToLoop);
setTimeout(function () {
clearTimeout(initialiserLoop); // stop looping after 15s
}, timeToStop);
}
}
var index = {
sendEvent: sendEvent
};
function initHotjar() {
window.hj = window.hj || function () {
(hj.q = hj.q || []).push(arguments);
};
}
/**
* Function for HJ tagging. Use hotjar.tag() to call the function.
* @param {string} name - Name of tag to pass through
*/
function tag(name) {
var n = Array.isArray(name) ? name : Array(name);
initHotjar();
hj("tagRecording", n);
}
/**
* Function for HJ triggering. Use hotjar.trigger() to call the function.
* @param {string} name - Name of trigger
*/
function trigger(name) {
initHotjar();
hj("trigger", name);
}
var index$1 = {
tag: tag,
trigger: trigger
};
/**
* Function to convert non-compatible array format to a standard array for IE compatibility
* @param {array} x - The array (nodelist, HTMLCollection etc) to be converted to a standard array (e.g. document.querySelectorAll(".all-my-selectors"))
*/
function toArray(x) {
return [].slice.call(x, 0);
}
/**
* A function that allows you to watch an element's mutations, and perform a callback function if a mutation is observed.
* @param {string|node} elemToObserve - A CSS selector or a node.
* @param {array|object|string} option - Pass multiple mutation observer options to be set to TRUE. The options are (case sensitive) {@link https://developer.mozilla.org/en-US/docs/Web/API/MutationObserverInit | childList, subtree, attributes, characterData, attributeOldValue, characterDataOldValue and attributeFilter.} The standard MutationObserver JS Object config can be passed in here for more complex options, such as the 'attributeFilter' option, which requires an array to be passed in with it. Finally, you can pass in a single config option to be set to true as a string.
* @param {boolean} pauseForMutations - Pass in true or false if you would like the observer to pause whilst mutations are made, then re-connect once complete. Typically the observer would be paused whilst changes are made to, or inside, the element that is being observed.
* @param {function} callback - The function to run when a mutation is observed.
*/
function mutObs(elemToObserve, option, pauseForMutations, callback) {
var _arguments = arguments;
var observer;
observer = new MutationObserver(function (mutations) {
if (pauseForMutations === true) {
observer.disconnect();
callback(mutations);
mutObs(_arguments[0], _arguments[1], _arguments[2], _arguments[3]);
} else {
callback(mutations);
}
});
var observerConfig = generateConfig(option);
var target = typeof elemToObserve === "string" ? document.querySelector(elemToObserve) : elemToObserve;
observer.observe(target, observerConfig);
function generateConfig(option) {
if (typeof option === "string") {
// IF option is a string
var config_object = "{ \"".concat(option, "\" : true}");
return JSON.parse(config_object);
} else if (typeof option !== "string" && !Array.isArray(option)) {
// IF option is a JS object
return option;
} else {
// IF option is an array of strings
var _config_object = "{";
option.forEach(function (config_opt, i) {
if (option.length === i + 1) {
_config_object = _config_object + " \"".concat(config_opt, "\" : true}");
} else {
_config_object = _config_object + " \"".concat(config_opt, "\" : true,");
}
});
return JSON.parse(_config_object);
}
}
return observer;
}
/**
* A JavaScript helper function that executes a callback when the URL changes, optionally matching against provided regular expression(s).
*
* @param {Function} callback - The function to execute when the URL changes.
* @param {RegExp|RegExp[]} [regex] - (Optional) The regular expression(s) to match against the URL.
* The callback will be executed only if the URL matches at least one of the provided regular expressions.
* If not specified or null, the callback will be executed on any URL change.
*
* @example
* executeOnUrlChange(() => {
* console.log('URL has changed!');
* }, /^https?:\/\/(www\.)?example\.com/);
*
* @notes
* - The function initializes by executing the callback immediately.
* - It uses a Mutation Observer to detect URL changes efficiently and accurately.
* - The callback is triggered only when there is a change in the URL and, if provided, when the URL matches the regular expression(s).
* - If the regular expression parameter is omitted or null, the callback will be executed on any URL change.
* - The function is compatible with modern browsers that support the Mutation Observer API.
* It may not work in older browsers such as Internet Explorer 10 and earlier.
*
* @returns {void}
*/
function executeOnUrlChange(callback, regex) {
var currentUrl = window.location.href;
var regexArr = Array.isArray(regex) ? regex : [regex].filter(Boolean);
var checkUrlChange = function checkUrlChange() {
var newUrl = window.location.href;
if (newUrl !== currentUrl && (!regexArr.length || regexArr.some(function (r) {
return r.test(newUrl);
}))) {
currentUrl = newUrl;
callback();
}
};
var observer = new MutationObserver(checkUrlChange); // Execute the callback immediately
callback(); // Observe changes in the body element (or any specific element you want to observe)
observer.observe(document.body, {
childList: true,
subtree: true
});
}
export { executeOnUrlChange, index as ga, index$1 as hotjar, mutObs, onElementIsVisible, onMouseLeave, onNoInteraction, pollElements, pollFor, toArray };