Source: utils/utils.js

/**
 * Gets the bounding client rectangle of an element.
 * Falls back to using Range if element.getBoundingClientRect is undefined.
 *
 * @param {Element} element - The DOM element to get the bounding rectangle for.
 * @returns {DOMRect | undefined} The bounding client rectangle or undefined if no element.
 */
export function getBoundingClientRect(element) {
	if (!element) {
		return;
	}
	let rect;
	if (typeof element.getBoundingClientRect !== "undefined") {
		rect = element.getBoundingClientRect();
	} else {
		let range = document.createRange();
		range.selectNode(element);
		rect = range.getBoundingClientRect();
	}
	return rect;
}

/**
 * Gets the client rectangles of an element.
 * Falls back to using Range if element.getClientRects is undefined.
 *
 * @param {Element} element - The DOM element to get client rectangles for.
 * @returns {DOMRectList | undefined} The client rectangles or undefined if no element.
 */
export function getClientRects(element) {
	if (!element) {
		return;
	}
	let rect;
	if (typeof element.getClientRects !== "undefined") {
		rect = element.getClientRects();
	} else {
		let range = document.createRange();
		range.selectNode(element);
		rect = range.getClientRects();
	}
	return rect;
}

/**
 * Generates a UUID (version 4).
 * Based on: http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript
 *
 * @returns {string} A UUID string.
 */
export function UUID() {
	var d = new Date().getTime();
	if (
		typeof performance !== "undefined" &&
		typeof performance.now === "function"
	) {
		d += performance.now(); //use high-precision timer if available
	}
	return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
		var r = (d + Math.random() * 16) % 16 | 0;
		d = Math.floor(d / 16);
		return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
	});
}

/**
 * Find the position of an element in a NodeList.
 *
 * @param {Element} element - The element to find.
 * @param {NodeList} nodeList - The NodeList to search within.
 * @returns {number} The index of the element in the NodeList, or -1 if not found.
 */
export function positionInNodeList(element, nodeList) {
	for (let i = 0; i < nodeList.length; i++) {
		if (element === nodeList[i]) {
			return i;
		}
	}
	return -1;
}

/**
 * Finds a unique CSS selector for a given element.
 * The selector is unique within the element's document.
 *
 * @param {Element} ele - The element to find a selector for.
 * @returns {string} A unique CSS selector string.
 */
export function findCssSelector(ele) {
	let document = ele.ownerDocument;

	let cssEscape = window.CSS.escape;

	if (
		ele.id &&
		document.querySelectorAll("#" + cssEscape(ele.id)).length === 1
	) {
		return "#" + cssEscape(ele.id);
	}

	let tagName = ele.localName;
	if (tagName === "html") {
		return "html";
	}
	if (tagName === "head") {
		return "head";
	}
	if (tagName === "body") {
		return "body";
	}

	let selector, index, matches;
	if (ele.classList.length > 0) {
		for (let i = 0; i < ele.classList.length; i++) {
			selector = "." + cssEscape(ele.classList.item(i));
			matches = document.querySelectorAll(selector);
			if (matches.length === 1) {
				return selector;
			}
			selector = cssEscape(tagName) + selector;
			matches = document.querySelectorAll(selector);
			if (matches.length === 1) {
				return selector;
			}
			index = positionInNodeList(ele, ele.parentNode.children) + 1;
			selector = selector + ":nth-child(" + index + ")";
			matches = document.querySelectorAll(selector);
			if (matches.length === 1) {
				return selector;
			}
		}
	}

	if (ele.parentNode !== document && ele.parentNode.nodeType === 1) {
		index = positionInNodeList(ele, ele.parentNode.children) + 1;
		selector =
			findCssSelector(ele.parentNode) +
			" > " +
			cssEscape(tagName) +
			":nth-child(" +
			index +
			")";
	}

	return selector;
}

/**
 * Returns the value of the first attribute found from the given list on the element.
 *
 * @param {Element} element - The element to check attributes on.
 * @param {string[]} attributes - Array of attribute names to look for.
 * @returns {string | null | undefined} The attribute value, or undefined if none found.
 */
export function attr(element, attributes) {
	for (var i = 0; i < attributes.length; i++) {
		if (element.hasAttribute(attributes[i])) {
			return element.getAttribute(attributes[i]);
		}
	}
}

/**
 * Escapes a string for use in a CSS selector.
 * Allows # and . characters.
 *
 * @param {string} value - The string to escape.
 * @returns {string} The escaped string.
 * @throws {TypeError} If no argument is provided.
 */
export function querySelectorEscape(value) {
	if (arguments.length == 0) {
		throw new TypeError("`CSS.escape` requires an argument.");
	}
	var string = String(value);

	var length = string.length;
	var index = -1;
	var codeUnit;
	var result = "";
	var firstCodeUnit = string.charCodeAt(0);
	while (++index < length) {
		codeUnit = string.charCodeAt(index);

		if (codeUnit == 0x0000) {
			result += "\uFFFD";
			continue;
		}

		if (
			(codeUnit >= 0x0001 && codeUnit <= 0x001f) ||
			codeUnit == 0x007f ||
			(index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
			(index == 1 &&
				codeUnit >= 0x0030 &&
				codeUnit <= 0x0039 &&
				firstCodeUnit == 0x002d)
		) {
			result += "\\" + codeUnit.toString(16) + " ";
			continue;
		}

		if (index == 0 && length == 1 && codeUnit == 0x002d) {
			result += "\\" + string.charAt(index);
			continue;
		}

		if (codeUnit == 0x002e) {
			if (string.charAt(0) == "#") {
				result += "\\.";
				continue;
			}
		}

		if (
			codeUnit >= 0x0080 ||
			codeUnit == 0x002d ||
			codeUnit == 0x005f ||
			codeUnit == 35 || // Allow #
			codeUnit == 46 || // Allow .
			(codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
			(codeUnit >= 0x0041 && codeUnit <= 0x005a) ||
			(codeUnit >= 0x0061 && codeUnit <= 0x007a)
		) {
			result += string.charAt(index);
			continue;
		}

		result += "\\" + string.charAt(index);
	}
	return result;
}

/**
 * Creates a deferred object with promise, resolve, and reject.
 *
 * @returns {object} Deferred object with properties:
 *   - promise: {Promise} The promise object.
 *   - resolve: {function} Function to resolve the promise.
 *   - reject: {function} Function to reject the promise.
 *   - id: {string} Unique identifier.
 */
export function defer() {
	this.resolve = null;
	this.reject = null;
	this.id = UUID();

	this.promise = new Promise((resolve, reject) => {
		this.resolve = resolve;
		this.reject = reject;
	});
	Object.freeze(this);
}

/**
 * Uses requestIdleCallback if available, otherwise falls back to requestAnimationFrame.
 */
export const requestIdleCallback =
	typeof window !== "undefined" &&
	("requestIdleCallback" in window
		? window.requestIdleCallback
		: window.requestAnimationFrame);

/**
 * Converts a CSSValue object to a string representation.
 *
 * @param {Object} obj - The CSSValue object.
 * @returns {string} The combined CSS value and unit string.
 */
export function CSSValueToString(obj) {
	return obj.value + (obj.unit || "");
}