From 6f254bf581816aec38c1f1bac4a421d98ac8d1ce Mon Sep 17 00:00:00 2001 From: Victor Westerlund Date: Wed, 11 Sep 2024 16:49:29 +0200 Subject: [PATCH] Initial code commit --- .gitignore | 16 ++++++++ .npmignore | 16 ++++++++ package.json | 13 ++++++ src/Elevent.mjs | 102 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100755 .gitignore create mode 100644 .npmignore create mode 100644 package.json create mode 100644 src/Elevent.mjs diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..1a6ac11 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Bootstrapping # +################# +vendor +.env.ini + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Icon? +ehthumbs.db +Thumbs.db +.directory diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..1a6ac11 --- /dev/null +++ b/.npmignore @@ -0,0 +1,16 @@ +# Bootstrapping # +################# +vendor +.env.ini + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +Icon? +ehthumbs.db +Thumbs.db +.directory diff --git a/package.json b/package.json new file mode 100644 index 0000000..ebd8266 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "elevent", + "version": "1.1.2", + "main": "src/Elevent.mjs", + "type": "module", + "exports": { + ".": { + "import": { + "default": "./src/Elevent.mjs" + } + } + } +} diff --git a/src/Elevent.mjs b/src/Elevent.mjs new file mode 100644 index 0000000..78c9d12 --- /dev/null +++ b/src/Elevent.mjs @@ -0,0 +1,102 @@ +export class Elevent { + _type; + _callback; + _selector; + + _boundElements = new WeakSet(); + _controller = new AbortController(); + _observer = new MutationObserver((mutations) => this.#mutationEvent(mutations)); + + /** + * Create a new Elevent instance. + * @param {string|null} eventType + * @param {HTMLCollection|HTMLElement|string|null} target Strings will be treated as CSS selectors, null will create a template instance + * @param {function} callback + */ + constructor(eventType = null, target = null, callback) { + this._type = eventType; + this._callback = callback; + + if (target) { + switch (target.constructor) { + case HTMLCollection: + this.#bindHTMLCollection(target); + break; + + case String: + this._selector = target; + this.#bindHTMLCollection(document.querySelectorAll(target)); + + // Automatically bind new DOM elements that match the provided CSS selector string + this._observer.observe(document.body, { + subtree: true, + childList: true + }); + break; + + default: + if (target instanceof HTMLElement) { + this.bind(target); + } + } + } + } + + /** + * Event handler for this._observer + * @param {array} mutations + */ + #mutationEvent(mutations) { + // Iterate over all MutationRecords + mutations.forEach(mutation => mutation.addedNodes.forEach(node => { + // Bind new element if its CSS selector matches and has not already been bound + if (node.matches(this._selector) && !this._boundElements.has(node)) { + this.bind(node); + } + })); + } + + /** + * Bind all HTMLElements inside an HTMLCollection + * @param {HTMLCollection} collection + */ + #bindHTMLCollection(collection) { + [...collection].forEach(element => this.bind(element)); + } + + /** + * Bind an element to this instance. + * @param {HTMLElement} target + */ + bind(target) { + /** + * Bind event listener of EventType on target element. + * The instance provided callback will only be executed if the target element is present in the bound elements Set. + */ + target.addEventListener(this._type, (event) => this._boundElements.has(target) && this._callback(event, this), { + signal: this._controller.signal + }); + + this._boundElements.add(target); + } + + /** + * Remove all event listeners created from this instance. + * @param {HTMLElement} [target=null] An optional HTMLElement can be provided to soft-remove a specific element. + */ + remove(target = null) { + /** + * Soft-remove a single element from the bound elements Set. + * This will still trigger the event listener but the instance provided callback will not be executed. + */ + if (target) { + this._boundElements.delete(target); + return; + } + + this._controller.abort(); + this._observer.disconnect(); + + this._boundElements = new WeakSet(); + } +} \ No newline at end of file