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.

267 lines (266 loc) 9.12 kB
/** * Callback type used for element mutation detectors. * * @callback ElementDetectorsFn * @param {MutationRecord} mutation - Single mutation record being processed. * @param {number} index - Index of the current mutation in the batch. * @param {MutationRecord[]} mutations - Full list of mutation records from the observer callback. * @returns {void} */ /** * TinyElementObserver * * A utility class for tracking DOM element mutations. * It leverages the native MutationObserver API, providing a higher-level abstraction * with a system of configurable detectors that can dispatch custom events or run custom logic. */ class TinyElementObserver { /** @type {Element|undefined} */ #el; /** * Get the current element being observed. * @returns {Element|undefined} The DOM element being tracked, or `undefined` if none is set. */ get el() { return this.#el; } /** * Set the target element to be observed. * Can only be set once. * * @param {Element|undefined} el - The DOM element to observe. * @throws {Error} If the element is already defined. * @throws {TypeError} If the provided value is not an Element. */ set el(el) { if (this.#el) throw new Error('The observed element has already been set and cannot be reassigned.'); if (typeof el !== 'undefined' && !(el instanceof Element)) throw new TypeError('The observed element must be a valid DOM Element.'); this.#el = el; } /** * Configuration settings for the MutationObserver instance. * * @type {MutationObserverInit} */ #settings = {}; /** * Get the observer settings. * @returns {MutationObserverInit} */ get settings() { return this.#settings; } /** * Set the observer settings. * @param {MutationObserverInit} settings */ set settings(settings) { if (typeof settings !== 'object' || settings === null) throw new TypeError('settings must be a non-null object.'); this.#settings = settings; } /** * Internal MutationObserver instance that tracks DOM attribute changes. * @type {MutationObserver|null} */ #observer = null; /** * Get the current MutationObserver instance. * @returns {MutationObserver|null} */ get observer() { return this.#observer; } /** * List of detectors executed on observed mutations. * Each detector is a tuple: * - name: string identifier * - handler: function processing MutationRecords * * @type {Array<[string, ElementDetectorsFn]>} */ #detectors = []; /** * Get the element detectors. * @returns {Array<[string, ElementDetectorsFn]>} */ get detectors() { return this.#detectors.map((item) => [item[0], item[1]]); } /** * Set the element detectors. * @param {Array<[string, ElementDetectorsFn]>} detectors */ set detectors(detectors) { if (!Array.isArray(detectors)) throw new TypeError('detectors must be an array.'); /** @type {Array<[string, ElementDetectorsFn]>} */ const values = []; for (const [name, fn] of detectors) { if (typeof name !== 'string') throw new TypeError('Detector name must be a string.'); if (typeof fn !== 'function') throw new TypeError(`Detector handler for "${name}" must be a function.`); values.push([name, fn]); } this.#detectors = values; } /** * Returns true if a MutationObserver is currently active. * @returns {boolean} */ get isActive() { return !!this.#observer; } /** * Get the number of registered detectors. * * @returns {number} Total count of detectors. */ get size() { return this.#detectors.length; } /** * Create a new TinyElementObserver instance. * * @param {Object} [settings={}] - Configuration object. * @param {Element} [settings.el] - Optional DOM element to observe from the start. * @param {Array<[string, ElementDetectorsFn]>} [settings.initDetectors=[]] - Optional initial detectors to register. * @param {MutationObserverInit} [settings.initCfg] - Optional MutationObserver configuration. */ constructor({ el, initDetectors = [], initCfg = {} } = {}) { this.el = el; if (initDetectors.length) this.detectors = initDetectors; if (initCfg) this.settings = initCfg; } /** * Remove all registered detectors. * After calling this, no mutation events will be processed * until new detectors are added again. */ clear() { this.#detectors = []; } /** * Start tracking DOM mutations on the defined element. * * @throws {Error} If no element has been set to observe. */ start() { if (!this.#el) throw new Error('Cannot start observation: no target element has been set.'); if (this.#observer) return; this.#observer = new MutationObserver((mutations) => { mutations.forEach((value, index, array) => this.#detectors.forEach((item) => item[1](value, index, array))); }); this.#observer.observe(this.#el, this.#settings); } /** * Stop tracking changes. */ stop() { if (!this.#observer) return; this.#observer.disconnect(); this.#observer = null; } // ================= Detectors Editor ================= /** * Add a detector to the end of the array. * @param {string} name * @param {ElementDetectorsFn} handler */ add(name, handler) { this.#validateDetector(name, handler); this.#detectors.push([name, handler]); } /** * Add a detector to the start of the array. * @param {string} name * @param {ElementDetectorsFn} handler */ insertAtStart(name, handler) { this.#validateDetector(name, handler); this.#detectors.unshift([name, handler]); } /** * Insert a detector at a specific index. * @param {number} index * @param {string} name * @param {ElementDetectorsFn} handler * @param {'before'|'after'} position - Position relative to the index */ insertAt(index, name, handler, position = 'after') { this.#validateDetector(name, handler); if (typeof index !== 'number' || index < 0 || index >= this.#detectors.length) throw new RangeError('Invalid index for insertDetectorAt.'); const insertIndex = position === 'before' ? index : index + 1; this.#detectors.splice(insertIndex, 0, [name, handler]); } /** * Remove a detector at a specific index. * @param {number} index */ removeAt(index) { if (typeof index !== 'number' || index < 0 || index >= this.#detectors.length) throw new RangeError('Invalid index for removeDetectorAt.'); this.#detectors.splice(index, 1); } /** * Remove detectors relative to a specific index. * @param {number} index - Reference index * @param {number} before - Number of items before the index to remove * @param {number} after - Number of items after the index to remove */ removeAround(index, before = 0, after = 0) { if (typeof index !== 'number' || index < 0 || index >= this.#detectors.length) throw new RangeError('Invalid index for removeDetectorsAround.'); const start = Math.max(0, index - before); const deleteCount = before + 1 + after; this.#detectors.splice(start, deleteCount); } /** * Check if a detector exists at a specific index. * @param {number} index * @returns {boolean} */ isIndexUsed(index) { return index >= 0 && index < this.#detectors.length; } /** * Check if a handler function already exists in the array. * @param {ElementDetectorsFn} handler * @returns {boolean} */ hasHandler(handler) { if (typeof handler !== 'function') throw new TypeError('Handler must be a function.'); return this.#detectors.some(([_, fn]) => fn === handler); } /** * Internal validation for detector entries. * @param {string} name * @param {ElementDetectorsFn} handler */ #validateDetector(name, handler) { if (typeof name !== 'string' || !name.trim()) throw new TypeError('Detector name must be a non-empty string.'); if (typeof handler !== 'function') throw new TypeError(`Detector handler for "${name}" must be a function.`); } /** * Completely destroy this observer instance. * Stops the MutationObserver (if active) and clears all detectors, * leaving the instance unusable until reconfigured. */ destroy() { this.stop(); this.clear(); } } export default TinyElementObserver;