Files
sigpro/sigpro3.js
2026-04-08 22:01:24 +02:00

139 lines
5.1 KiB
JavaScript

const SigPro = (() => {
const doc = typeof document !== "undefined" ? document : null;
const isFunc = (f) => typeof f === "function";
const isArr = Array.isArray;
let activeEffect = null, currentOwner = null;
const queue = new Set();
const tick = () => {
const runs = [...queue]; queue.clear();
runs.forEach(fn => fn());
if (queue.size) tick();
};
const onUnmount = (fn) => currentOwner?._cleanups.add(fn);
const cleanupNode = (node) => {
if (node._cleanups) { node._cleanups.forEach(f => f()); node._cleanups.clear(); }
node.childNodes?.forEach(cleanupNode);
};
const effect = (fn) => {
const runner = () => {
stop();
const prevEff = activeEffect, prevOwn = currentOwner;
activeEffect = runner; currentOwner = runner;
try { runner._cb = fn(); } finally { activeEffect = prevEff; currentOwner = prevOwn; }
};
const stop = () => {
runner._deps?.forEach(subs => subs.delete(runner)); runner._deps?.clear();
if (isFunc(runner._cb)) runner._cb();
runner._cleanups?.forEach(f => f()); runner._cleanups?.clear();
};
Object.assign(runner, { _deps: new Set(), _cleanups: new Set(), stop });
if (currentOwner) onUnmount(stop);
runner(); return stop;
};
const signal = (val) => {
const subs = new Set();
return (...args) => {
if (args.length) {
const next = isFunc(args[0]) ? args[0](val) : args[0];
if (!Object.is(val, next)) {
val = next;
subs.forEach(e => { queue.add(e); if (queue.size === 1) queueMicrotask(tick); });
}
} else {
if (activeEffect) { subs.add(activeEffect); activeEffect._deps.add(subs); }
return val;
}
};
};
const computed = (fn) => {
const s = signal();
effect(() => s(fn()));
return () => s();
};
const h = (tag, props = {}, children = []) => {
if (props instanceof Node || isArr(props) || typeof props !== 'object') { children = props; props = {}; }
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
const el = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : doc.createElement(tag);
el._cleanups = new Set();
for (let [k, v] of Object.entries(props)) {
if (k === "ref") { isFunc(v) ? v(el) : (v.current = el); continue; }
if (k.startsWith("on")) {
const ev = k.slice(2).toLowerCase(); el.addEventListener(ev, v);
el._cleanups.add(() => el.removeEventListener(ev, v));
} else if (isFunc(v)) {
el._cleanups.add(effect(() => {
const val = v(), safe = (k === 'src' || k === 'href') && String(val).includes('javascript:') ? '#' : val;
k === "class" ? (el.className = safe || "") : (safe == null || safe === false ? el.removeAttribute(k) : el.setAttribute(k, safe === true ? "" : safe));
}));
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
el.addEventListener(k === "checked" ? "change" : "input", (e) => v(e.target[k === "checked" ? "checked" : "value"]));
}
} else el.setAttribute(k, v);
}
const ensureNode = (n) => n instanceof Node ? n : doc.createTextNode(String(n ?? ""));
const append = (c) => {
if (isArr(c)) return c.forEach(append);
if (isFunc(c)) {
const m = doc.createTextNode(""); el.appendChild(m); let curr = [];
el._cleanups.add(effect(() => {
const res = c(), next = (isArr(res) ? res : [res]).flat().map(ensureNode);
curr.forEach(n => { cleanupNode(n); n.remove(); });
next.forEach(n => m.parentNode?.insertBefore(n, m)); curr = next;
}));
} else if (c !== null && c !== false) el.appendChild(ensureNode(c));
};
append(children);
if (currentOwner) onUnmount(() => cleanupNode(el));
return el;
};
const If = (cond, thenFn, elseFn = null) => {
let last, cached;
return () => {
const v = !!(isFunc(cond) ? cond() : cond);
if (v === last) return cached;
last = v;
const target = v ? thenFn : elseFn;
return cached = isFunc(target) ? target() : target;
};
};
const For = (list, keyFn, itemFn) => {
let cache = new Map();
return () => {
const items = (isFunc(list) ? list() : list) || [];
const nextCache = new Map();
const nodes = items.map((item, i) => {
const key = keyFn(item, i);
const node = cache.get(key) || itemFn(item, i);
nextCache.set(key, node); return node;
});
for (const [k, n] of cache) if (!nextCache.has(k)) { cleanupNode(n); n.remove(); }
cache = nextCache; return nodes;
};
};
const mount = (comp, target) => {
const t = typeof target === "string" ? doc.querySelector(target) : target;
cleanupNode(t);
const prev = currentOwner, _cleanups = new Set();
currentOwner = { _cleanups };
const node = h(comp);
currentOwner = prev;
node._cleanups = new Set([...(node._cleanups || []), ..._cleanups]);
t.replaceChildren(node);
return () => cleanupNode(node);
};
return { signal, computed, effect, h, If, For, mount, onUnmount };
})();