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.

293 lines (261 loc) 8.45 kB
'use strict'; /** * 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(); } } module.exports = TinyElementObserver;