UNPKG

tiny-essentials

Version:

Collection of small, essential scripts designed to be used across various projects. These simple utilities are crafted for speed, ease of use, and versatility.

1,402 lines (1,268 loc) 189 kB
'use strict'; var collision = require('../basics/collision.cjs'); const { areElsColliding, areElsPerfColliding, areElsCollTop, areElsCollBottom, areElsCollLeft, areElsCollRight, } = collision; /** * Callback invoked on each animation frame with the current scroll position, * normalized animation time (`0` to `1`), and a completion flag. * * @typedef {(progress: { x: number, y: number, isComplete: boolean, time: number }) => void} OnScrollAnimation */ /** * A list of supported easing function names for smooth animations. * * @typedef {'linear' | 'easeInQuad' | 'easeOutQuad' | 'easeInOutQuad' | 'easeInCubic' | 'easeOutCubic' | 'easeInOutCubic'} Easings */ /** * Represents a raw Node element or an instance of TinyHtml. * This type is used to abstract interactions with both plain elements * and wrapped elements via the TinyHtml class. * * @typedef {Node|TinyHtml|null} TinyNode */ /** * Represents a raw DOM element or an instance of TinyHtml. * This type is used to abstract interactions with both plain elements * and wrapped elements via the TinyHtml class. * * @typedef {Element|TinyHtml} TinyElement */ /** * Represents a raw DOM html element or an instance of TinyHtml. * This type is used to abstract interactions with both plain elements * and wrapped elements via the TinyHtml class. * * @typedef {HTMLElement|TinyHtml} TinyHtmlElement */ /** * Represents a raw DOM event target element or an instance of TinyHtml. * This type is used to abstract interactions with both plain elements * and wrapped elements via the TinyHtml class. * * @typedef {EventTarget|TinyHtml} TinyEventTarget */ /** * Represents a raw DOM input element or an instance of TinyHtml. * This type is used to abstract interactions with both plain elements * and wrapped elements via the TinyHtml class. * * @typedef {InputElement|TinyHtml} TinyInputElement */ /** * Represents a raw DOM element/window or an instance of TinyHtml. * This type is used to abstract interactions with both plain elements * and wrapped elements via the TinyHtml class. * * @typedef {ElementAndWindow|TinyHtml} TinyElementAndWindow */ /** * Represents a value that can be either a DOM Element or the global Window object. * Useful for functions that operate generically on scrollable or measurable targets. * * @typedef {Element|Window} ElementAndWindow */ /** * Represents a raw DOM element/window/document or an instance of TinyHtml. * This type is used to abstract interactions with both plain elements * and wrapped elements via the TinyHtml class. * * @typedef {ElementAndWinAndDoc|TinyHtml} TinyElementAndWinAndDoc */ /** * Represents a value that can be either a DOM Element, or the global Window object, or the document object. * Useful for functions that operate generically on scrollable or measurable targets. * * @typedef {Element|Window|Document} ElementAndWinAndDoc */ /** * Represents a raw DOM element with document or an instance of TinyHtml. * This type is used to abstract interactions with both plain elements * and wrapped elements via the TinyHtml class. * * @typedef {ElementWithDoc|TinyHtml} TinyElementWithDoc */ /** * Represents a value that can be either a DOM Element, or the document object. * Useful for functions that operate generically on measurable targets. * * @typedef {Element|Document} ElementWithDoc */ /** * A parameter type used for filtering or matching elements. * It can be: * - A string (CSS selector), * - A raw DOM element, * - An array of raw DOM elements, * - A filtering function that receives an index and element, * and returns true if it matches. * * @typedef {string|Element|Element[]|((index: number, el: Element) => boolean)} WinnowRequest */ /** * Elements accepted as constructor values for TinyHtml. * These include common DOM elements and root containers. * * @typedef {Window|Element|Document|Text} ConstructorElValues */ /** * Options passed to `addEventListener` or `removeEventListener`. * Can be a boolean or an object of type `AddEventListenerOptions`. * * @typedef {boolean|AddEventListenerOptions} EventRegistryOptions */ /** * Structure describing a registered event callback and its options. * * @typedef {Object} EventRegistryItem * @property {EventListenerOrEventListenerObject|null} handler - The function to be executed when the event is triggered. * @property {EventRegistryOptions} [options] - Optional configuration passed to the listener. */ /** * Maps event names (e.g., `"click"`, `"keydown"`) to a list of registered handlers and options. * * @typedef {Record<string, EventRegistryItem[]>} EventRegistryList */ /** * WeakMap storing all event listeners per element. * Each element has a registry mapping event names to their handler lists. * * @type {WeakMap<ConstructorElValues|EventTarget, EventRegistryList>} */ const __eventRegistry = new WeakMap(); /** * A key-value store associated with a specific DOM element. * Keys are strings, and values can be of any type. * * @typedef {Record<string, *>} ElementDataStore */ /** * WeakMap to hold private data for elements * * @type {WeakMap<ConstructorElValues, ElementDataStore>} */ const __elementDataMap = new WeakMap(); /** * Stores directional collision locks separately. * Each direction has its own WeakMap to allow independent locking. * * @type {{ * top: WeakMap<Element, true>, * bottom: WeakMap<Element, true>, * left: WeakMap<Element, true>, * right: WeakMap<Element, true> * }} */ const __elemCollision = { top: new WeakMap(), bottom: new WeakMap(), left: new WeakMap(), right: new WeakMap(), }; /** * Possible directions from which a collision was detected and locked. * * @typedef {'top'|'bottom'|'left'|'right'} CollisionDirLock */ /** * @typedef {Object} HtmlElBoxSides * @property {number} x - Total horizontal size (left + right) * @property {number} y - Total vertical size (top + bottom) * @property {number} left * @property {number} right * @property {number} top * @property {number} bottom */ /** * @typedef {string | number | Date | boolean | null} SetValueBase - Primitive types accepted as input values. */ /** * @typedef {'string' | 'date' | 'number'} GetValueTypes * Types of value extractors supported by TinyHtml._valTypes. */ /** * @typedef {SetValueBase|SetValueBase[]} SetValueList - A single value or an array of values to be assigned to the input element. */ /** * A list of HTML form elements that can have a `.value` property used by TinyHtml. * Includes common input types used in forms. * * @typedef {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLOptionElement} InputElement */ /** * Represents a parsed HTML element in JSON-like array form. * * @typedef {[ * tagName: string, // The tag name of the element (e.g., 'div', 'img') * attributes: Record<string, string>, // All element attributes as key-value pairs * ...children: (string | HtmlParsed)[] // Text or nested elements * ]} HtmlParsed */ /** * TinyHtml is a utility class that provides static and instance-level methods * for precise dimension and position computations on HTML elements. * It mimics some jQuery functionalities while using native browser APIs. * * Inspired by the jQuery project's open source implementations of element dimension * and offset utilities. This class serves as a lightweight alternative using modern DOM APIs. * * @class */ class TinyHtml { /** @typedef {import('../basics/collision.mjs').ObjRect} ObjRect */ static Utils = { ...collision }; /** * Fetches an HTML file from the given URL, parses it to JSON. * * @param {string | URL | globalThis.Request} url - The URL of the HTML file. * @param {RequestInit} [ops] - Optional fetch configuration (e.g., method, headers, cache, etc). * @returns {Promise<HtmlParsed[]>} A promise that resolves with the parsed JSON representation of the HTML structure. */ static async fetchHtmlFile(url, ops = { method: 'GET' }) { const res = await fetch(url, ops); const contentType = res.headers.get('Content-Type') || ''; if (!res.ok) throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`); // Only accept HTML responses if (!contentType.includes('text/html')) { throw new Error(`Invalid content type: ${contentType} (expected text/html)`); } const html = await res.text(); return TinyHtml.htmlToJson(html); } /** * Fetches an HTML file from the given URL, parses it to JSON, then converts it to DOM nodes. * * @param {string} url - The URL of the HTML file. * @param {RequestInit} [ops] - Optional fetch configuration (e.g., method, headers, cache, etc). * @returns {Promise<(HTMLElement|Text)[]>} A promise that resolves with the DOM nodes. */ static async fetchHtmlNodes(url, ops) { const json = await TinyHtml.fetchHtmlFile(url, ops); return TinyHtml.jsonToNodes(json); } /** * Fetches an HTML file from the given URL, parses it to JSON, then converts it to TinyHtml instances. * * @param {string} url - The URL of the HTML file. * @param {RequestInit} [ops] - Optional fetch configuration (e.g., method, headers, cache, etc). * @returns {Promise<TinyHtml[]>} A promise that resolves with the TinyHtml instances. */ static async fetchHtmlTinyElems(url, ops) { const nodes = await TinyHtml.fetchHtmlNodes(url, ops); return TinyHtml.toTinyElm(nodes); } /** * Converts the content of a <template> to an array of HtmlParsed. * * @param {HTMLTemplateElement} nodes * @returns {HtmlParsed[]} */ static templateToJson(nodes) { return TinyHtml.htmlToJson( [...nodes.content.childNodes] .map((node) => node instanceof Element ? node.getHTML() : node instanceof Text ? node.textContent : '', ) .join(''), ); } /** * Converts the content of a <template> to real DOM nodes. * * @param {HTMLTemplateElement} nodes * @returns {(Element|Text)[]} */ static templateToNodes(nodes) { /** @type {(Element|Text)[]} */ const result = []; [...nodes.content.cloneNode(true).childNodes].map((node) => { if (!(node instanceof Element) && !(node instanceof Text) && !(node instanceof Comment)) throw new Error( `Expected only Element nodes in <template>, but found: ${node.constructor.name}`, ); if (!(node instanceof Comment)) result.push(node); }); return result; } /** * Converts the content of a <template> to an array of TinyHtml elements. * * @param {HTMLTemplateElement} nodes * @returns {TinyHtml[]} */ static templateToTinyElems(nodes) { return TinyHtml.toTinyElm(TinyHtml.templateToNodes(nodes)); } /** * Parses a full HTML string into a JSON-like structure. * * @param {string} htmlString - Full HTML markup as a string. * @returns {HtmlParsed[]} An array of parsed HTML elements in structured format. */ static htmlToJson(htmlString) { const container = document.createElement('div'); container.innerHTML = htmlString.trim(); const result = []; /** * @param {Node} el * @returns {*} */ const parseElement = (el) => { if (el instanceof Comment) return null; if (el instanceof Text) { const text = el.textContent?.trim(); return text ? text : null; } if (!(el instanceof Element)) return null; const tag = el.tagName.toLowerCase(); /** @type {Record<string, string>} */ const props = {}; for (const attr of el.attributes) { props[TinyHtml.getPropName(attr.name)] = attr.value; } const children = Array.from(el.childNodes).map(parseElement).filter(Boolean); return children.length > 0 ? [tag, props, ...children] : [tag, props]; }; for (const child of container.childNodes) { const parsed = parseElement(child); if (parsed) result.push(parsed); } container.innerHTML = ''; return result; } /** * Converts a JSON-like HTML structure back to DOM Elements. * * @param {HtmlParsed[]} jsonArray - Parsed JSON format of HTML. * @returns {(HTMLElement|Text)[]} List of DOM nodes. */ static jsonToNodes(jsonArray) { /** * @param {HtmlParsed|string} nodeData * @returns {HTMLElement|Text} */ const createElement = (nodeData) => { if (typeof nodeData === 'string') { return document.createTextNode(nodeData); } if (!Array.isArray(nodeData)) return document.createTextNode(''); const [tag, props, ...children] = nodeData; const el = document.createElement(tag); for (const [key, value] of Object.entries(props)) { el.setAttribute(TinyHtml.getAttrName(key), value); } for (const child of children) { const childEl = createElement(child); if (childEl instanceof Comment) continue; el.appendChild(childEl); } return el; }; return jsonArray.map(createElement).filter((node) => !(node instanceof Comment)); } /** * Converts a JSON-like HTML structure back to TinyHtml instances. * * @param {HtmlParsed[]} jsonArray - Parsed JSON format of HTML. * @returns {TinyHtml[]} List of TinyHtml instances. */ static jsonToTinyElems(jsonArray) { return TinyHtml.toTinyElm(TinyHtml.jsonToNodes(jsonArray)); } /** * Creates a new TinyHtml element from a tag name and optional attributes. * * You can alias this method for convenience: * ```js * const createElement = TinyHtml.createFrom; * const myDiv = createElement('div', { class: 'box' }); * ``` * * @param {string} tagName - The HTML tag name (e.g., 'div', 'span', 'button'). * @param {Record<string, string|null>} [attrs] - Optional key-value pairs representing HTML attributes. * If the value is `null`, the attribute will still be set with an empty value. * @returns {TinyHtml} - A new instance of TinyHtml representing the created element. * @throws {TypeError} - If `tagName` is not a string, or `attrs` is not a plain object when defined. */ static createFrom(tagName, attrs) { if (typeof tagName !== 'string') throw new TypeError('The "tagName" must be a string.'); if (typeof attrs !== 'undefined' && typeof attrs !== 'object') throw new TypeError('The "attrs" must be a object.'); const elem = TinyHtml.createElement(tagName); if (typeof attrs === 'object') { for (const attrName in attrs) { elem.setAttr(attrName, attrs[attrName]); } } return elem; } /** * Creates a new DOM element with the specified tag name and options, then wraps it in a TinyHtml instance. * * @param {string} tagName - The tag name of the element to create (e.g., 'div', 'span'). * @param {ElementCreationOptions} [ops] - Optional settings for creating the element. * @returns {TinyHtml} A TinyHtml instance wrapping the newly created DOM element. * @throws {TypeError} If tagName is not a string or ops is not an object. */ static createElement(tagName, ops) { if (typeof tagName !== 'string') throw new TypeError(`[TinyHtml] createElement(): The tagName must be a string.`); if (typeof ops !== 'undefined' && typeof ops !== 'object') throw new TypeError(`[TinyHtml] createElement(): The ops must be a object.`); return new TinyHtml(document.createElement(tagName, ops)); } /** * Creates a new TinyHtml instance that wraps a DOM TextNode. * * This method is useful when you want to insert raw text content into the DOM * without it being interpreted as HTML. The returned instance behaves like any * other TinyHtml element and can be appended or manipulated as needed. * * @param {string} value - The plain text content to be wrapped in a TextNode. * @returns {TinyHtml} A TinyHtml instance wrapping the newly created DOM TextNode. * @throws {TypeError} If the provided value is not a string. */ static createTextNode(value) { if (typeof value !== 'string') throw new TypeError(`[TinyHtml] createTextNode(): The value must be a string.`); return new TinyHtml(document.createTextNode(value)); } /** * Creates an HTMLElement or TextNode from an HTML string. * Supports both elements and plain text. * * @param {string} htmlString - The HTML string to convert. * @returns {TinyHtml} - A single HTMLElement or TextNode. */ static createElementFromHTML(htmlString) { const template = document.createElement('template'); htmlString = htmlString.trim(); // If it's only text (e.g., no "<" tag), return a TextNode if (!htmlString.startsWith('<')) { return TinyHtml.createTextNode(htmlString); } template.innerHTML = htmlString; if (!(template.content.firstChild instanceof Element)) throw new Error('The HTML string must contain a valid HTML element.'); return new TinyHtml(template.content.firstChild); } /** * Queries the document for the first element matching the CSS selector and wraps it in a TinyHtml instance. * * @param {string} selector - A valid CSS selector string. * @param {Document|Element} elem - Target element. * @returns {TinyHtml|null} A TinyHtml instance wrapping the matched element. */ static query(selector, elem = document) { const newEl = elem.querySelector(selector); if (!newEl) return null; return new TinyHtml(newEl); } /** * Queries the element for the first element matching the CSS selector and wraps it in a TinyHtml instance. * * @param {string} selector - A valid CSS selector string. * @returns {TinyHtml|null} A TinyHtml instance wrapping the matched element. */ querySelector(selector) { return TinyHtml.query(selector, TinyHtml._preElem(this, 'query')); } /** * Queries the document for all elements matching the CSS selector and wraps them in TinyHtml instances. * * @param {string} selector - A valid CSS selector string. * @param {Document|Element} elem - Target element. * @returns {TinyHtml} An array of TinyHtml instances wrapping the matched elements. */ static queryAll(selector, elem = document) { return new TinyHtml(elem.querySelectorAll(selector)); } /** * Queries the element for all elements matching the CSS selector and wraps them in TinyHtml instances. * * @param {string} selector - A valid CSS selector string. * @returns {TinyHtml} An array of TinyHtml instances wrapping the matched elements. */ querySelectorAll(selector) { return TinyHtml.queryAll(selector, TinyHtml._preElem(this, 'queryAll')); } /** * Retrieves an element by its ID and wraps it in a TinyHtml instance. * * @param {string} selector - The ID of the element to retrieve. * @returns {TinyHtml|null} A TinyHtml instance wrapping the found element. */ static getById(selector) { const newEl = document.getElementById(selector); if (!newEl) return null; return new TinyHtml(newEl); } /** * Retrieves all elements with the specified class name and wraps them in TinyHtml instances. * * @param {string} selector - The class name to search for. * @param {Document|Element} elem - Target element. * @returns {TinyHtml} An array of TinyHtml instances wrapping the found elements. */ static getByClassName(selector, elem = document) { return new TinyHtml(elem.getElementsByClassName(selector)); } /** * Retrieves all elements with the specified class name and wraps them in TinyHtml instances. * * @param {string} selector - The class name to search for. * @returns {TinyHtml} An array of TinyHtml instances wrapping the found elements. */ getElementsByClassName(selector) { return TinyHtml.getByClassName(selector, TinyHtml._preElem(this, 'getByClassName')); } /** * Retrieves all elements with the specified name attribute and wraps them in TinyHtml instances. * * @param {string} selector - The name attribute to search for. * @returns {TinyHtml} An array of TinyHtml instances wrapping the found elements. */ static getByName(selector) { return new TinyHtml(document.getElementsByName(selector)); } /** * Retrieves all elements with the specified local tag name within the given namespace URI, * and wraps them in TinyHtml instances. * * @param {string} localName - The local name (tag) of the elements to search for. * @param {string|null} [namespaceURI='http://www.w3.org/1999/xhtml'] - The namespace URI to search within. * @param {Document|Element} elem - Target element. * @returns {TinyHtml} An array of TinyHtml instances wrapping the found elements. */ static getByTagNameNS(localName, namespaceURI = 'http://www.w3.org/1999/xhtml', elem = document) { return new TinyHtml(elem.getElementsByTagNameNS(namespaceURI, localName)); } /** * Retrieves all elements with the specified local tag name within the given namespace URI, * and wraps them in TinyHtml instances. * * @param {string} localName - The local name (tag) of the elements to search for. * @param {string|null} [namespaceURI='http://www.w3.org/1999/xhtml'] - The namespace URI to search within. * @returns {TinyHtml} An array of TinyHtml instances wrapping the found elements. */ getElementsByTagNameNS(localName, namespaceURI = 'http://www.w3.org/1999/xhtml') { return TinyHtml.getByTagNameNS( localName, namespaceURI, TinyHtml._preElem(this, 'getByTagNameNS'), ); } ////////////////////////////////////////////////////////////////// /** * Returns the current target held by this instance. * * @param {number} index - The index of the element to retrieve. * @returns {ConstructorElValues} - The instance's target element. */ get(index) { if (typeof index !== 'number') throw new TypeError('The index must be a number.'); if (!this.#el[index]) throw new Error(`No element found at index ${index}.`); return this.#el[index]; } /** * Extracts a single DOM element from the internal list at the specified index. * * @param {number} index - The index of the element to extract. * @returns {TinyHtml} A new TinyHtml instance wrapping the extracted element. */ extract(index) { if (typeof index !== 'number') throw new TypeError('The index must be a number.'); if (!this.#el[index]) throw new Error(`Cannot extract: no element exists at index ${index}.`); return new TinyHtml(this.#el[index]); } /** * Checks whether the element exists at the given index. * * @param {number} index - The index to check. * @returns {boolean} - True if the element exists; otherwise, false. */ exists(index) { if (typeof index !== 'number') throw new TypeError('The index must be a number.'); if (!this.#el[index]) return false; return true; } /** * Returns the current targets held by this instance. * * @returns {ConstructorElValues[]} - The instance's targets element. */ getAll() { return [...this.#el]; } /** * Returns the current Element held by this instance. * * @param {string} where - The method name or context calling this. * @param {number} index - The index of the element to retrieve. * @returns {ConstructorElValues} - The instance's element. * @readonly */ _getElement(where, index) { if ( !(this.#el[index] instanceof Element) && !(this.#el[index] instanceof Window) && !(this.#el[index] instanceof Document) && !(this.#el[index] instanceof Text) ) throw new Error(`[TinyHtml] Invalid Element in ${where}().`); return this.#el[index]; } /** * Returns the current Elements held by this instance. * * @param {string} where - The method name or context calling this. * @returns {ConstructorElValues[]} - The instance's elements. * @readonly */ _getElements(where) { if ( !this.#el.every( (el) => el instanceof Element || el instanceof Window || el instanceof Document || el instanceof Text, ) ) throw new Error(`[TinyHtml] Invalid Element in ${where}().`); return [...this.#el]; } ////////////////////////////////////////////////////// /** * Prepares and validates multiple elements against allowed types. * * @param {TinyElement | EventTarget | null | (TinyElement | EventTarget | null)[]} elems - The input elements to validate. * @param {string} where - The method name or context calling this. * @param {any[]} TheTinyElements - The list of allowed constructors (e.g., Element, Document). * @param {string[]} elemName - The list of expected element names for error reporting. * @returns {any[]} - A flat array of validated elements. * @throws {Error} - If any element is not an instance of one of the allowed types. * @readonly */ static _preElemsTemplate(elems, where, TheTinyElements, elemName) { /** @param {(TinyElement|EventTarget|null)[]} item */ const checkElement = (item) => { /** @type {any[]} */ const results = []; item.map((elem) => (elem instanceof TinyHtml ? elem._getElements(where) : [elem]).map((result) => { let allowed = false; for (const TheTinyElement of TheTinyElements) { if (result instanceof TheTinyElement) { allowed = true; break; } } if (!allowed) throw new Error( `[TinyHtml] Invalid element of the list "${elemName.join(',')}" in ${where}().`, ); results.push(result); return result; }), ); return results; }; if (!Array.isArray(elems)) return checkElement([elems]); return checkElement(elems); } /** * Prepares and validates a single element against allowed types. * * @param {TinyElement | EventTarget | null | (TinyElement | EventTarget | null)[]} elems - The input element or list to validate. * @param {string} where - The method name or context calling this. * @param {any[]} TheTinyElements - The list of allowed constructors (e.g., Element, Document). * @param {string[]} elemName - The list of expected element names for error reporting. * @param {boolean} [canNull=false] - Whether `null` is allowed as a valid value. * @returns {any} - The validated element or `null` if allowed. * @throws {Error} - If the element is not valid or if multiple elements are provided. * @readonly */ static _preElemTemplate(elems, where, TheTinyElements, elemName, canNull = false) { /** @param {(TinyElement|EventTarget|null)[]} item */ const checkElement = (item) => { const elem = item[0]; const result = elem instanceof TinyHtml ? elem._getElements(where) : [elem]; if (result.length > 1) throw new Error( `[TinyHtml] Invalid element amount in ${where}() (Received ${result.length}/1).`, ); let allowed = false; for (const TheTinyElement of TheTinyElements) { if (result[0] instanceof TheTinyElement) { allowed = true; break; } } if (canNull && (result[0] === null || typeof result[0] === 'undefined')) { result[0] = null; allowed = true; } if (!allowed) throw new Error( `[TinyHtml] Invalid element of the list "${elemName.join(',')}" in ${where}().`, ); return result[0]; }; if (!Array.isArray(elems)) return checkElement([elems]); if (elems.length > 1) throw new Error( `[TinyHtml] Invalid element amount in ${where}() (Received ${elems.length}/1).`, ); return checkElement(elems); } /** * Ensures the input is returned as an array. * Useful to normalize operations across multiple or single elements. * * @param {TinyElement|TinyElement[]} elems - A single element or array of elements. * @param {string} where - The method or context name where validation is being called. * @returns {Element[]} - Always returns an array of elements. * @readonly */ static _preElems(elems, where) { return TinyHtml._preElemsTemplate(elems, where, [Element], ['Element']); } /** * Ensures the input is returned as an single element. * Useful to normalize operations across multiple or single elements. * * @param {TinyElement|TinyElement[]} elems - A single element or array of elements. * @param {string} where - The method or context name where validation is being called. * @returns {Element} - Always returns an single element. * @readonly */ static _preElem(elems, where) { return TinyHtml._preElemTemplate(elems, where, [Element], ['Element']); } /** * Ensures the input is returned as an array. * Useful to normalize operations across multiple or single nodes. * * @param {TinyNode|TinyNode[]} elems - A single node or array of nodes. * @param {string} where - The method or context name where validation is being called. * @returns {Node[]} - Always returns an array of nodes. * @readonly */ static _preNodeElems(elems, where) { return TinyHtml._preElemsTemplate(elems, where, [Node], ['Node']); } /** * Ensures the input is returned as an single node. * Useful to normalize operations across multiple or single nodes. * * @param {TinyNode|TinyNode[]} elems - A single node or array of nodes. * @param {string} where - The method or context name where validation is being called. * @returns {Node} - Always returns an single node. * @readonly */ static _preNodeElem(elems, where) { return TinyHtml._preElemTemplate(elems, where, [Node], ['Node']); } /** * Ensures the input is returned as an single node. * Useful to normalize operations across multiple or single nodes. * * @param {TinyNode|TinyNode[]} elems - A single node or array of nodes. * @param {string} where - The method or context name where validation is being called. * @returns {Node|null} - Always returns an single node or null. * @readonly */ static _preNodeElemWithNull(elems, where) { return TinyHtml._preElemTemplate(elems, where, [Node], ['Node'], true); } /** * Ensures the input is returned as an array. * Useful to normalize operations across multiple or single html elements. * * @param {TinyElement|TinyElement[]} elems - A single html element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {HTMLElement[]} - Always returns an array of html elements. * @readonly */ static _preHtmlElems(elems, where) { return TinyHtml._preElemsTemplate(elems, where, [HTMLElement], ['HTMLElement']); } /** * Ensures the input is returned as an single html element. * Useful to normalize operations across multiple or single html elements. * * @param {TinyElement|TinyElement[]} elems - A single html element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {HTMLElement} - Always returns an single html element. * @readonly */ static _preHtmlElem(elems, where) { return TinyHtml._preElemTemplate(elems, where, [HTMLElement], ['HTMLElement']); } /** * Ensures the input is returned as an array. * Useful to normalize operations across multiple or single event target elements. * * @param {TinyInputElement|TinyInputElement[]} elems - A single event target element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {InputElement[]} - Always returns an array of event target elements. * @readonly */ static _preInputElems(elems, where) { return TinyHtml._preElemsTemplate( elems, where, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, HTMLOptionElement], ['HTMLInputElement', 'HTMLSelectElement', 'HTMLTextAreaElement', 'HTMLOptionElement'], ); } /** * Ensures the input is returned as an single event target element. * Useful to normalize operations across multiple or single event target elements. * * @param {TinyInputElement|TinyInputElement[]} elems - A single event target element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {InputElement} - Always returns an single event target element. * @readonly */ static _preInputElem(elems, where) { return TinyHtml._preElemTemplate( elems, where, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, HTMLOptionElement], ['HTMLInputElement', 'HTMLSelectElement', 'HTMLTextAreaElement', 'HTMLOptionElement'], ); } /** * Ensures the input is returned as an array. * Useful to normalize operations across multiple or single event target elements. * * @param {TinyEventTarget|TinyEventTarget[]} elems - A single event target element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {EventTarget[]} - Always returns an array of event target elements. * @readonly */ static _preEventTargetElems(elems, where) { return TinyHtml._preElemsTemplate(elems, where, [EventTarget], ['EventTarget']); } /** * Ensures the input is returned as an single event target element. * Useful to normalize operations across multiple or single event target elements. * * @param {TinyEventTarget|TinyEventTarget[]} elems - A single event target element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {EventTarget} - Always returns an single event target element. * @readonly */ static _preEventTargetElem(elems, where) { return TinyHtml._preElemTemplate(elems, where, [EventTarget], ['EventTarget']); } /** * Ensures the input is returned as an array. * Useful to normalize operations across multiple or single element/window elements. * * @param {TinyElementAndWindow|TinyElementAndWindow[]} elems - A single element/window element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {ElementAndWindow[]} - Always returns an array of element/window elements. * @readonly */ static _preElemsAndWindow(elems, where) { return TinyHtml._preElemsTemplate(elems, where, [Element, Window], ['Element', 'Window']); } /** * Ensures the input is returned as an single element/window element. * Useful to normalize operations across multiple or single element/window elements. * * @param {TinyElementAndWindow|TinyElementAndWindow[]} elems - A single element/window element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {ElementAndWindow} - Always returns an single element/window element. * @readonly */ static _preElemAndWindow(elems, where) { return TinyHtml._preElemTemplate(elems, where, [Element, Window], ['Element', 'Window']); } /** * Ensures the input is returned as an array. * Useful to normalize operations across multiple or single element/window/document elements. * * @param {TinyElementAndWinAndDoc|TinyElementAndWinAndDoc[]} elems - A single element/document/window element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {ElementAndWindow[]} - Always returns an array of element/document/window elements. * @readonly */ static _preElemsAndWinAndDoc(elems, where) { const result = TinyHtml._preElemsTemplate( elems, where, [Element, Window, Document], ['Element', 'Window', 'Document'], ); return result.map((elem) => (!(elem instanceof Document) ? elem : elem.documentElement)); } /** * Ensures the input is returned as an single element/window/document element. * Useful to normalize operations across multiple or single element/window/document elements. * * @param {TinyElementAndWinAndDoc|TinyElementAndWinAndDoc[]} elems - A single element/document/window element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {ElementAndWindow} - Always returns an single element/document/window element. * @readonly */ static _preElemAndWinAndDoc(elems, where) { const result = TinyHtml._preElemTemplate( elems, where, [Element, Window, Document], ['Element', 'Window', 'Document'], ); if (result instanceof Document) return result.documentElement; return result; } /** * Ensures the input is returned as an array. * Useful to normalize operations across multiple or single element with document elements. * * @param {TinyElementWithDoc|TinyElementWithDoc[]} elems - A single element with document element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {ElementWithDoc[]} - Always returns an array of element with document elements. * @readonly */ static _preElemsWithDoc(elems, where) { return TinyHtml._preElemsTemplate(elems, where, [Element, Document], ['Element', 'Document']); } /** * Ensures the input is returned as an single element with document element. * Useful to normalize operations across multiple or single element with document elements. * * @param {TinyElementWithDoc|TinyElementWithDoc[]} elems - A single element/window element or array of html elements. * @param {string} where - The method or context name where validation is being called. * @returns {ElementWithDoc} - Always returns an single element/window element. * @readonly */ static _preElemWithDoc(elems, where) { return TinyHtml._preElemTemplate(elems, where, [Element, Document], ['Element', 'Document']); } /** * Normalizes and converts one or more DOM elements (or TinyHtml instances) * into an array of `TinyHtml` instances. * * - If a plain DOM element is passed, it is wrapped into a `TinyHtml` instance. * - If a `TinyHtml` instance is already passed, it is preserved. * - If an array is passed, all elements inside are converted accordingly. * * This ensures consistent access to methods of the `TinyHtml` class regardless * of the input form. * * @param {TinyElement|Text|(TinyElement|Text)[]} elems - A single element or an array of elements (DOM or TinyHtml). * @returns {TinyHtml[]} An array of TinyHtml instances corresponding to the input elements. */ static toTinyElm(elems) { /** @param {(TinyElement|Text)[]} item */ const checkElement = (item) => item.map((elem) => (!(elem instanceof TinyHtml) ? new TinyHtml(elem) : elem)); if (!Array.isArray(elems)) return checkElement([elems]); return checkElement(elems); } /** * Extracts native `Element` instances from one or more elements, * which can be either raw DOM elements or wrapped in `TinyHtml`. * * - If a `TinyHtml` instance is passed, its internal DOM element is extracted. * - If a raw DOM element is passed, it is returned as-is. * - If an array is passed, each element is processed accordingly. * * This function guarantees that the return value is always an array of * raw `Element` objects, regardless of whether the input was * a mix of `TinyHtml` or native DOM elements. * * @param {TinyElement|TinyElement[]} elems - A single element or an array of elements (DOM or TinyHtml`). * @returns {Element[]} An array of Element instances extracted from the input. */ static fromTinyElm(elems) { /** @param {TinyElement[]} item */ const checkElement = (item) => { /** @type {Element[]} */ const result = []; item.map((elem) => /** @type {Element[]} */ ( elem instanceof TinyHtml ? elem._getElements('fromTinyElm') : [elem] ).map((elem) => result.push(elem)), ); return result; }; if (!Array.isArray(elems)) return checkElement([elems]); return checkElement(elems); } /** * Filters an array of elements based on a selector, function, element, or array of elements. * * @param {TinyElement|TinyElement[]} elems * @param {WinnowRequest} qualifier * @param {string} where - The context/method name using this validation. * @param {boolean} not Whether to invert the result (used for .not()) * @returns {Element[]} */ static winnow(elems, qualifier, where, not = false) { if (typeof not !== 'boolean') throw new TypeError('The "not" must be a boolean.'); if (typeof qualifier === 'function') { return TinyHtml._preElems(elems, where).filter( (el, i) => !!qualifier.call(el, i, el) !== not, ); } if (qualifier instanceof Element) { return TinyHtml._preElems(elems, where).filter((el) => (el === qualifier) !== not); } if ( Array.isArray(qualifier) || (typeof qualifier !== 'string' && // @ts-ignore qualifier.length != null) ) { return TinyHtml._preElems(elems, where).filter((el) => qualifier.includes(el) !== not); } // Assume it's a selector string let selector = qualifier; if (not) selector = `:not(${selector})`; return TinyHtml._preElems(elems, where).filter( (el) => el.nodeType === 1 && el.matches(selector), ); } /** * Filters a set of elements by a CSS selector. * * @param {TinyElement|TinyElement[]} elems * @param {string} selector * @param {boolean} not * @returns {Element[]} */ static filter(elems, selector, not = false) { if (not) selector = `:not(${selector})`; return TinyHtml._preElems(elems, 'filter').filter( (el) => el.nodeType === 1 && el.matches(selector), ); } /** * Returns only the elements matching the given selector or function. * * @param {TinyElement|TinyElement[]} elems * @param {WinnowRequest} selector * @returns {Element[]} */ static filterOnly(elems, selector) { return TinyHtml.winnow(elems, selector, 'filterOnly', false); } /** * Returns only the elements **not** matching the given selector or function. * * @param {TinyElement|TinyElement[]} elems * @param {WinnowRequest} selector * @returns {Element[]} */ static not(elems, selector) { return TinyHtml.winnow(elems, selector, 'not', true); } /** * Returns only the elements **not** matching the given selector or function. * * @param {WinnowRequest} selector * @returns {Element[]} */ not(selector) { return TinyHtml.not(this, selector); } /** * Finds elements matching a selector within a context. * * @param {TinyElement|TinyElement[]} context * @param {string} selector * @returns {Element[]} */ static find(context, selector) { const result = []; for (const el of TinyHtml._preElems(context, 'find')) { result.push(...el.querySelectorAll(selector)); } return [...new Set(result)]; } /** * Finds elements in your element matching a selector within a context. * * @param {string} selector * @returns {Element[]} */ find(selector) { return TinyHtml.find(this, selector); } /** * Checks if at least one element matches the selector. * * @param {TinyElement|TinyElement[]} elems * @param {WinnowRequest} selector * @returns {boolean} */ static is(elems, selector) { return TinyHtml.winnow(elems, selector, 'is', false).length > 0; } /** * Checks if the element matches the selector. * * @param {WinnowRequest} selector * @returns {boolean} */ is(selector) { return TinyHtml.is(this, selector); } /** * Returns elements from the current list that contain the given target(s). * @param {TinyElement|TinyElement[]} roots - A single element or an array of elements (DOM or TinyHtml). * @param {string|TinyElement|TinyElement[]} target - Selector or DOM element(s). * @returns {Element[]} Elements that contain the target. */ static has(roots, target) { const targets = typeof target === 'string' ? [...document.querySelectorAll(target)] : TinyHtml._preElems(target, 'has'); return TinyHtml._preElems(roots, 'has').filter((root) => targets.some((t) => root && root.contains(t)), ); } /** * Return if the element has the target(s). * @param {string|TinyElement|TinyElement[]} target - Selector or DOM element(s). * @returns {boolean} Elements that contain the target. */ has(target) { return TinyHtml.has(this, target).length > 0; } /** * Finds the closest ancestor (including self) that matches the selector. * * @param {TinyElement|TinyElement[]} els - A single element or an array of elements (DOM or TinyHtml). * @param {string|Element} selector - A selector string or DOM element to match. * @param {Element|null} [context] - An optional context to stop searching. * @returns {Element[]} */ static closest(els, selector, context) { const matched = []; for (const el of TinyHtml._preElems(els, 'closest')) { /** @type {Element | null} */ let current = el; while (current && current !== context) { if ( current.nodeType === 1 && (typeof selector === 'string' ? current.matches(selector) : current === selector) ) { matched.push(current); break; } current = current.parentElement; } } return [...new Set(matched)]; } /** * Finds the closest ancestor (including self) that matches the selector. * * @param {string|Element} selector - A selector string or DOM element to match. * @param {Element|null} [context] - An optional context to stop searching. * @returns {Element[]} */ closest(selector, context) { return TinyHtml.closest(this, selector, context); } /** * Compares two DOM elements to determine if they refer to the same node in the document. * * This performs a strict equality check (`===`) between the two elements. * * @param {TinyNode} elem - The first DOM element to compare. * @param {TinyNode} otherElem - The second DOM element to compare. * @returns {boolean} `true` if both elements are the same DOM node; otherwise, `false`. */ static isSameDom(elem, otherElem) { return ( TinyHtml._preNodeElem(elem, 'isSameDom') === TinyHtml._preNodeElem(otherElem, 'isSameDom') ); } /** * Compares two DOM elements to determine if they refer to the same node in the document. * * This performs a strict equality check (`===`) between the two elements. * * @param {TinyNode} elem - The DOM element to compare. * @returns {boolean} `true` if both elements are the same DOM node; otherwise, `false`. */ isSameDom(elem) { return TinyHtml.isSameDom(this, elem); } ////////////////////////////////////////////////////////////////// /** @type {ElementDataStore} */ _data = {}; /** * Internal data selectors for accessing public or private data stores. * * @type {Record<string, (where: string, elem: TinyElement) => ElementDataStore>} * @readonly */ static _dataSelector = { public: (where, el) => { const elem = TinyHtml._preElem(el, where); let data = __elementDataMap.get(elem); if (!data) { data = {}; __elementDataMap.set(elem, data); } return data; }, private: (where, el) => { if (!(el instanceof TinyHtml)) throw new Error(`Element must be a TinyHtml instance to execute ${where}().`); return el._data; }, }; /** * Retrieves data associated with a DOM element. * * If a `key` is provided, the corresponding value is returned. * If no `key` is given, a shallow copy of all stored data is returned. * * @param {TinyElement} el - The DOM element. * @param {string|null} [key] - The specific key to retrieve from the data store. * @param {boolean} [isPrivate=false] - Whether to access the private data store. * @returns {ElementDataStore|undefined|any} - The stored value, all data, or undefined if the key doesn't exist. */ static data(el, key, isPrivate = false) { // Get or initialize the data object const data = TinyHtml._dataSelector[!isPrivate ? 'public' : 'private']('data', el); // Getter for all if (key === undefined || key === null) return { ...data }; // Getter for specific key if (typeof key !== 'string') throw new TypeError('The key must be a string.'); return data.hasOwnProperty(key) ? data[key] : undefined; } /** * Retrieves data associated with a DOM element. * * If a `key` is provided, the corresponding value is returned. * If no `key` is given, a shallow copy of all stored data is returned. * * @param {string} [key] - The specific key to retrieve from the data store. * @param {boolean} [isPrivate=false] - Whether to access the private data store. * @returns {ElementDataStore|undefined|any} - The stored value, all data, or undefined if the key doesn't exist. */ data(key, isPrivate) { return TinyHtml.data(this, key, isPrivate); } /** * Stores a value associated with a specific key for a DOM element. * * @param {TinyElement} el - The DOM element. * @param {string} key - The key under which the data will be stored. * @param {any} va