mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-07-01 12:22:15 +02:00
⚡ Refact docs and examples (linting) (#22)
This commit is contained in:
@ -1,7 +1,8 @@
|
||||
import { keyLTE, evc, evd, eva } from "./dom-common.js";
|
||||
import { scope } from "./dom.js";
|
||||
import { c_ch_o } from "./events-observer.js";
|
||||
export function customElementRender(custom_element, target, render, props= observedAttributes){
|
||||
export function customElementRender(target, render, props= observedAttributes){
|
||||
const custom_element= target.host || target;
|
||||
scope.push({
|
||||
scope: custom_element,
|
||||
host: (...c)=> c.length ? c.forEach(c=> c(custom_element)) : custom_element
|
||||
|
@ -6,6 +6,7 @@ export const enviroment= {
|
||||
H: globalThis.HTMLElement,
|
||||
S: globalThis.SVGElement,
|
||||
M: globalThis.MutationObserver,
|
||||
q: p=> p || Promise.resolve(),
|
||||
};
|
||||
import { isUndef } from './helpers.js';
|
||||
function setDeleteAttr(obj, prop, val){
|
||||
@ -13,11 +14,11 @@ function setDeleteAttr(obj, prop, val){
|
||||
For some native attrs you can unset only to set empty string.
|
||||
This can be confusing as it is seen in inspector `<… id=""`.
|
||||
Options:
|
||||
1. Leave it, as it is native behaviour
|
||||
2. Sets as empty string and removes the corresponding attribute when also has empty string
|
||||
*3. Sets as undefined and removes the corresponding attribute when "undefined" string discovered
|
||||
4. Point 2. with checks for coincidence (e.g. use special string)
|
||||
*/
|
||||
1. Leave it, as it is native behaviour
|
||||
2. Sets as empty string and removes the corresponding attribute when also has empty string
|
||||
3. (*) Sets as undefined and removes the corresponding attribute when "undefined" string discovered
|
||||
4. Point 2. with checks for coincidence (e.g. use special string)
|
||||
*/
|
||||
Reflect.set(obj, prop, val);
|
||||
if(!isUndef(val)) return;
|
||||
Reflect.deleteProperty(obj, prop);
|
||||
|
128
src/dom.js
128
src/dom.js
@ -1,6 +1,8 @@
|
||||
import { signals } from "./signals-common.js";
|
||||
import { enviroment as env } from './dom-common.js';
|
||||
|
||||
//TODO: add type, docs ≡ make it public
|
||||
export function queue(promise){ return env.q(promise); }
|
||||
/** @type {{ scope: object, prevent: boolean, host: function }[]} */
|
||||
const scopes= [ {
|
||||
get scope(){ return env.D.body; },
|
||||
@ -10,13 +12,13 @@ const scopes= [ {
|
||||
export const scope= {
|
||||
get current(){ return scopes[scopes.length-1]; },
|
||||
get host(){ return this.current.host; },
|
||||
|
||||
|
||||
preventDefault(){
|
||||
const { current }= this;
|
||||
current.prevent= true;
|
||||
return current;
|
||||
},
|
||||
|
||||
|
||||
get state(){ return [ ...scopes ]; },
|
||||
push(s= {}){ return scopes.push(Object.assign({}, this.current, { prevent: false }, s)); },
|
||||
pushRoot(){ return scopes.push(scopes[0]); },
|
||||
@ -25,22 +27,25 @@ export const scope= {
|
||||
return scopes.pop();
|
||||
},
|
||||
};
|
||||
// following chainableAppend implementation is OK as the ElementPrototype.append description already is { writable: true, enumerable: true, configurable: true }
|
||||
//NOTE: following chainableAppend implementation is OK as the ElementPrototype.append description already is { writable: true, enumerable: true, configurable: true } // editorconfig-checker-disable-line
|
||||
function append(...els){ this.appendOriginal(...els); return this; }
|
||||
export function chainableAppend(el){ if(el.append===append) return el; el.appendOriginal= el.append; el.append= append; return el; }
|
||||
export function chainableAppend(el){
|
||||
if(el.append===append) return el; el.appendOriginal= el.append; el.append= append; return el;
|
||||
}
|
||||
let namespace;
|
||||
export function createElement(tag, attributes, ...addons){
|
||||
/* jshint maxcomplexity: 15 */
|
||||
const s= signals(this);
|
||||
let scoped= 0;
|
||||
let el, el_host;
|
||||
//TODO Array.isArray(tag) ⇒ set key (cache els)
|
||||
if(Object(attributes)!==attributes || s.isSignal(attributes))
|
||||
attributes= { textContent: attributes };
|
||||
switch(true){
|
||||
case typeof tag==="function": {
|
||||
scoped= 1;
|
||||
scope.push({ scope: tag, host: (...c)=> c.length ? (scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined) : el_host });
|
||||
const host= (...c)=> !c.length ? el_host :
|
||||
(scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined);
|
||||
scope.push({ scope: tag, host });
|
||||
el= tag(attributes || undefined);
|
||||
const is_fragment= el instanceof env.F;
|
||||
if(el.nodeName==="#comment") break;
|
||||
@ -65,52 +70,6 @@ export function createElement(tag, attributes, ...addons){
|
||||
scoped= 2;
|
||||
return el;
|
||||
}
|
||||
import { hasOwn } from "./helpers.js";
|
||||
/** @param {HTMLElement} element @param {HTMLElement} [root] */
|
||||
export function simulateSlots(element, root, mapper){
|
||||
if(typeof root!=="object"){
|
||||
mapper= root;
|
||||
root= element;
|
||||
}
|
||||
const _default= Symbol.for("default");
|
||||
const slots= Array.from(root.querySelectorAll("slot"))
|
||||
.reduce((out, curr)=> Reflect.set(out, curr.name || _default, curr) && out, {});
|
||||
const has_d= hasOwn(slots, _default);
|
||||
element.append= new Proxy(element.append, {
|
||||
apply(orig, _, els){
|
||||
if(els[0]===root) return orig.apply(element, els);
|
||||
if(!els.length) return element;
|
||||
|
||||
const d= env.D.createDocumentFragment();
|
||||
for(const el of els){
|
||||
if(!el || !el.slot){ if(has_d) d.append(el); continue; }
|
||||
const name= el.slot;
|
||||
const slot= slots[name];
|
||||
elementAttribute(el, "remove", "slot");
|
||||
if(!slot) continue;
|
||||
simulateSlotReplace(slot, el, mapper);
|
||||
Reflect.deleteProperty(slots, name);
|
||||
}
|
||||
if(has_d){
|
||||
slots[_default].replaceWith(d);
|
||||
Reflect.deleteProperty(slots, _default);
|
||||
}
|
||||
element.append= orig; //TODO: better memory management, but non-native behavior!
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if(element!==root){
|
||||
const els= Array.from(element.childNodes);
|
||||
els.forEach(el=> el.remove());
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
function simulateSlotReplace(slot, element, mapper){
|
||||
if(mapper) mapper(slot, element);
|
||||
try{ slot.replaceWith(assign(element, { className: [ element.className, slot.className ], dataset: { ...slot.dataset } })); }
|
||||
catch(_){ slot.replaceWith(element); }
|
||||
}
|
||||
/**
|
||||
* @param { { type: "component", name: string, host: "this" | "parentElement" } | { type: "reactive" | "later" } } attrs
|
||||
* @param {boolean} [is_open=false]
|
||||
@ -123,8 +82,7 @@ createElement.mark= function(attrs, is_open= false){
|
||||
return out;
|
||||
};
|
||||
export { createElement as el };
|
||||
|
||||
//const namespaceHelper= ns=> ns==="svg" ? "http://www.w3.org/2000/svg" : ns;
|
||||
//TODO?: const namespaceHelper= ns=> ns==="svg" ? "http://www.w3.org/2000/svg" : ns;
|
||||
export function createElementNS(ns){
|
||||
const _this= this;
|
||||
return function createElementNSCurried(...rest){
|
||||
@ -136,12 +94,46 @@ export function createElementNS(ns){
|
||||
}
|
||||
export { createElementNS as elNS };
|
||||
|
||||
/** @param {HTMLElement} element @param {HTMLElement} [root] */
|
||||
export function simulateSlots(element, root= element){
|
||||
const mark_e= "¹⁰", mark_s= "✓"; //NOTE: Markers to identify slots processed by this function. Also “prevents” native behavior as it is unlikely to use these in names. // editorconfig-checker-disable-line
|
||||
const slots= Object.fromEntries(
|
||||
Array.from(root.querySelectorAll("slot"))
|
||||
.filter(s => !s.name.endsWith(mark_e))
|
||||
.map(s => [(s.name += mark_e), s]));
|
||||
element.append= new Proxy(element.append, {
|
||||
apply(orig, _, els){
|
||||
if(els[0]===root) return orig.apply(element, els);
|
||||
for(const el of els){
|
||||
const name= (el.slot||"")+mark_e;
|
||||
try{ elementAttribute(el, "remove", "slot"); } catch(_error){}
|
||||
const slot= slots[name];
|
||||
if(!slot) return;
|
||||
if(!slot.name.startsWith(mark_s)){
|
||||
slot.childNodes.forEach(c=> c.remove());
|
||||
slot.name= mark_s+name;
|
||||
}
|
||||
slot.append(el);
|
||||
//TODO?: el.dispatchEvent(new CustomEvent("dde:slotchange", { detail: slot }));
|
||||
}
|
||||
element.append= orig; //TODO?: better memory management, but non-native behavior!
|
||||
return element;
|
||||
}
|
||||
});
|
||||
if(element!==root){
|
||||
const els= Array.from(element.childNodes);
|
||||
//TODO?: els.forEach(el=> el.remove());
|
||||
element.append(...els);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
const assign_context= new WeakMap();
|
||||
const { setDeleteAttr }= env;
|
||||
export function assign(element, ...attributes){
|
||||
if(!attributes.length) return element;
|
||||
assign_context.set(element, assignContext(element, this));
|
||||
|
||||
|
||||
for(const [ key, value ] of Object.entries(Object.assign({}, ...attributes)))
|
||||
assignAttribute.call(this, element, key, value);
|
||||
assign_context.delete(element);
|
||||
@ -150,7 +142,7 @@ export function assign(element, ...attributes){
|
||||
export function assignAttribute(element, key, value){
|
||||
const { setRemoveAttr, s }= assignContext(element, this);
|
||||
const _this= this;
|
||||
|
||||
|
||||
value= s.processReactiveAttribute(element, key, value,
|
||||
(key, value)=> assignAttribute.call(_this, element, key, value));
|
||||
const [ k ]= key;
|
||||
@ -160,11 +152,11 @@ export function assignAttribute(element, key, value){
|
||||
key= key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
||||
return setRemoveAttr(key, value);
|
||||
}
|
||||
if("className"===key) key= "class";//just optimalization, `isPropSetter` returns false immediately
|
||||
if("className"===key) key= "class";//NOTE: just optimalization, this makes `isPropSetter` returns false immediately // editorconfig-checker-disable-line
|
||||
switch(key){
|
||||
case "xlink:href":
|
||||
return setRemoveAttr(key, value, "http://www.w3.org/1999/xlink");
|
||||
case "textContent": //just optimalization, its part of Node ⇒ deep for `isPropSetter`
|
||||
case "textContent": //NOTE: just optimalization, this makes `isPropSetter` returns false immediately (as its part of Node ⇒ deep for `isPropSetter`) // editorconfig-checker-disable-line
|
||||
return setDeleteAttr(element, key, value);
|
||||
case "style":
|
||||
if(typeof value!=="object") break;
|
||||
@ -192,17 +184,13 @@ export function classListDeclarative(element, toggle){
|
||||
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;
|
||||
}
|
||||
export function elementAttribute(element, op, key, value){
|
||||
if(element instanceof env.H)
|
||||
return element[op+"Attribute"](key, value);
|
||||
return element[op+"AttributeNS"](null, key, value);
|
||||
}
|
||||
import { isUndef } from "./helpers.js";
|
||||
//TODO add cache? `(Map/Set)<el.tagName+key,isUndef>`
|
||||
//TODO: add cache? `(Map/Set)<el.tagName+key,isUndef>`
|
||||
function isPropSetter(el, key){
|
||||
if(!(key in el)) return false;
|
||||
const des= getPropDescriptor(el, key);
|
||||
@ -216,7 +204,9 @@ function getPropDescriptor(p, key){
|
||||
return des;
|
||||
}
|
||||
|
||||
/** @template {Record<any, any>} T @param {object} s @param {T} obj @param {(param: [ keyof T, T[keyof T] ])=> void} cb */
|
||||
/**
|
||||
* @template {Record<any, any>} T @param {object} s @param {T} obj @param {(param: [ keyof T, T[keyof T] ])=> void} cb
|
||||
* */
|
||||
function forEachEntries(s, obj, cb){
|
||||
if(typeof obj !== "object" || obj===null) return;
|
||||
return Object.entries(obj).forEach(function process([ key, val ]){
|
||||
@ -226,7 +216,9 @@ function forEachEntries(s, obj, cb){
|
||||
});
|
||||
}
|
||||
|
||||
function attrArrToStr(attr){ return Array.isArray(attr) ? attr.filter(Boolean).join(" ") : attr; }
|
||||
function setRemove(obj, prop, key, val){ return obj[ (isUndef(val) ? "remove" : "set") + prop ](key, attrArrToStr(val)); }
|
||||
function setRemoveNS(obj, prop, key, val, ns= null){ return obj[ (isUndef(val) ? "remove" : "set") + prop + "NS" ](ns, key, attrArrToStr(val)); }
|
||||
function setDelete(obj, key, val){ Reflect.set(obj, key, val); if(!isUndef(val)) return; return Reflect.deleteProperty(obj, key); }
|
||||
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, key, val){
|
||||
Reflect.set(obj, key, val); if(!isUndef(val)) return; return Reflect.deleteProperty(obj, key); }
|
||||
|
@ -82,12 +82,12 @@ function connectionsChangesObserverConstructor(){
|
||||
is_observing= false;
|
||||
observer.disconnect();
|
||||
}
|
||||
//TODO remount support?
|
||||
//TODO: remount support?
|
||||
function requestIdle(){ return new Promise(function(resolve){
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
}); }
|
||||
async function collectChildren(element){
|
||||
if(store.size > 30)//TODO limit?
|
||||
if(store.size > 30)//TODO?: limit
|
||||
await requestIdle();
|
||||
const out= [];
|
||||
if(!(element instanceof Node)) return out;
|
||||
@ -103,10 +103,10 @@ function connectionsChangesObserverConstructor(){
|
||||
for(const element of addedNodes){
|
||||
if(is_root) collectChildren(element).then(observerAdded);
|
||||
if(!store.has(element)) continue;
|
||||
|
||||
|
||||
const ls= store.get(element);
|
||||
if(!ls.length_c) continue;
|
||||
|
||||
|
||||
element.dispatchEvent(new Event(evc));
|
||||
ls.connected= new WeakSet();
|
||||
ls.length_c= 0;
|
||||
@ -120,7 +120,7 @@ function connectionsChangesObserverConstructor(){
|
||||
for(const element of removedNodes){
|
||||
if(is_root) collectChildren(element).then(observerRemoved);
|
||||
if(!store.has(element)) continue;
|
||||
|
||||
|
||||
const ls= store.get(element);
|
||||
if(!ls.length_d) continue;
|
||||
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
|
||||
|
@ -8,6 +8,7 @@ export function dispatchEvent(name, options, host){
|
||||
d.unshift(element);
|
||||
element= typeof host==="function"? host() : host;
|
||||
}
|
||||
//TODO: what about re-emmitting?
|
||||
const event= d.length ? new CustomEvent(name, Object.assign({ detail: d[0] }, options)) : new Event(name, options);
|
||||
return element.dispatchEvent(event);
|
||||
};
|
||||
@ -64,9 +65,9 @@ on.attributeChanged= function(listener, options){
|
||||
element.addEventListener(eva, listener, options);
|
||||
if(element[keyLTE] || els_attribute_store.has(element))
|
||||
return element;
|
||||
|
||||
|
||||
if(!env.M) return element;
|
||||
|
||||
|
||||
const observer= new env.M(function(mutations){
|
||||
for(const { attributeName, target } of mutations)
|
||||
target.dispatchEvent(
|
||||
@ -77,4 +78,4 @@ on.attributeChanged= function(listener, options){
|
||||
//TODO: clean up when element disconnected
|
||||
return element;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -21,7 +21,7 @@ export function signal(value, actions){
|
||||
if(typeof value!=="function")
|
||||
return create(false, value, actions);
|
||||
if(isSignal(value)) return value;
|
||||
|
||||
|
||||
const out= create(true);
|
||||
const contextReWatch= function(){
|
||||
const [ origin, ...deps_old ]= deps.get(contextReWatch);
|
||||
@ -58,7 +58,7 @@ signal.on= function on(s, listener, options= {}){
|
||||
if(Array.isArray(s)) return s.forEach(s=> on(s, listener, options));
|
||||
addSignalListener(s, listener);
|
||||
if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener));
|
||||
//TODO cleanup when signal removed
|
||||
//TODO: cleanup when signal removed
|
||||
};
|
||||
signal.symbols= {
|
||||
//signal: mark,
|
||||
@ -77,11 +77,11 @@ signal.clear= function(...signals){
|
||||
o.listeners.forEach(l=> {
|
||||
o.listeners.delete(l);
|
||||
if(!deps.has(l)) return;
|
||||
|
||||
|
||||
const ls= deps.get(l);
|
||||
ls.delete(s);
|
||||
if(ls.size>1) return;
|
||||
|
||||
|
||||
s.clear(...ls);
|
||||
deps.delete(l);
|
||||
});
|
||||
|
@ -21,7 +21,7 @@ export function signal(value, actions){
|
||||
if(typeof value!=="function")
|
||||
return create(false, value, actions);
|
||||
if(isSignal(value)) return value;
|
||||
|
||||
|
||||
const out= create(true);
|
||||
const contextReWatch= function(){
|
||||
const [ origin, ...deps_old ]= deps.get(contextReWatch);
|
||||
@ -58,7 +58,7 @@ signal.on= function on(s, listener, options= {}){
|
||||
if(Array.isArray(s)) return s.forEach(s=> on(s, listener, options));
|
||||
addSignalListener(s, listener);
|
||||
if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener));
|
||||
//TODO cleanup when signal removed
|
||||
//TODO: cleanup when signal removed
|
||||
};
|
||||
signal.symbols= {
|
||||
//signal: mark,
|
||||
@ -77,11 +77,11 @@ signal.clear= function(...signals){
|
||||
o.listeners.forEach(l=> {
|
||||
o.listeners.delete(l);
|
||||
if(!deps.has(l)) return;
|
||||
|
||||
|
||||
const ls= deps.get(l);
|
||||
ls.delete(s);
|
||||
if(ls.size>1) return;
|
||||
|
||||
|
||||
s.clear(...ls);
|
||||
deps.delete(l);
|
||||
});
|
||||
|
Reference in New Issue
Block a user