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
JavaScript
'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