diff --git a/javascript/js-extension/README.md b/javascript/js-extension/README.md new file mode 100644 index 0000000..e11fe44 --- /dev/null +++ b/javascript/js-extension/README.md @@ -0,0 +1,96 @@ +# js-extension (JavaScript) + +A tiny, dependency‑free utility class with a few DOM and string helpers that I often reach for in small projects and prototypes. + +It includes: +- `addGlobalEventListener` — delegated events with a CSS selector +- `createElement` — quick DOM element creation with attributes, class, dataset, and text +- `qs` / `qsa` — short aliases for `querySelector` / `querySelectorAll` with optional parent +- `strToHtml` — convert an HTML string to a `DocumentFragment` +- `isJson` — quick check if a string parses as JSON +- `sleep` — simple delay (`Promise`-based) + +## Quick usage + +Minimal examples showing what the helpers do. Adjust to your environment as needed. + +```js +// Assume jsExtension is available in scope + +// 1) Delegated event listener +jsExtension.addGlobalEventListener("click", "[data-action=remove]", (e) => { + const btn = e.target.closest("[data-action=remove]"); + btn?.closest(".item")?.remove(); +}); + +// 2) Create element with attributes, class, dataset, and text +const card = jsExtension.createElement("div", { + class: "card", + id: "card-1", + dataset: { id: "1", type: "example" }, + "aria-label": "Example card", + text: "Hello!", +}); + +// 3) Shorthand query helpers +const firstItem = jsExtension.qs(".item"); +const allItems = jsExtension.qsa(".item"); + +// 4) String to HTML fragment +const frag = jsExtension.strToHtml(` + +`); +document.body.appendChild(frag); + +// 5) JSON check +jsExtension.isJson('{"a":1}'); // true +jsExtension.isJson('{a:1}'); // false + +// 6) Sleep helper +await jsExtension.sleep(300); +``` + +## API + +### `addGlobalEventListener(type, selector, callback, options?, parent=document)` +Adds a single delegated listener on `parent` and runs `callback` when the event target matches `selector` (or is inside an element that matches). + +- `type`: string event type, e.g. `"click"` +- `selector`: CSS selector to match or `closest()` to +- `callback`: function receiving the event +- `options`: optional `addEventListener` options +- `parent`: `HTMLElement | Document` (defaults to `document`) + +### `createElement(type, options={}) => HTMLElement` +Creates an element and applies the provided options. +Supported option keys: +- `class`: string — added via `classList.add` +- `dataset`: object — assigned to `element.dataset[key] = value` +- `text`: string — sets `textContent` +- any other key — assigned as attribute: `setAttribute(key, value)` + +### `qs(selector, parent=document) => Element|null` +Shorthand for `parent.querySelector(selector)`. Returns `null` if `parent` is not a `Document`/`HTMLElement`. + +### `qsa(selector, parent=document) => NodeList` +Shorthand for `parent.querySelectorAll(selector)`. Falls back to `document.querySelectorAll` when `parent` is not a `Document`/`HTMLElement`. + +### `strToHtml(str) => DocumentFragment` +Converts an HTML string into a `DocumentFragment` using `Range#createContextualFragment`. + +### `isJson(str) => boolean` +Returns `true` if `JSON.parse(str)` succeeds; otherwise `false`. + +### `sleep(time) => Promise` +Resolves after `time` milliseconds using `setTimeout`. + +## Notes +- DOM helpers assume a browser environment (i.e., `document` is available). +- `addGlobalEventListener` uses event delegation; attach it once at a suitable ancestor. + +## License + +See the repository-level `LICENSE` file. diff --git a/javascript/js-extension/js-extension.js b/javascript/js-extension/js-extension.js new file mode 100644 index 0000000..be5a209 --- /dev/null +++ b/javascript/js-extension/js-extension.js @@ -0,0 +1,102 @@ +export default class jsExtension { + /** + * @param {string} type + * @param {string} selector + * @param {Function} callback + * @param options + * @param {HTMLElement|Document} parent + */ + static addGlobalEventListener(type, selector, callback, options, parent = document) { + parent.addEventListener(type, (eventListener) => { + const target = eventListener.target; + + if (!(target instanceof Element)) return; + + if (target.matches(selector) || target.closest(selector)) { + callback(eventListener); + } + }, options); + } + + /** + * @param {string} type + * @param {Object} options + * @returns {HTMLElement} + */ + static createElement(type, options = {}) { + const htmlElement = document.createElement(type); + + for(const [_key, _value] of /** @type {[string, string][]} */Object.entries(options)) { + + if(_key === "class") { + htmlElement.classList.add(_value); + break; + } + + if(_key === "dataset") { + Object.entries(_value).forEach(([dataKey, dataValue]) => { + htmlElement.dataset[dataKey] = dataValue; + }); + break; + } + + if(_key === "text") { + htmlElement.textContent = _value; + break; + } + + htmlElement.setAttribute(_key, _value); + } + + return htmlElement; + } + + /** + * @param {string} selector + * @param {HTMLElement|Document} parent + * @returns {Element|null} + */ + static qs(selector, parent = document) { + return parent instanceof Document || parent instanceof HTMLElement + ? parent.querySelector(selector) + : null; + } + + /** + * @param {string} selector + * @param {HTMLElement|Document} parent + * @returns {NodeList} + */ + static qsa(selector, parent = document) { + return parent instanceof Document || parent instanceof HTMLElement + ? parent.querySelectorAll(selector) + : document.querySelectorAll(selector); + } + + /** + * @param {string} str + * @return {DocumentFragment} + */ + static strToHtml(str) { + return document.createRange().createContextualFragment(str.trim()); + } + + /** + * @param str + * @returns {boolean} + */ + static isJson(str) { + try { JSON.parse(str); } catch(e) { return false; } + + return true; + } + + /** + * @param {number} time + * @returns {Promise} + */ + static sleep(time) { + return new Promise(resolve => setTimeout(resolve, time)); + } +} +