diff --git a/src/sigpro.js b/src/sigpro.js index 02e20f0..a45b529 100644 --- a/src/sigpro.js +++ b/src/sigpro.js @@ -7,239 +7,250 @@ const toNd = n => n?._rt ? n._cnt : (n instanceof Node ? n : txt(n)); export const fragment = p => p.children; export const val = v => isF(v) ? v() : v; -let aEff = null, aOwn = null, isFlushing = 0, bDepth = 0; -const eQ = new Set(), MOUNTED = new WeakMap(); +let curEffect = null, curOwner = null, flushing = 0, batchDepth = 0; +const effectQueue = new Set(), MOUNTED = new WeakMap(); const SVG_NS = "http://www.w3.org/2000/svg", XLINK = "http://www.w3.org/1999/xlink"; const SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(",")); -const DANG_ATTR = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]); +const DANG_ATTR = new Set(["src","href","formaction","action","background","code","archive"]); -const clr = s => { if (s) { s.forEach(f => f()); s.clear(); } }; +const clr = s => { if (s) { s.forEach(f=>f()); s.clear(); } }; const dispose = e => { - if (!e || e._x) return; - e._x = 1; - let st = [e], c; - while ((c = st.pop())) { - clr(c._c); - if (c._ch) { c._ch.forEach(x => st.push(x)); c._ch.clear(); } - if (c._d) { c._d.forEach(d => d.delete(c)); c._d.clear(); } + if (!e || e._dead) return; + e._dead = 1; + let stack = [e], c; + while ((c = stack.pop())) { + clr(c._cln); + if (c._kids) { c._kids.forEach(x=>stack.push(x)); c._kids.clear(); } + if (c._deps) { c._deps.forEach(d=>d.delete(c)); c._deps.clear(); } } }; -export const onUnmount = f => aOwn && (aOwn._c ||= new Set()).add(f); -const untrack = f => { let p = aEff; aEff = null; try { return f() } finally { aEff = p } }; +export const onUnmount = f => curOwner && ((curOwner._cln ||= new Set()).add(f)); +const untrack = f => { let p = curEffect; curEffect = null; try { return f() } finally { curEffect = p } }; -const createEffect = (f, isC = 0) => { - const e = () => { - if (e._x) return; - if (e._d) e._d.forEach(s => s.delete(e)); - clr(e._c); - let pE = aEff, pO = aOwn; - aEff = aOwn = e; - try { return e._res = f(); } +const createEffect = (fn, isComputed = 0) => { + const eff = () => { + if (eff._dead) return; + eff._deps?.forEach(s => s.delete(eff)); + clr(eff._cln); + let pE = curEffect, pO = curOwner; + curEffect = curOwner = eff; + try { return eff._res = fn(); } catch (err) { console.error("[SigPro]", err); } - finally { aEff = pE; aOwn = pO; } + finally { curEffect = pE; curOwner = pO; } }; - e._d = e._c = e._ch = null; - e._x = 0; e._iC = isC; - e._dp = aEff ? aEff._dp + 1 : 0; - e._m = []; e._p = aOwn; - if (aOwn) (aOwn._ch ||= new Set()).add(e); - return e; + eff._deps = eff._cln = eff._kids = null; + eff._dead = 0; eff._comp = isComputed; + eff._depth = curEffect ? curEffect._depth + 1 : 0; + eff._mnt = []; eff._parent = curOwner; + if (curOwner) (curOwner._kids ||= new Set()).add(eff); + return eff; }; const flush = () => { - if (isFlushing) return; - isFlushing = 1; - let q = [...eQ].sort((a, b) => a._dp - b._dp); - eQ.clear(); - for (let e of q) if (!e._x) e(); - isFlushing = 0; + if (flushing) return; + flushing = 1; + let q = [...effectQueue].sort((a,b)=>a._depth - b._depth); + effectQueue.clear(); + for (let e of q) if (!e._dead) e(); + flushing = 0; }; export const batch = f => { - bDepth++; - try { return f() } finally { if (!--bDepth && eQ.size && !isFlushing) flush() } + batchDepth++; + try { return f() } finally { if (!--batchDepth && effectQueue.size && !flushing) flush() } }; -const trkUpd = (s, trg = 0) => { - if (!trg && aEff && !aEff._x) { - s.add(aEff); - (aEff._d ||= new Set()).add(s); - } else if (trg && s.size) { +const track = (signal, trigger = 0) => { + if (!trigger && curEffect && !curEffect._dead) { + signal.add(curEffect); + (curEffect._deps ||= new Set()).add(signal); + } else if (trigger && signal.size) { let q = 0; - for (let e of s) { - if (e === aEff || e._x) continue; - if (e._iC) { e._dt = 1; if (e._sb) trkUpd(e._sb, 1); } - else { eQ.add(e); q = 1; } + for (let e of signal) { + if (e === curEffect || e._dead) continue; + if (e._comp) { e._stale = 1; e._sub && track(e._sub, 1); } + else { effectQueue.add(e); q = 1; } } - if (q && !isFlushing && !bDepth) queueMicrotask(flush); + if (q && !flushing && !batchDepth) queueMicrotask(flush); } }; -export const $ = (v, k = null) => { - let s = new Set(); - if (isF(v)) { - let c, cp = () => { - if (cp._dt) { - let p = aEff; aEff = cp; - try { let n = v(); if (!Object.is(c, n)) { c = n; trkUpd(s, 1); } } - finally { aEff = p; } - cp._dt = 0; +export const $ = (val, key = null) => { + const subs = new Set(); + if (isF(val)) { + let cached, get = () => { + if (get._stale) { + let p = curEffect; curEffect = get; + try { let n = val(); if (!Object.is(cached, n)) { cached = n; track(subs, 1); } } + finally { curEffect = p; } + get._stale = 0; } - trkUpd(s); return c; + track(subs); return cached; }; - cp._iC = cp._dt = 1; cp._sb = s; cp._d = null; cp._x = 0; - return cp; + get._comp = get._stale = 1; get._sub = subs; get._deps = null; get._dead = 0; + return get; } - if (k) try { v = JSON.parse(localStorage.getItem(k)) ?? v } catch (e) { } - return (...a) => { - if (a.length) { - let n = isF(a[0]) ? a[0](v) : a[0]; - if (!Object.is(v, n)) { v = n; if (k) localStorage.setItem(k, JSON.stringify(v)); trkUpd(s, 1); } + if (key) try { val = JSON.parse(localStorage.getItem(key)) ?? val } catch {} + return (...args) => { + if (args.length) { + let n = isF(args[0]) ? args[0](val) : args[0]; + if (!Object.is(val, n)) { val = n; if (key) localStorage.setItem(key, JSON.stringify(val)); track(subs, 1); } } - trkUpd(s); return v; + track(subs); return val; }; }; export const watch = (src, cb) => { - let e = createEffect(cb ? () => { let v = isA(src) ? src.map(s => s()) : src(); untrack(() => cb(v)) } : src); - e(); return () => dispose(e); + let eff = createEffect(cb ? () => { let v = isA(src) ? src.map(s=>s()) : src(); untrack(()=>cb(v)); } : src); + eff(); + return () => dispose(eff); }; -const clnNd = n => { +const cleanNode = n => { if (!n) return; - clr(n._c); - if (n._oE) dispose(n._oE); - if (n.childNodes) n.childNodes.forEach(clnNd); + clr(n._cln); + if (n._ownEff) dispose(n._ownEff); + n.childNodes && n.childNodes.forEach(cleanNode); }; -const valAtt = (k, v) => (v == null || v === false) ? null : +const safeAttr = (k, v) => + v == null || v === false ? null : (DANG_ATTR.has(k) || k.startsWith("on")) && /^\s*(javascript|data|vbscript):/i.test(String(v)) ? '#' : v; -export const h = (tag, prp = {}, ch = []) => { - if (prp instanceof Node || isA(prp) || !isO(prp)) { ch = prp; prp = {}; } +export const h = (tag, props = {}, children = []) => { + if (props instanceof Node || isA(props) || !isO(props)) { children = props; props = {}; } if (isF(tag)) { - let e = createEffect(() => e._res = tag(prp, { children: ch, emit: (ev, ...a) => prp[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...a) })); - e(); - if (e._res == null) return null; - let nd = e._res instanceof Node || (isA(e._res) && e._res.every(n => n instanceof Node)) ? e._res : txt(e._res); - let att = n => { if (isO(n) && !n._rt) { n._m = e._m || []; n._c = e._c || new Set(); n._oE = e; } }; - isA(nd) ? nd.forEach(att) : att(nd); - return nd; + let eff = createEffect(() => eff._res = tag(props, { children, emit: (ev,...a) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...a) })); + eff(); + if (eff._res == null) return null; + let node = eff._res instanceof Node || (isA(eff._res) && eff._res.every(n => n instanceof Node)) ? eff._res : txt(eff._res); + let mark = n => { if (isO(n) && !n._rt) { n._mnt = eff._mnt || []; n._cln = eff._cln || new Set(); n._ownEff = eff; } }; + isA(node) ? node.forEach(mark) : mark(node); + return node; } - let isS = SVG_TAGS.has(tag), el = isS ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag); - el._c = new Set(); + let isSVG = SVG_TAGS.has(tag), + el = isSVG ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag); + el._cln = new Set(); - for (let k in prp) { - let v = prp[k]; + for (let k in props) { + let v = props[k]; if (k === "ref") { isF(v) ? v(el) : (v.current = el); continue; } - if (isS && k.startsWith("xlink:")) { - let cv = valAtt(k.slice(6), v); + if (isSVG && k.startsWith("xlink:")) { + let cv = safeAttr(k.slice(6), v); cv == null ? el.removeAttributeNS(XLINK, k.slice(6)) : el.setAttributeNS(XLINK, k.slice(6), cv); continue; } if (k.startsWith("on")) { let ev = k.slice(2).toLowerCase(); el.addEventListener(ev, v); - let off = () => el.removeEventListener(ev, v); el._c.add(off); onUnmount(off); + let off = () => el.removeEventListener(ev, v); el._cln.add(off); onUnmount(off); } else if (isF(v)) { - let e = createEffect(() => { - let r = valAtt(k, v()); + let eff = createEffect(() => { + let r = safeAttr(k, v()); if (k === "class") el.className = r || ""; else if (r == null) el.removeAttribute(k); else if (k === "style" && typeof r == "string") el.setAttribute("style", r); - else if (k in el && !isS) el[k] = r; + else if (k in el && !isSVG) el[k] = r; else el.setAttribute(k, r === true ? "" : r); }); - e(); el._c.add(() => dispose(e)); onUnmount(() => dispose(e)); + eff(); el._cln.add(() => dispose(eff)); onUnmount(() => dispose(eff)); if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) { el.addEventListener(k === "checked" ? "change" : "input", ev => v(ev.target[k])); } } else { - let r = valAtt(k, v); + let r = safeAttr(k, v); if (r != null) { if (k === "style" && typeof r == "string") el.setAttribute("style", r); - else if (k in el && !isS) el[k] = r; + else if (k in el && !isSVG) el[k] = r; else el.setAttribute(k, r === true ? "" : r); } } } - const app = c => { - if (isA(c)) return c.forEach(app); + const append = c => { + if (isA(c)) return c.forEach(append); if (isF(c)) { - let anc = txt(""), cur = []; el.appendChild(anc); - let e = createEffect(() => { - let r = c(), nxt = (isA(r) ? r : [r]).map(toNd), ref = anc; - cur.forEach(n => { n._rt ? n.destroy() : clnNd(n); if (n.parentNode) n.remove(); }); - for (let i = nxt.length - 1; i >= 0; i--) { - let nd = nxt[i]; + let anchor = txt(""), cur = []; el.appendChild(anchor); + let eff = createEffect(() => { + let res = c(), next = (isA(res) ? res : [res]).map(toNd), ref = anchor; + cur.forEach(n => { n._rt ? n.destroy() : cleanNode(n); n.parentNode && n.remove(); }); + for (let i = next.length - 1; i >= 0; i--) { + let nd = next[i]; if (nd.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(nd, ref); - if (nd._m) nd._m.forEach(f => f()); ref = nd; + nd._mnt && nd._mnt.forEach(f => f()); + ref = nd; } - cur = nxt; + cur = next; }); - e(); el._c.add(() => dispose(e)); onUnmount(() => dispose(e)); + eff(); el._cln.add(() => dispose(eff)); onUnmount(() => dispose(eff)); } else { let nd = toNd(c); el.appendChild(nd); - if (nd._m) nd._m.forEach(f => f()); + nd._mnt && nd._mnt.forEach(f => f()); } }; - app(ch); return el; + append(children); + return el; }; -export const render = rFn => { - let c = new Set(), pO = aOwn, pE = aEff, cnt = doc.createElement("div"); +export const render = fn => { + let cln = new Set(), pO = curOwner, pE = curEffect, + cnt = doc.createElement("div"); cnt.style.display = "contents"; cnt.setAttribute("role", "presentation"); - aOwn = { _c: c }; aEff = null; - const pRes = r => { + curOwner = { _cln: cln }; curEffect = null; + const place = r => { if (!r) return; - if (r._rt) { c.add(r.destroy); cnt.appendChild(r._cnt); } - else if (isA(r)) r.forEach(pRes); + if (r._rt) { cln.add(r.destroy); cnt.appendChild(r._cnt); } + else if (isA(r)) r.forEach(place); else cnt.appendChild(r instanceof Node ? r : txt(r)); }; - try { pRes(rFn({ onCleanup: f => c.add(f) })); } finally { aOwn = pO; aEff = pE; } - return { _rt: 1, _cnt: cnt, destroy: () => { clr(c); clnNd(cnt); cnt.remove(); } }; + try { place(fn({ onCleanup: f => cln.add(f) })); } finally { curOwner = pO; curEffect = pE; } + return { _rt: 1, _cnt: cnt, destroy: () => { clr(cln); cleanNode(cnt); cnt.remove(); } }; }; -export const when = (c, Y, N = null) => { - let anc = txt(""), rt = h("div", { style: "display:contents" }, [anc]), v; - watch(() => !!val(c), s => { - if (v) { v.destroy(); v = null; } - let ct = s ? Y : N; - if (ct) { v = render(() => val(ct)); rt.insertBefore(v._cnt, anc); } +export const when = (cond, onTrue, onFalse = null) => { + let anchor = txt(""), root = h("div", { style: "display:contents" }, [anchor]), current; + watch(() => !!val(cond), v => { + current?.destroy(); current = null; + let template = v ? onTrue : onFalse; + if (template) { current = render(() => val(template)); root.insertBefore(current._cnt, anchor); } }); - onUnmount(() => v?.destroy()); return rt; + onUnmount(() => current?.destroy()); + return root; }; -export const each = (s, fn, kF) => { - let anc = txt(""), rt = h("div", { style: "display:contents" }, [anc]), cch = new Map(); - watch(() => val(s) || [], it => { - let nCc = new Map(), nOd = []; - for (let i = 0, l = (it || []).length; i < l; i++) { - let t = it[i], k = kF ? (t?.[kF] ?? i) : (t?.id ?? i), v = cch.get(k); - if (!v) v = render(() => fn(t, i)); else cch.delete(k); - nCc.set(k, v); nOd.push(v); +export const each = (src, mapFn, keyFn) => { + let anchor = txt(""), root = h("div", { style: "display:contents" }, [anchor]), + cache = new Map(); + watch(() => val(src) || [], items => { + let newCache = new Map(), order = []; + for (let i = 0; i < items.length; i++) { + let item = items[i], k = keyFn ? (item?.[keyFn] ?? i) : (item?.id ?? i), v = cache.get(k); + if (!v) v = render(() => mapFn(item, i)); else cache.delete(k); + newCache.set(k, v); order.push(v); } - cch.forEach(v => v.destroy()); - let ref = anc; - for (let i = nOd.length - 1; i >= 0; i--) { - let nd = nOd[i]._cnt; - if (nd.nextSibling !== ref) rt.insertBefore(nd, ref); ref = nd; + cache.forEach(v => v.destroy()); + let ref = anchor; + for (let i = order.length - 1; i >= 0; i--) { + let nd = order[i]._cnt; + if (nd.nextSibling !== ref) root.insertBefore(nd, ref); + ref = nd; } - cch = nCc; + cache = newCache; }); - return rt; + return root; }; -export const mount = (c, tgt) => { - let t = typeof tgt == "string" ? doc.querySelector(tgt) : tgt; - if (!t) return; - if (MOUNTED.has(t)) MOUNTED.get(t).destroy(); - let i = render(isF(c) ? c : () => c); - t.replaceChildren(i._cnt); MOUNTED.set(t, i); return i; +export const mount = (component, target) => { + let el = typeof target === "string" ? doc.querySelector(target) : target; + if (!el) return; + if (MOUNTED.has(el)) MOUNTED.get(el).destroy(); + let instance = render(isF(component) ? component : () => component); + el.replaceChildren(instance._cnt); + MOUNTED.set(el, instance); + return instance; }; const htmlTags = "a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video";