From 7a17fa8e35117a75bb34535da8bbfed1113193c3 Mon Sep 17 00:00:00 2001 From: Jan Andrle Date: Tue, 22 Aug 2023 16:30:03 +0200 Subject: [PATCH] :tada: --- index.d.ts | 79 +++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 75 ++++++++++++++++++++++++++++++++++++++++++++++ test/index.html | 16 ++++++++++ test/index.js | 17 +++++++++++ 4 files changed, 187 insertions(+) create mode 100644 index.d.ts create mode 100644 index.js create mode 100644 test/index.html create mode 100644 test/index.js diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..7f373fd --- /dev/null +++ b/index.d.ts @@ -0,0 +1,79 @@ +/** + * @private + */ +type T_DOM_HETNM= HTMLElementTagNameMap & SVGElementTagNameMap & { + '<>': DocumentFragment, + '': HTMLElement, + 'zzz_text': Text +} +/** + * @private + */ +type T_DOM_ATTRS_MODIFIED= { + /** + * In fact argumen for `*.setAttribute("style", *)`. + */ + style: string + /** + * Provide option to add/remove/toggle CSS clasess (index of object) using 1/0/-1. In fact `el.classList.toggle(class_name)` for `-1` and `el.classList.toggle(class_name, Boolean(...))` for others. + */ + classList: Record +} +/** + * Just element attributtes + * + * In most cases, you can use native propertie such as [MDN WEB/API/Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) and so on (e.g. [`Text`](https://developer.mozilla.org/en-US/docs/Web/API/Text)). + * + * There is added support for `data[A-Z].*`/`aria[A-Z].*` to be converted to the kebab-case alternatives. + * @private + */ +type T_DOM_ATTRS= + T extends keyof T_DOM_HETNM ? + Omit & T_DOM_ATTRS_MODIFIED : + Omit & T_DOM_ATTRS_MODIFIED; +/** + * Procedure for merging object into the element properties. + * Very simple example: `$dom.assign(document.body, { className: "test" });` is equivalent to `document.body.className= "test";`. + * It is not deep copy in general, but it supports `style`, `style_vars` and `dataset` objects (see below). + * + * **#1: All together** + * ```javascript + * const el= document.body; + * const onclick= function(){ console.log(this.dataset.js_param); }; + * $dom.assign(el, { textContent: "BODY", style: "color: red;", dataset: { js_param: "CLICKED" }, onclick }); + * //result HTML: BODY + * //console output on click: "CLICKED" + * $dom.assign(el, { style: { color: "green" } }); + * //result HTML: BODY + * //console output on click: "CLICKED" + * ``` + * + * **`\*.classList.toggle`** + * ```javascript + * const el= document.body; + * $dom.assign(el, { classList: { testClass: -1 } }); + * //result HTML: … + * $dom.assign(el, { classList: { testClass: -1 } }); + * //result HTML: … + * + * $dom.assign(el, { classList: { testClass: true } });//or 1 + * //result HTML: … + * $dom.assign(el, { classList: { testClass: false } });//or 0 + * //result HTML: … + * //... + * ``` + * + * **#3 Links and images** + * ```javascript + * $dom.assign(A_ELEMENT, { href: "www.google.com" });//=> + * ``` + * @category Public + * @version 2.0.0 + */ +export function assign(element: EL, ...attrs_array: T_DOM_ATTRS[]): EL + +export function el(tag_name: TAG, attrs: T_DOM_ATTRS): T_DOM_HETNM[TAG] diff --git a/index.js b/index.js new file mode 100644 index 0000000..39efeaa --- /dev/null +++ b/index.js @@ -0,0 +1,75 @@ +[ HTMLElement, DocumentFragment ].forEach(c=> { + const { append }= c.prototype; + c.prototype.append= function(...els){ append.apply(this, els); return this; }; +}); + +export function createElement(tag, attributes){ + if(tag==="<>") return document.createDocumentFragment(); + if(tag==="") return document.createTextNode(attributes.textContent ?? attributes.innerText ?? attributes.innerHTML); + return assign(document.createElement(tag), attributes); +} +export { createElement as el }; +export function createElementNS(tag, attributes, attributes_todo){ + let namespace= "svg"; + if(typeof attributes_todo !== "undefined"){ + namespace= tag; tag= attributes; attributes= attributes_todo; } + if(tag==="<>") return document.createDocumentFragment(); + if(tag==="") return document.createTextNode(attributes.textContent ?? attributes.innerText ?? attributes.innerHTML); + return assign(document.createElementNS(namespace==="svg" ? "http://www.w3.org/2000/svg" : namespace, tag), attributes); +} +export { createElementNS as elNS }; + +export function assign(element, ...attributes){ + // prefers https://developer.mozilla.org/en-US/docs/Glossary/IDL + if(!attributes.length) return element; + const is_svg= element instanceof SVGElement; + const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute"); + + Object.entries(Object.assign({}, ...attributes)).forEach(function([ key, attr ]){ + if(key[0]==="=") return setRemoveAttr(key.slice(1), attr); + if(key[0]===".") return setDelete(element, key.slice(1), attr); + if(typeof attr === "object"){ + switch(key){ + case "style": return forEachEntries(attr, setRemove.bind(null, element.style, "Property")) + case "dataset": return forEachEntries(attr, setDelete.bind(null, element.dataset)); + case "ariaset": return forEachEntries(attr, (key, val)=> setRemoveAttr("aria-"+key, val)); + case "classList": return classListDeclartive(element, attr); + default: return Reflect.set(element, key, attr); + } + } + if(/(aria|data)([A-Z])/.test(key)){ + key= key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); + return setRemoveAttr(key, attr); + } + switch(key){ + case "href" || "src" || "style": + return setRemoveAttr(key, attr); + case "xlink:href": + return setRemoveAttr(key, attr, "http://www.w3.org/1999/xlink"); + case "textContent" || "innerText": + if(!is_svg) break; + return element.appendChild(document.createTextNode(attr)); + } + if(key in element && !is_svg) + return setDelete(element, key, attr); + return setRemoveAttr(key, attr); + }); + return element; +} +export function classListDeclartive(element, toggle){ + if(typeof toggle !== "object") return element; + + forEachEntries(toggle, + (class_name, val)=> + element.classList.toggle(class_name, val===-1 ? undefined : Boolean(val))) + return element; +} + +export function empty(el){ Array.from(el.children).forEach(el=> el.remove()); return el; } + +function forEachEntries(obj, cb){ return Object.entries(obj).forEach(([ key, val ])=> cb(key, val)); } +function isUndef(value){ return typeof value==="undefined"; } + +function setRemove(obj, prop, key, val){ return obj[ (isUndef(val) ? "remove" : "set") + prop ](key, val); } +function setRemoveNS(obj, prop, key, val, ns= null){ return obj[ (isUndef(val) ? "remove" : "set") + prop + "NS" ](ns, key, val); } +function setDelete(obj, prop, val){ return Reflect[ isUndef(val) ? "deleteProperty" : "set" ](obj, prop, val); } diff --git a/test/index.html b/test/index.html new file mode 100644 index 0000000..87046fe --- /dev/null +++ b/test/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + Small DOM element creation enhancements + + + + + + diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..fa85fdb --- /dev/null +++ b/test/index.js @@ -0,0 +1,17 @@ +import { el, elNS, assign } from "../index.js"; +Object.assign(globalThis, { el, elNS, assign }); + +console.log(el("p", { className: "red", textContent: "Hello "})); +console.log(el("p", { className: "red", textContent: "Hello "}) instanceof HTMLParagraphElement); + +document.head.append( + el("style", { textContent: ` + .red{ color: red; } + ` }) +) +document.body.append( + el("p", { className: "red" }).append( + el("", { textContent: "Hello " }), + el("strong", { textContent: "World" }) + ) +);