Update to new 1.2.0
This commit is contained in:
809
dist/sigpro.esm.js
vendored
809
dist/sigpro.esm.js
vendored
@@ -1,73 +1,399 @@
|
|||||||
// sigpro.js
|
// sigpro.js
|
||||||
var activeEffect = null;
|
|
||||||
var currentOwner = null;
|
|
||||||
var effectQueue = new Set;
|
|
||||||
var isFlushing = false;
|
|
||||||
var MOUNTED_NODES = new WeakMap;
|
|
||||||
var doc = document;
|
|
||||||
var isArr = Array.isArray;
|
|
||||||
var assign = Object.assign;
|
|
||||||
var createEl = (t) => doc.createElement(t);
|
|
||||||
var createText = (t) => doc.createTextNode(String(t ?? ""));
|
|
||||||
var isFunc = (f) => typeof f === "function";
|
var isFunc = (f) => typeof f === "function";
|
||||||
var isObj = (o) => typeof o === "object" && o !== null;
|
var isObj = (o) => o && typeof o === "object";
|
||||||
var runWithContext = (effect, callback) => {
|
var isArr = Array.isArray;
|
||||||
const previousEffect = activeEffect;
|
var doc = typeof document !== "undefined" ? document : null;
|
||||||
activeEffect = effect;
|
var ensureNode = (n) => n?._isRuntime ? n.container : n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n));
|
||||||
|
var activeEffect = null;
|
||||||
|
var activeOwner = null;
|
||||||
|
var isFlushing = false;
|
||||||
|
var batchDepth = 0;
|
||||||
|
var effectQueue = new Set;
|
||||||
|
var proxyCache = new WeakMap;
|
||||||
|
var ITER = Symbol("iter");
|
||||||
|
var MOUNTED_NODES = new WeakMap;
|
||||||
|
var dispose = (eff) => {
|
||||||
|
if (!eff || eff._disposed)
|
||||||
|
return;
|
||||||
|
eff._disposed = true;
|
||||||
|
const stack = [eff];
|
||||||
|
while (stack.length) {
|
||||||
|
const e = stack.pop();
|
||||||
|
if (e._cleanups) {
|
||||||
|
e._cleanups.forEach((fn) => fn());
|
||||||
|
e._cleanups.clear();
|
||||||
|
}
|
||||||
|
if (e._children) {
|
||||||
|
e._children.forEach((child) => stack.push(child));
|
||||||
|
e._children.clear();
|
||||||
|
}
|
||||||
|
if (e._deps) {
|
||||||
|
e._deps.forEach((depSet) => depSet.delete(e));
|
||||||
|
e._deps.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var onMount = (fn) => {
|
||||||
|
if (activeOwner)
|
||||||
|
(activeOwner._mounts ||= []).push(fn);
|
||||||
|
};
|
||||||
|
var onUnmount = (fn) => {
|
||||||
|
if (activeOwner)
|
||||||
|
(activeOwner._cleanups ||= new Set).add(fn);
|
||||||
|
};
|
||||||
|
var untrack = (fn) => {
|
||||||
|
const p = activeEffect;
|
||||||
|
activeEffect = null;
|
||||||
try {
|
try {
|
||||||
return callback();
|
return fn();
|
||||||
} finally {
|
} finally {
|
||||||
activeEffect = previousEffect;
|
activeEffect = p;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var cleanupNode = (node) => {
|
var createEffect = (fn, isComputed = false) => {
|
||||||
if (node._cleanups) {
|
const effect = () => {
|
||||||
node._cleanups.forEach((dispose) => dispose());
|
if (effect._disposed)
|
||||||
node._cleanups.clear();
|
return;
|
||||||
}
|
if (effect._deps)
|
||||||
node.childNodes?.forEach(cleanupNode);
|
effect._deps.forEach((s) => s.delete(effect));
|
||||||
|
if (effect._cleanups) {
|
||||||
|
effect._cleanups.forEach((c) => c());
|
||||||
|
effect._cleanups.clear();
|
||||||
|
}
|
||||||
|
const prevEffect = activeEffect;
|
||||||
|
const prevOwner = activeOwner;
|
||||||
|
activeEffect = activeOwner = effect;
|
||||||
|
try {
|
||||||
|
return effect._result = fn();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[SigPro]", e);
|
||||||
|
} finally {
|
||||||
|
activeEffect = prevEffect;
|
||||||
|
activeOwner = prevOwner;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
effect._deps = effect._cleanups = effect._children = null;
|
||||||
|
effect._disposed = false;
|
||||||
|
effect._isComputed = isComputed;
|
||||||
|
effect._depth = activeEffect ? activeEffect._depth + 1 : 0;
|
||||||
|
effect._mounts = [];
|
||||||
|
effect._parent = activeOwner;
|
||||||
|
if (activeOwner)
|
||||||
|
(activeOwner._children ||= new Set).add(effect);
|
||||||
|
return effect;
|
||||||
};
|
};
|
||||||
var flushEffects = () => {
|
var flush = () => {
|
||||||
if (isFlushing)
|
if (isFlushing)
|
||||||
return;
|
return;
|
||||||
isFlushing = true;
|
isFlushing = true;
|
||||||
while (effectQueue.size > 0) {
|
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth);
|
||||||
const sortedEffects = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
|
effectQueue.clear();
|
||||||
effectQueue.clear();
|
for (const e of sorted)
|
||||||
for (const effect of sortedEffects) {
|
if (!e._disposed)
|
||||||
if (!effect._deleted)
|
e();
|
||||||
effect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isFlushing = false;
|
isFlushing = false;
|
||||||
};
|
};
|
||||||
var trackSubscription = (subscribers) => {
|
var Batch = (fn) => {
|
||||||
if (activeEffect && !activeEffect._deleted) {
|
batchDepth++;
|
||||||
subscribers.add(activeEffect);
|
try {
|
||||||
activeEffect._deps.add(subscribers);
|
return fn();
|
||||||
|
} finally {
|
||||||
|
batchDepth--;
|
||||||
|
if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) {
|
||||||
|
flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var triggerUpdate = (subscribers) => {
|
var trackUpdate = (subs, trigger = false) => {
|
||||||
subscribers.forEach((effect) => {
|
if (!trigger && activeEffect && !activeEffect._disposed) {
|
||||||
if (effect === activeEffect || effect._deleted)
|
subs.add(activeEffect);
|
||||||
return;
|
(activeEffect._deps ||= new Set).add(subs);
|
||||||
if (effect._isComputed) {
|
} else if (trigger) {
|
||||||
effect.markDirty();
|
let hasQueue = false;
|
||||||
if (effect._subs)
|
subs.forEach((e) => {
|
||||||
triggerUpdate(effect._subs);
|
if (e === activeEffect || e._disposed)
|
||||||
} else {
|
return;
|
||||||
effectQueue.add(effect);
|
if (e._isComputed) {
|
||||||
|
e._dirty = true;
|
||||||
|
if (e._subs)
|
||||||
|
trackUpdate(e._subs, true);
|
||||||
|
} else {
|
||||||
|
effectQueue.add(e);
|
||||||
|
hasQueue = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (hasQueue && !isFlushing && batchDepth === 0)
|
||||||
|
queueMicrotask(flush);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var $ = (val, key = null) => {
|
||||||
|
const subs = new Set;
|
||||||
|
if (isFunc(val)) {
|
||||||
|
let cache, dirty = true;
|
||||||
|
const computed = () => {
|
||||||
|
if (dirty) {
|
||||||
|
const prev = activeEffect;
|
||||||
|
activeEffect = computed;
|
||||||
|
try {
|
||||||
|
const next = val();
|
||||||
|
if (!Object.is(cache, next)) {
|
||||||
|
cache = next;
|
||||||
|
dirty = false;
|
||||||
|
trackUpdate(subs, true);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
activeEffect = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackUpdate(subs);
|
||||||
|
return cache;
|
||||||
|
};
|
||||||
|
computed._isComputed = true;
|
||||||
|
computed._subs = subs;
|
||||||
|
computed._dirty = true;
|
||||||
|
computed._deps = null;
|
||||||
|
computed._disposed = false;
|
||||||
|
computed.markDirty = () => {
|
||||||
|
dirty = true;
|
||||||
|
};
|
||||||
|
computed.stop = () => {
|
||||||
|
computed._disposed = true;
|
||||||
|
if (computed._deps) {
|
||||||
|
computed._deps.forEach((depSet) => depSet.delete(computed));
|
||||||
|
computed._deps.clear();
|
||||||
|
}
|
||||||
|
subs.clear();
|
||||||
|
};
|
||||||
|
if (activeOwner)
|
||||||
|
onUnmount(computed.stop);
|
||||||
|
return computed;
|
||||||
|
}
|
||||||
|
if (key)
|
||||||
|
try {
|
||||||
|
val = JSON.parse(localStorage.getItem(key)) ?? val;
|
||||||
|
} catch (e) {}
|
||||||
|
return (...args) => {
|
||||||
|
if (args.length) {
|
||||||
|
const next = isFunc(args[0]) ? args[0](val) : args[0];
|
||||||
|
if (!Object.is(val, next)) {
|
||||||
|
val = next;
|
||||||
|
if (key)
|
||||||
|
localStorage.setItem(key, JSON.stringify(val));
|
||||||
|
trackUpdate(subs, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackUpdate(subs);
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var $$ = (target) => {
|
||||||
|
if (!isObj(target))
|
||||||
|
return target;
|
||||||
|
if (proxyCache.has(target))
|
||||||
|
return proxyCache.get(target);
|
||||||
|
const subsMap = new Map;
|
||||||
|
const getSubs = (k) => {
|
||||||
|
let s = subsMap.get(k);
|
||||||
|
if (!s)
|
||||||
|
subsMap.set(k, s = new Set);
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
const proxy = new Proxy(target, {
|
||||||
|
get(t, k) {
|
||||||
|
trackUpdate(getSubs(k));
|
||||||
|
return $$(t[k]);
|
||||||
|
},
|
||||||
|
set(t, k, v) {
|
||||||
|
const isNew = !(k in t);
|
||||||
|
if (!Object.is(t[k], v)) {
|
||||||
|
t[k] = v;
|
||||||
|
trackUpdate(getSubs(k), true);
|
||||||
|
if (isNew)
|
||||||
|
trackUpdate(getSubs(ITER), true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deleteProperty(t, k) {
|
||||||
|
const res = Reflect.deleteProperty(t, k);
|
||||||
|
if (res) {
|
||||||
|
trackUpdate(getSubs(k), true);
|
||||||
|
trackUpdate(getSubs(ITER), true);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
ownKeys(t) {
|
||||||
|
trackUpdate(getSubs(ITER));
|
||||||
|
return Reflect.ownKeys(t);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!isFlushing)
|
proxyCache.set(target, proxy);
|
||||||
queueMicrotask(flushEffects);
|
return proxy;
|
||||||
|
};
|
||||||
|
var Watch = (sources, cb) => {
|
||||||
|
if (cb === undefined) {
|
||||||
|
const effect2 = createEffect(sources);
|
||||||
|
effect2();
|
||||||
|
return () => dispose(effect2);
|
||||||
|
}
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const vals = Array.isArray(sources) ? sources.map((s) => s()) : sources();
|
||||||
|
untrack(() => cb(vals));
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
return () => dispose(effect);
|
||||||
|
};
|
||||||
|
var cleanupNode = (node) => {
|
||||||
|
if (node._cleanups) {
|
||||||
|
node._cleanups.forEach((fn) => fn());
|
||||||
|
node._cleanups.clear();
|
||||||
|
}
|
||||||
|
if (node._ownerEffect)
|
||||||
|
dispose(node._ownerEffect);
|
||||||
|
if (node.childNodes)
|
||||||
|
node.childNodes.forEach(cleanupNode);
|
||||||
|
};
|
||||||
|
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||||
|
var isDangerousAttr = (key) => key === "src" || key === "href" || key.startsWith("on");
|
||||||
|
var validateAttr = (key, val) => {
|
||||||
|
if (val == null || val === false)
|
||||||
|
return null;
|
||||||
|
if (isDangerousAttr(key)) {
|
||||||
|
const sVal = String(val);
|
||||||
|
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
||||||
|
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`);
|
||||||
|
return "#";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
var Tag = (tag, props = {}, children = []) => {
|
||||||
|
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||||
|
children = props;
|
||||||
|
props = {};
|
||||||
|
}
|
||||||
|
if (isFunc(tag)) {
|
||||||
|
const ctx = { _mounts: [], _cleanups: new Set };
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const result2 = tag(props, {
|
||||||
|
children,
|
||||||
|
emit: (ev, ...args) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...args)
|
||||||
|
});
|
||||||
|
effect._result = result2;
|
||||||
|
return result2;
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
const result = effect._result;
|
||||||
|
if (result == null)
|
||||||
|
return null;
|
||||||
|
const node = result instanceof Node || isArr(result) && result.every((n) => n instanceof Node) ? result : doc.createTextNode(String(result));
|
||||||
|
const attach = (n) => {
|
||||||
|
if (isObj(n) && !n._isRuntime) {
|
||||||
|
n._mounts = effect._mounts || [];
|
||||||
|
n._cleanups = effect._cleanups || new Set;
|
||||||
|
n._ownerEffect = effect;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
isArr(node) ? node.forEach(attach) : attach(node);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
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 in props) {
|
||||||
|
if (!props.hasOwnProperty(k))
|
||||||
|
continue;
|
||||||
|
let v = props[k];
|
||||||
|
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);
|
||||||
|
const off = () => el.removeEventListener(ev, v);
|
||||||
|
el._cleanups.add(off);
|
||||||
|
onUnmount(off);
|
||||||
|
} else if (isFunc(v)) {
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const val = validateAttr(k, v());
|
||||||
|
if (k === "class")
|
||||||
|
el.className = val || "";
|
||||||
|
else if (val == null)
|
||||||
|
el.removeAttribute(k);
|
||||||
|
else if (k in el && !isSVG)
|
||||||
|
el[k] = val;
|
||||||
|
else
|
||||||
|
el.setAttribute(k, val === true ? "" : val);
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
el._cleanups.add(() => dispose(effect));
|
||||||
|
onUnmount(() => dispose(effect));
|
||||||
|
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||||
|
const evType = k === "checked" ? "change" : "input";
|
||||||
|
el.addEventListener(evType, (ev) => v(ev.target[k]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const val = validateAttr(k, v);
|
||||||
|
if (val != null) {
|
||||||
|
if (k in el && !isSVG)
|
||||||
|
el[k] = val;
|
||||||
|
else
|
||||||
|
el.setAttribute(k, val === true ? "" : val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const append = (c) => {
|
||||||
|
if (isArr(c))
|
||||||
|
return c.forEach(append);
|
||||||
|
if (isFunc(c)) {
|
||||||
|
const anchor = doc.createTextNode("");
|
||||||
|
el.appendChild(anchor);
|
||||||
|
let currentNodes = [];
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const res = c();
|
||||||
|
const next = (isArr(res) ? res : [res]).map(ensureNode);
|
||||||
|
currentNodes.forEach((n) => {
|
||||||
|
if (n._isRuntime)
|
||||||
|
n.destroy();
|
||||||
|
else
|
||||||
|
cleanupNode(n);
|
||||||
|
if (n.parentNode)
|
||||||
|
n.remove();
|
||||||
|
});
|
||||||
|
let ref = anchor;
|
||||||
|
for (let i = next.length - 1;i >= 0; i--) {
|
||||||
|
const node = next[i];
|
||||||
|
if (node.parentNode !== ref.parentNode)
|
||||||
|
ref.parentNode?.insertBefore(node, ref);
|
||||||
|
if (node._mounts)
|
||||||
|
node._mounts.forEach((fn) => fn());
|
||||||
|
ref = node;
|
||||||
|
}
|
||||||
|
currentNodes = next;
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
el._cleanups.add(() => dispose(effect));
|
||||||
|
onUnmount(() => dispose(effect));
|
||||||
|
} else {
|
||||||
|
const node = ensureNode(c);
|
||||||
|
el.appendChild(node);
|
||||||
|
if (node._mounts)
|
||||||
|
node._mounts.forEach((fn) => fn());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
append(children);
|
||||||
|
return el;
|
||||||
};
|
};
|
||||||
var Render = (renderFn) => {
|
var Render = (renderFn) => {
|
||||||
const cleanups = new Set;
|
const cleanups = new Set;
|
||||||
const previousOwner = currentOwner;
|
const mounts = [];
|
||||||
const container = createEl("div");
|
const previousOwner = activeOwner;
|
||||||
|
const previousEffect = activeEffect;
|
||||||
|
const container = doc.createElement("div");
|
||||||
container.style.display = "contents";
|
container.style.display = "contents";
|
||||||
currentOwner = { cleanups };
|
container.setAttribute("role", "presentation");
|
||||||
|
activeOwner = { _cleanups: cleanups, _mounts: mounts };
|
||||||
|
activeEffect = null;
|
||||||
const processResult = (result) => {
|
const processResult = (result) => {
|
||||||
if (!result)
|
if (!result)
|
||||||
return;
|
return;
|
||||||
@@ -77,14 +403,16 @@ var Render = (renderFn) => {
|
|||||||
} else if (isArr(result)) {
|
} else if (isArr(result)) {
|
||||||
result.forEach(processResult);
|
result.forEach(processResult);
|
||||||
} else {
|
} else {
|
||||||
container.appendChild(result instanceof Node ? result : createText(result));
|
container.appendChild(result instanceof Node ? result : doc.createTextNode(String(result == null ? "" : result)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
||||||
} finally {
|
} finally {
|
||||||
currentOwner = previousOwner;
|
activeOwner = previousOwner;
|
||||||
|
activeEffect = previousEffect;
|
||||||
}
|
}
|
||||||
|
mounts.forEach((fn) => fn());
|
||||||
return {
|
return {
|
||||||
_isRuntime: true,
|
_isRuntime: true,
|
||||||
container,
|
container,
|
||||||
@@ -95,338 +423,108 @@ var Render = (renderFn) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
var $ = (initialValue, storageKey = null) => {
|
var If = (cond, ifYes, ifNot = null) => {
|
||||||
const subscribers = new Set;
|
const anchor = doc.createTextNode("");
|
||||||
if (isFunc(initialValue)) {
|
const root = Tag("div", { style: "display:contents" }, [anchor]);
|
||||||
let cachedValue, isDirty = true;
|
let currentView = null;
|
||||||
const effect = () => {
|
Watch(() => !!(isFunc(cond) ? cond() : cond), (show) => {
|
||||||
if (effect._deleted)
|
if (currentView) {
|
||||||
return;
|
currentView.destroy();
|
||||||
effect._deps.forEach((dep) => dep.delete(effect));
|
|
||||||
effect._deps.clear();
|
|
||||||
runWithContext(effect, () => {
|
|
||||||
const newValue = initialValue();
|
|
||||||
if (!Object.is(cachedValue, newValue) || isDirty) {
|
|
||||||
cachedValue = newValue;
|
|
||||||
isDirty = false;
|
|
||||||
triggerUpdate(subscribers);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
assign(effect, {
|
|
||||||
_deps: new Set,
|
|
||||||
_isComputed: true,
|
|
||||||
_subs: subscribers,
|
|
||||||
_deleted: false,
|
|
||||||
markDirty: () => isDirty = true,
|
|
||||||
stop: () => {
|
|
||||||
effect._deleted = true;
|
|
||||||
effect._deps.forEach((dep) => dep.delete(effect));
|
|
||||||
subscribers.clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (currentOwner)
|
|
||||||
currentOwner.cleanups.add(effect.stop);
|
|
||||||
return () => {
|
|
||||||
if (isDirty)
|
|
||||||
effect();
|
|
||||||
trackSubscription(subscribers);
|
|
||||||
return cachedValue;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let value = initialValue;
|
|
||||||
if (storageKey) {
|
|
||||||
try {
|
|
||||||
const saved = localStorage.getItem(storageKey);
|
|
||||||
if (saved !== null)
|
|
||||||
value = JSON.parse(saved);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("SigPro Storage Lock", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (...args) => {
|
|
||||||
if (args.length) {
|
|
||||||
const nextValue = isFunc(args[0]) ? args[0](value) : args[0];
|
|
||||||
if (!Object.is(value, nextValue)) {
|
|
||||||
value = nextValue;
|
|
||||||
if (storageKey)
|
|
||||||
localStorage.setItem(storageKey, JSON.stringify(value));
|
|
||||||
triggerUpdate(subscribers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trackSubscription(subscribers);
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
var $$ = (object, cache = new WeakMap) => {
|
|
||||||
if (!isObj(object))
|
|
||||||
return object;
|
|
||||||
if (cache.has(object))
|
|
||||||
return cache.get(object);
|
|
||||||
const keySubscribers = {};
|
|
||||||
const proxy = new Proxy(object, {
|
|
||||||
get(target, key) {
|
|
||||||
if (activeEffect)
|
|
||||||
trackSubscription(keySubscribers[key] ??= new Set);
|
|
||||||
const value = Reflect.get(target, key);
|
|
||||||
return isObj(value) ? $$(value, cache) : value;
|
|
||||||
},
|
|
||||||
set(target, key, value) {
|
|
||||||
if (Object.is(target[key], value))
|
|
||||||
return true;
|
|
||||||
const success = Reflect.set(target, key, value);
|
|
||||||
if (keySubscribers[key])
|
|
||||||
triggerUpdate(keySubscribers[key]);
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cache.set(object, proxy);
|
|
||||||
return proxy;
|
|
||||||
};
|
|
||||||
var Watch = (target, callbackFn) => {
|
|
||||||
const isExplicit = isArr(target);
|
|
||||||
const callback = isExplicit ? callbackFn : target;
|
|
||||||
if (!isFunc(callback))
|
|
||||||
return () => {};
|
|
||||||
const owner = currentOwner;
|
|
||||||
const runner = () => {
|
|
||||||
if (runner._deleted)
|
|
||||||
return;
|
|
||||||
runner._deps.forEach((dep) => dep.delete(runner));
|
|
||||||
runner._deps.clear();
|
|
||||||
runner._cleanups.forEach((cleanup) => cleanup());
|
|
||||||
runner._cleanups.clear();
|
|
||||||
const previousOwner = currentOwner;
|
|
||||||
runner.depth = activeEffect ? activeEffect.depth + 1 : 0;
|
|
||||||
runWithContext(runner, () => {
|
|
||||||
currentOwner = { cleanups: runner._cleanups };
|
|
||||||
if (isExplicit) {
|
|
||||||
runWithContext(null, callback);
|
|
||||||
target.forEach((dep) => isFunc(dep) && dep());
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
currentOwner = previousOwner;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
assign(runner, {
|
|
||||||
_deps: new Set,
|
|
||||||
_cleanups: new Set,
|
|
||||||
_deleted: false,
|
|
||||||
stop: () => {
|
|
||||||
if (runner._deleted)
|
|
||||||
return;
|
|
||||||
runner._deleted = true;
|
|
||||||
effectQueue.delete(runner);
|
|
||||||
runner._deps.forEach((dep) => dep.delete(runner));
|
|
||||||
runner._cleanups.forEach((cleanup) => cleanup());
|
|
||||||
if (owner)
|
|
||||||
owner.cleanups.delete(runner.stop);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (owner)
|
|
||||||
owner.cleanups.add(runner.stop);
|
|
||||||
runner();
|
|
||||||
return runner.stop;
|
|
||||||
};
|
|
||||||
var Tag = (tag, props = {}, children = []) => {
|
|
||||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
|
||||||
children = props;
|
|
||||||
props = {};
|
|
||||||
}
|
|
||||||
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
|
|
||||||
const element = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : createEl(tag);
|
|
||||||
element._cleanups = new Set;
|
|
||||||
element.onUnmount = (fn) => element._cleanups.add(fn);
|
|
||||||
const booleanAttributes = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
|
||||||
const updateAttribute = (name, value) => {
|
|
||||||
const sanitized = (name === "src" || name === "href") && String(value).toLowerCase().includes("javascript:") ? "#" : value;
|
|
||||||
if (booleanAttributes.includes(name)) {
|
|
||||||
element[name] = !!sanitized;
|
|
||||||
sanitized ? element.setAttribute(name, "") : element.removeAttribute(name);
|
|
||||||
} else {
|
|
||||||
sanitized == null ? element.removeAttribute(name) : element.setAttribute(name, sanitized);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (let [key, value] of Object.entries(props)) {
|
|
||||||
if (key === "ref") {
|
|
||||||
isFunc(value) ? value(element) : value.current = element;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const isSignal = isFunc(value);
|
|
||||||
if (key.startsWith("on")) {
|
|
||||||
const eventName = key.slice(2).toLowerCase().split(".")[0];
|
|
||||||
element.addEventListener(eventName, value);
|
|
||||||
element._cleanups.add(() => element.removeEventListener(eventName, value));
|
|
||||||
} else if (isSignal) {
|
|
||||||
element._cleanups.add(Watch(() => {
|
|
||||||
const currentVal = value();
|
|
||||||
key === "class" ? element.className = currentVal || "" : updateAttribute(key, currentVal);
|
|
||||||
}));
|
|
||||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName) && (key === "value" || key === "checked")) {
|
|
||||||
const event = key === "checked" ? "change" : "input";
|
|
||||||
const handler = (e) => value(e.target[key]);
|
|
||||||
element.addEventListener(event, handler);
|
|
||||||
element._cleanups.add(() => element.removeEventListener(event, handler));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateAttribute(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const appendChildNode = (child) => {
|
|
||||||
if (isArr(child))
|
|
||||||
return child.forEach(appendChildNode);
|
|
||||||
if (isFunc(child)) {
|
|
||||||
const marker = createText("");
|
|
||||||
element.appendChild(marker);
|
|
||||||
let currentNodes = [];
|
|
||||||
element._cleanups.add(Watch(() => {
|
|
||||||
const result = child();
|
|
||||||
const nextNodes = (isArr(result) ? result : [result]).map((node) => node?._isRuntime ? node.container : node instanceof Node ? node : createText(node));
|
|
||||||
currentNodes.forEach((node) => {
|
|
||||||
cleanupNode(node);
|
|
||||||
node.remove();
|
|
||||||
});
|
|
||||||
nextNodes.forEach((node) => marker.parentNode?.insertBefore(node, marker));
|
|
||||||
currentNodes = nextNodes;
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
element.appendChild(child instanceof Node ? child : createText(child));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
appendChildNode(children);
|
|
||||||
return element;
|
|
||||||
};
|
|
||||||
var If = (condition, thenVal, otherwiseVal = null, transition = null) => {
|
|
||||||
const marker = createText("");
|
|
||||||
const container = Tag("div", { style: "display:contents" }, [marker]);
|
|
||||||
let currentView = null, lastState = null;
|
|
||||||
Watch(() => {
|
|
||||||
const state = !!(isFunc(condition) ? condition() : condition);
|
|
||||||
if (state === lastState)
|
|
||||||
return;
|
|
||||||
lastState = state;
|
|
||||||
const dispose = () => {
|
|
||||||
if (currentView)
|
|
||||||
currentView.destroy();
|
|
||||||
currentView = null;
|
currentView = null;
|
||||||
};
|
|
||||||
if (currentView && !state && transition?.out) {
|
|
||||||
transition.out(currentView.container, dispose);
|
|
||||||
} else {
|
|
||||||
dispose();
|
|
||||||
}
|
}
|
||||||
const branch = state ? thenVal : otherwiseVal;
|
const content = show ? ifYes : ifNot;
|
||||||
if (branch) {
|
if (content) {
|
||||||
currentView = Render(() => isFunc(branch) ? branch() : branch);
|
currentView = Render(() => isFunc(content) ? content() : content);
|
||||||
container.insertBefore(currentView.container, marker);
|
root.insertBefore(currentView.container, anchor);
|
||||||
if (state && transition?.in)
|
|
||||||
transition.in(currentView.container);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return container;
|
onUnmount(() => currentView?.destroy());
|
||||||
|
return root;
|
||||||
};
|
};
|
||||||
var For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
|
var For = (src, itemFn, keyFn) => {
|
||||||
const marker = createText("");
|
const anchor = doc.createTextNode("");
|
||||||
const container = Tag(tag, props, [marker]);
|
const root = Tag("div", { style: "display:contents" }, [anchor]);
|
||||||
let viewCache = new Map;
|
let cache = new Map;
|
||||||
Watch(() => {
|
Watch(() => (isFunc(src) ? src() : src) || [], (items) => {
|
||||||
const items = (isFunc(source) ? source() : source) || [];
|
|
||||||
const nextCache = new Map;
|
const nextCache = new Map;
|
||||||
const order = [];
|
const nextOrder = [];
|
||||||
for (let i = 0;i < items.length; i++) {
|
const newItems = items || [];
|
||||||
const item = items[i];
|
for (let i = 0;i < newItems.length; i++) {
|
||||||
const key = keyFn ? keyFn(item, i) : i;
|
const item = newItems[i];
|
||||||
let view = viewCache.get(key);
|
const key = keyFn ? keyFn(item, i) : item?.id ?? i;
|
||||||
if (!view) {
|
let view = cache.get(key);
|
||||||
const result = renderFn(item, i);
|
if (!view)
|
||||||
view = result instanceof Node ? { container: result, destroy: () => {
|
view = Render(() => itemFn(item, i));
|
||||||
cleanupNode(result);
|
else
|
||||||
result.remove();
|
cache.delete(key);
|
||||||
} } : Render(() => result);
|
|
||||||
}
|
|
||||||
viewCache.delete(key);
|
|
||||||
nextCache.set(key, view);
|
nextCache.set(key, view);
|
||||||
order.push(key);
|
nextOrder.push(view);
|
||||||
}
|
}
|
||||||
viewCache.forEach((v) => v.destroy());
|
cache.forEach((view) => view.destroy());
|
||||||
let anchor = marker;
|
let lastRef = anchor;
|
||||||
for (let i = order.length - 1;i >= 0; i--) {
|
for (let i = nextOrder.length - 1;i >= 0; i--) {
|
||||||
const view = nextCache.get(order[i]);
|
const view = nextOrder[i];
|
||||||
if (view.container.nextSibling !== anchor) {
|
const node = view.container;
|
||||||
container.insertBefore(view.container, anchor);
|
if (node.nextSibling !== lastRef)
|
||||||
}
|
root.insertBefore(node, lastRef);
|
||||||
anchor = view.container;
|
lastRef = node;
|
||||||
}
|
}
|
||||||
viewCache = nextCache;
|
cache = nextCache;
|
||||||
});
|
});
|
||||||
return container;
|
return root;
|
||||||
};
|
};
|
||||||
var Router = (routes) => {
|
var Router = (routes) => {
|
||||||
const currentPath = $(window.location.hash.replace(/^#/, "") || "/");
|
const getHash = () => window.location.hash.slice(1) || "/";
|
||||||
window.addEventListener("hashchange", () => currentPath(window.location.hash.replace(/^#/, "") || "/"));
|
const path = $(getHash());
|
||||||
const outlet = Tag("div", { class: "router-transition" });
|
const handler = () => path(getHash());
|
||||||
|
window.addEventListener("hashchange", handler);
|
||||||
|
onUnmount(() => window.removeEventListener("hashchange", handler));
|
||||||
|
const hook = Tag("div", { class: "router-hook" });
|
||||||
let currentView = null;
|
let currentView = null;
|
||||||
Watch([currentPath], async () => {
|
Watch([path], () => {
|
||||||
const path = currentPath();
|
const cur = path();
|
||||||
const route = routes.find((r) => {
|
const route = routes.find((r) => {
|
||||||
const routeParts = r.path.split("/").filter(Boolean);
|
const p1 = r.path.split("/").filter(Boolean);
|
||||||
const pathParts = path.split("/").filter(Boolean);
|
const p2 = cur.split("/").filter(Boolean);
|
||||||
return routeParts.length === pathParts.length && routeParts.every((part, i) => part.startsWith(":") || part === pathParts[i]);
|
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]);
|
||||||
}) || routes.find((r) => r.path === "*");
|
}) || routes.find((r) => r.path === "*");
|
||||||
if (route) {
|
if (route) {
|
||||||
let component = route.component;
|
currentView?.destroy();
|
||||||
if (isFunc(component) && component.toString().includes("import")) {
|
|
||||||
component = (await component()).default || await component();
|
|
||||||
}
|
|
||||||
const params = {};
|
const params = {};
|
||||||
route.path.split("/").filter(Boolean).forEach((part, i) => {
|
route.path.split("/").filter(Boolean).forEach((p, i) => {
|
||||||
if (part.startsWith(":"))
|
if (p[0] === ":")
|
||||||
params[part.slice(1)] = path.split("/").filter(Boolean)[i];
|
params[p.slice(1)] = cur.split("/").filter(Boolean)[i];
|
||||||
});
|
});
|
||||||
if (currentView)
|
Router.params(params);
|
||||||
currentView.destroy();
|
currentView = Render(() => isFunc(route.component) ? route.component(params) : route.component);
|
||||||
if (Router.params)
|
hook.replaceChildren(currentView.container);
|
||||||
Router.params(params);
|
|
||||||
currentView = Render(() => {
|
|
||||||
try {
|
|
||||||
return isFunc(component) ? component(params) : component;
|
|
||||||
} catch (e) {
|
|
||||||
return Tag("div", { class: "p-4 text-error" }, "Error loading view");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
outlet.appendChild(currentView.container);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return outlet;
|
return hook;
|
||||||
};
|
};
|
||||||
Router.params = $({});
|
Router.params = $({});
|
||||||
Router.to = (path) => window.location.hash = path.replace(/^#?\/?/, "#/");
|
Router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||||
Router.back = () => window.history.back();
|
Router.back = () => window.history.back();
|
||||||
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||||
var Mount = (component, target) => {
|
var Mount = (comp, target) => {
|
||||||
const targetEl = typeof target === "string" ? doc.querySelector(target) : target;
|
const t = typeof target === "string" ? doc.querySelector(target) : target;
|
||||||
if (!targetEl)
|
if (!t)
|
||||||
return;
|
return;
|
||||||
if (MOUNTED_NODES.has(targetEl))
|
if (MOUNTED_NODES.has(t))
|
||||||
MOUNTED_NODES.get(targetEl).destroy();
|
MOUNTED_NODES.get(t).destroy();
|
||||||
const instance = Render(isFunc(component) ? component : () => component);
|
const inst = Render(isFunc(comp) ? comp : () => comp);
|
||||||
targetEl.replaceChildren(instance.container);
|
t.replaceChildren(inst.container);
|
||||||
MOUNTED_NODES.set(targetEl, instance);
|
MOUNTED_NODES.set(t, inst);
|
||||||
return instance;
|
return inst;
|
||||||
};
|
};
|
||||||
var SigPro = { $, $$, Render, Watch, Tag, If, For, Router, Mount };
|
var SigPro = Object.freeze({ $, $$, Watch, Tag, Render, If, For, Router, Mount, onMount, onUnmount, Batch });
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
assign(window, SigPro);
|
Object.assign(window, SigPro);
|
||||||
const tags = `div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(" ");
|
"div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg".split(" ").forEach((t) => window[t[0].toUpperCase() + t.slice(1)] = (p, c) => SigPro.Tag(t, p, c));
|
||||||
tags.forEach((tag) => {
|
|
||||||
const helper = tag[0].toUpperCase() + tag.slice(1);
|
|
||||||
if (!(helper in window))
|
|
||||||
window[helper] = (p, c) => Tag(tag, p, c);
|
|
||||||
});
|
|
||||||
window.SigPro = Object.freeze(SigPro);
|
|
||||||
}
|
}
|
||||||
export {
|
export {
|
||||||
|
onUnmount,
|
||||||
|
onMount,
|
||||||
Watch,
|
Watch,
|
||||||
Tag,
|
Tag,
|
||||||
Router,
|
Router,
|
||||||
@@ -434,6 +532,7 @@ export {
|
|||||||
Mount,
|
Mount,
|
||||||
If,
|
If,
|
||||||
For,
|
For,
|
||||||
|
Batch,
|
||||||
$$,
|
$$,
|
||||||
$
|
$
|
||||||
};
|
};
|
||||||
|
|||||||
2
dist/sigpro.esm.min.js
vendored
2
dist/sigpro.esm.min.js
vendored
File diff suppressed because one or more lines are too long
809
dist/sigpro.js
vendored
809
dist/sigpro.js
vendored
@@ -30,6 +30,8 @@
|
|||||||
// index.js
|
// index.js
|
||||||
var exports_sigpro = {};
|
var exports_sigpro = {};
|
||||||
__export(exports_sigpro, {
|
__export(exports_sigpro, {
|
||||||
|
onUnmount: () => onUnmount,
|
||||||
|
onMount: () => onMount,
|
||||||
Watch: () => Watch,
|
Watch: () => Watch,
|
||||||
Tag: () => Tag,
|
Tag: () => Tag,
|
||||||
Router: () => Router,
|
Router: () => Router,
|
||||||
@@ -37,80 +39,407 @@
|
|||||||
Mount: () => Mount,
|
Mount: () => Mount,
|
||||||
If: () => If,
|
If: () => If,
|
||||||
For: () => For,
|
For: () => For,
|
||||||
|
Batch: () => Batch,
|
||||||
$$: () => $$,
|
$$: () => $$,
|
||||||
$: () => $
|
$: () => $
|
||||||
});
|
});
|
||||||
|
|
||||||
// sigpro.js
|
// sigpro.js
|
||||||
var activeEffect = null;
|
|
||||||
var currentOwner = null;
|
|
||||||
var effectQueue = new Set;
|
|
||||||
var isFlushing = false;
|
|
||||||
var MOUNTED_NODES = new WeakMap;
|
|
||||||
var doc = document;
|
|
||||||
var isArr = Array.isArray;
|
|
||||||
var assign = Object.assign;
|
|
||||||
var createEl = (t) => doc.createElement(t);
|
|
||||||
var createText = (t) => doc.createTextNode(String(t ?? ""));
|
|
||||||
var isFunc = (f) => typeof f === "function";
|
var isFunc = (f) => typeof f === "function";
|
||||||
var isObj = (o) => typeof o === "object" && o !== null;
|
var isObj = (o) => o && typeof o === "object";
|
||||||
var runWithContext = (effect, callback) => {
|
var isArr = Array.isArray;
|
||||||
const previousEffect = activeEffect;
|
var doc = typeof document !== "undefined" ? document : null;
|
||||||
activeEffect = effect;
|
var ensureNode = (n) => n?._isRuntime ? n.container : n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n));
|
||||||
|
var activeEffect = null;
|
||||||
|
var activeOwner = null;
|
||||||
|
var isFlushing = false;
|
||||||
|
var batchDepth = 0;
|
||||||
|
var effectQueue = new Set;
|
||||||
|
var proxyCache = new WeakMap;
|
||||||
|
var ITER = Symbol("iter");
|
||||||
|
var MOUNTED_NODES = new WeakMap;
|
||||||
|
var dispose = (eff) => {
|
||||||
|
if (!eff || eff._disposed)
|
||||||
|
return;
|
||||||
|
eff._disposed = true;
|
||||||
|
const stack = [eff];
|
||||||
|
while (stack.length) {
|
||||||
|
const e = stack.pop();
|
||||||
|
if (e._cleanups) {
|
||||||
|
e._cleanups.forEach((fn) => fn());
|
||||||
|
e._cleanups.clear();
|
||||||
|
}
|
||||||
|
if (e._children) {
|
||||||
|
e._children.forEach((child) => stack.push(child));
|
||||||
|
e._children.clear();
|
||||||
|
}
|
||||||
|
if (e._deps) {
|
||||||
|
e._deps.forEach((depSet) => depSet.delete(e));
|
||||||
|
e._deps.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var onMount = (fn) => {
|
||||||
|
if (activeOwner)
|
||||||
|
(activeOwner._mounts ||= []).push(fn);
|
||||||
|
};
|
||||||
|
var onUnmount = (fn) => {
|
||||||
|
if (activeOwner)
|
||||||
|
(activeOwner._cleanups ||= new Set).add(fn);
|
||||||
|
};
|
||||||
|
var untrack = (fn) => {
|
||||||
|
const p = activeEffect;
|
||||||
|
activeEffect = null;
|
||||||
try {
|
try {
|
||||||
return callback();
|
return fn();
|
||||||
} finally {
|
} finally {
|
||||||
activeEffect = previousEffect;
|
activeEffect = p;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var cleanupNode = (node) => {
|
var createEffect = (fn, isComputed = false) => {
|
||||||
if (node._cleanups) {
|
const effect = () => {
|
||||||
node._cleanups.forEach((dispose) => dispose());
|
if (effect._disposed)
|
||||||
node._cleanups.clear();
|
return;
|
||||||
}
|
if (effect._deps)
|
||||||
node.childNodes?.forEach(cleanupNode);
|
effect._deps.forEach((s) => s.delete(effect));
|
||||||
|
if (effect._cleanups) {
|
||||||
|
effect._cleanups.forEach((c) => c());
|
||||||
|
effect._cleanups.clear();
|
||||||
|
}
|
||||||
|
const prevEffect = activeEffect;
|
||||||
|
const prevOwner = activeOwner;
|
||||||
|
activeEffect = activeOwner = effect;
|
||||||
|
try {
|
||||||
|
return effect._result = fn();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[SigPro]", e);
|
||||||
|
} finally {
|
||||||
|
activeEffect = prevEffect;
|
||||||
|
activeOwner = prevOwner;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
effect._deps = effect._cleanups = effect._children = null;
|
||||||
|
effect._disposed = false;
|
||||||
|
effect._isComputed = isComputed;
|
||||||
|
effect._depth = activeEffect ? activeEffect._depth + 1 : 0;
|
||||||
|
effect._mounts = [];
|
||||||
|
effect._parent = activeOwner;
|
||||||
|
if (activeOwner)
|
||||||
|
(activeOwner._children ||= new Set).add(effect);
|
||||||
|
return effect;
|
||||||
};
|
};
|
||||||
var flushEffects = () => {
|
var flush = () => {
|
||||||
if (isFlushing)
|
if (isFlushing)
|
||||||
return;
|
return;
|
||||||
isFlushing = true;
|
isFlushing = true;
|
||||||
while (effectQueue.size > 0) {
|
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth);
|
||||||
const sortedEffects = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
|
effectQueue.clear();
|
||||||
effectQueue.clear();
|
for (const e of sorted)
|
||||||
for (const effect of sortedEffects) {
|
if (!e._disposed)
|
||||||
if (!effect._deleted)
|
e();
|
||||||
effect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isFlushing = false;
|
isFlushing = false;
|
||||||
};
|
};
|
||||||
var trackSubscription = (subscribers) => {
|
var Batch = (fn) => {
|
||||||
if (activeEffect && !activeEffect._deleted) {
|
batchDepth++;
|
||||||
subscribers.add(activeEffect);
|
try {
|
||||||
activeEffect._deps.add(subscribers);
|
return fn();
|
||||||
|
} finally {
|
||||||
|
batchDepth--;
|
||||||
|
if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) {
|
||||||
|
flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var triggerUpdate = (subscribers) => {
|
var trackUpdate = (subs, trigger = false) => {
|
||||||
subscribers.forEach((effect) => {
|
if (!trigger && activeEffect && !activeEffect._disposed) {
|
||||||
if (effect === activeEffect || effect._deleted)
|
subs.add(activeEffect);
|
||||||
return;
|
(activeEffect._deps ||= new Set).add(subs);
|
||||||
if (effect._isComputed) {
|
} else if (trigger) {
|
||||||
effect.markDirty();
|
let hasQueue = false;
|
||||||
if (effect._subs)
|
subs.forEach((e) => {
|
||||||
triggerUpdate(effect._subs);
|
if (e === activeEffect || e._disposed)
|
||||||
} else {
|
return;
|
||||||
effectQueue.add(effect);
|
if (e._isComputed) {
|
||||||
|
e._dirty = true;
|
||||||
|
if (e._subs)
|
||||||
|
trackUpdate(e._subs, true);
|
||||||
|
} else {
|
||||||
|
effectQueue.add(e);
|
||||||
|
hasQueue = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (hasQueue && !isFlushing && batchDepth === 0)
|
||||||
|
queueMicrotask(flush);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var $ = (val, key = null) => {
|
||||||
|
const subs = new Set;
|
||||||
|
if (isFunc(val)) {
|
||||||
|
let cache, dirty = true;
|
||||||
|
const computed = () => {
|
||||||
|
if (dirty) {
|
||||||
|
const prev = activeEffect;
|
||||||
|
activeEffect = computed;
|
||||||
|
try {
|
||||||
|
const next = val();
|
||||||
|
if (!Object.is(cache, next)) {
|
||||||
|
cache = next;
|
||||||
|
dirty = false;
|
||||||
|
trackUpdate(subs, true);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
activeEffect = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackUpdate(subs);
|
||||||
|
return cache;
|
||||||
|
};
|
||||||
|
computed._isComputed = true;
|
||||||
|
computed._subs = subs;
|
||||||
|
computed._dirty = true;
|
||||||
|
computed._deps = null;
|
||||||
|
computed._disposed = false;
|
||||||
|
computed.markDirty = () => {
|
||||||
|
dirty = true;
|
||||||
|
};
|
||||||
|
computed.stop = () => {
|
||||||
|
computed._disposed = true;
|
||||||
|
if (computed._deps) {
|
||||||
|
computed._deps.forEach((depSet) => depSet.delete(computed));
|
||||||
|
computed._deps.clear();
|
||||||
|
}
|
||||||
|
subs.clear();
|
||||||
|
};
|
||||||
|
if (activeOwner)
|
||||||
|
onUnmount(computed.stop);
|
||||||
|
return computed;
|
||||||
|
}
|
||||||
|
if (key)
|
||||||
|
try {
|
||||||
|
val = JSON.parse(localStorage.getItem(key)) ?? val;
|
||||||
|
} catch (e) {}
|
||||||
|
return (...args) => {
|
||||||
|
if (args.length) {
|
||||||
|
const next = isFunc(args[0]) ? args[0](val) : args[0];
|
||||||
|
if (!Object.is(val, next)) {
|
||||||
|
val = next;
|
||||||
|
if (key)
|
||||||
|
localStorage.setItem(key, JSON.stringify(val));
|
||||||
|
trackUpdate(subs, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackUpdate(subs);
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var $$ = (target) => {
|
||||||
|
if (!isObj(target))
|
||||||
|
return target;
|
||||||
|
if (proxyCache.has(target))
|
||||||
|
return proxyCache.get(target);
|
||||||
|
const subsMap = new Map;
|
||||||
|
const getSubs = (k) => {
|
||||||
|
let s = subsMap.get(k);
|
||||||
|
if (!s)
|
||||||
|
subsMap.set(k, s = new Set);
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
const proxy = new Proxy(target, {
|
||||||
|
get(t, k) {
|
||||||
|
trackUpdate(getSubs(k));
|
||||||
|
return $$(t[k]);
|
||||||
|
},
|
||||||
|
set(t, k, v) {
|
||||||
|
const isNew = !(k in t);
|
||||||
|
if (!Object.is(t[k], v)) {
|
||||||
|
t[k] = v;
|
||||||
|
trackUpdate(getSubs(k), true);
|
||||||
|
if (isNew)
|
||||||
|
trackUpdate(getSubs(ITER), true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deleteProperty(t, k) {
|
||||||
|
const res = Reflect.deleteProperty(t, k);
|
||||||
|
if (res) {
|
||||||
|
trackUpdate(getSubs(k), true);
|
||||||
|
trackUpdate(getSubs(ITER), true);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
ownKeys(t) {
|
||||||
|
trackUpdate(getSubs(ITER));
|
||||||
|
return Reflect.ownKeys(t);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!isFlushing)
|
proxyCache.set(target, proxy);
|
||||||
queueMicrotask(flushEffects);
|
return proxy;
|
||||||
|
};
|
||||||
|
var Watch = (sources, cb) => {
|
||||||
|
if (cb === undefined) {
|
||||||
|
const effect2 = createEffect(sources);
|
||||||
|
effect2();
|
||||||
|
return () => dispose(effect2);
|
||||||
|
}
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const vals = Array.isArray(sources) ? sources.map((s) => s()) : sources();
|
||||||
|
untrack(() => cb(vals));
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
return () => dispose(effect);
|
||||||
|
};
|
||||||
|
var cleanupNode = (node) => {
|
||||||
|
if (node._cleanups) {
|
||||||
|
node._cleanups.forEach((fn) => fn());
|
||||||
|
node._cleanups.clear();
|
||||||
|
}
|
||||||
|
if (node._ownerEffect)
|
||||||
|
dispose(node._ownerEffect);
|
||||||
|
if (node.childNodes)
|
||||||
|
node.childNodes.forEach(cleanupNode);
|
||||||
|
};
|
||||||
|
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||||
|
var isDangerousAttr = (key) => key === "src" || key === "href" || key.startsWith("on");
|
||||||
|
var validateAttr = (key, val) => {
|
||||||
|
if (val == null || val === false)
|
||||||
|
return null;
|
||||||
|
if (isDangerousAttr(key)) {
|
||||||
|
const sVal = String(val);
|
||||||
|
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
||||||
|
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`);
|
||||||
|
return "#";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
var Tag = (tag, props = {}, children = []) => {
|
||||||
|
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||||
|
children = props;
|
||||||
|
props = {};
|
||||||
|
}
|
||||||
|
if (isFunc(tag)) {
|
||||||
|
const ctx = { _mounts: [], _cleanups: new Set };
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const result2 = tag(props, {
|
||||||
|
children,
|
||||||
|
emit: (ev, ...args) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...args)
|
||||||
|
});
|
||||||
|
effect._result = result2;
|
||||||
|
return result2;
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
const result = effect._result;
|
||||||
|
if (result == null)
|
||||||
|
return null;
|
||||||
|
const node = result instanceof Node || isArr(result) && result.every((n) => n instanceof Node) ? result : doc.createTextNode(String(result));
|
||||||
|
const attach = (n) => {
|
||||||
|
if (isObj(n) && !n._isRuntime) {
|
||||||
|
n._mounts = effect._mounts || [];
|
||||||
|
n._cleanups = effect._cleanups || new Set;
|
||||||
|
n._ownerEffect = effect;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
isArr(node) ? node.forEach(attach) : attach(node);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
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 in props) {
|
||||||
|
if (!props.hasOwnProperty(k))
|
||||||
|
continue;
|
||||||
|
let v = props[k];
|
||||||
|
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);
|
||||||
|
const off = () => el.removeEventListener(ev, v);
|
||||||
|
el._cleanups.add(off);
|
||||||
|
onUnmount(off);
|
||||||
|
} else if (isFunc(v)) {
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const val = validateAttr(k, v());
|
||||||
|
if (k === "class")
|
||||||
|
el.className = val || "";
|
||||||
|
else if (val == null)
|
||||||
|
el.removeAttribute(k);
|
||||||
|
else if (k in el && !isSVG)
|
||||||
|
el[k] = val;
|
||||||
|
else
|
||||||
|
el.setAttribute(k, val === true ? "" : val);
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
el._cleanups.add(() => dispose(effect));
|
||||||
|
onUnmount(() => dispose(effect));
|
||||||
|
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||||
|
const evType = k === "checked" ? "change" : "input";
|
||||||
|
el.addEventListener(evType, (ev) => v(ev.target[k]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const val = validateAttr(k, v);
|
||||||
|
if (val != null) {
|
||||||
|
if (k in el && !isSVG)
|
||||||
|
el[k] = val;
|
||||||
|
else
|
||||||
|
el.setAttribute(k, val === true ? "" : val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const append = (c) => {
|
||||||
|
if (isArr(c))
|
||||||
|
return c.forEach(append);
|
||||||
|
if (isFunc(c)) {
|
||||||
|
const anchor = doc.createTextNode("");
|
||||||
|
el.appendChild(anchor);
|
||||||
|
let currentNodes = [];
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const res = c();
|
||||||
|
const next = (isArr(res) ? res : [res]).map(ensureNode);
|
||||||
|
currentNodes.forEach((n) => {
|
||||||
|
if (n._isRuntime)
|
||||||
|
n.destroy();
|
||||||
|
else
|
||||||
|
cleanupNode(n);
|
||||||
|
if (n.parentNode)
|
||||||
|
n.remove();
|
||||||
|
});
|
||||||
|
let ref = anchor;
|
||||||
|
for (let i = next.length - 1;i >= 0; i--) {
|
||||||
|
const node = next[i];
|
||||||
|
if (node.parentNode !== ref.parentNode)
|
||||||
|
ref.parentNode?.insertBefore(node, ref);
|
||||||
|
if (node._mounts)
|
||||||
|
node._mounts.forEach((fn) => fn());
|
||||||
|
ref = node;
|
||||||
|
}
|
||||||
|
currentNodes = next;
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
el._cleanups.add(() => dispose(effect));
|
||||||
|
onUnmount(() => dispose(effect));
|
||||||
|
} else {
|
||||||
|
const node = ensureNode(c);
|
||||||
|
el.appendChild(node);
|
||||||
|
if (node._mounts)
|
||||||
|
node._mounts.forEach((fn) => fn());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
append(children);
|
||||||
|
return el;
|
||||||
};
|
};
|
||||||
var Render = (renderFn) => {
|
var Render = (renderFn) => {
|
||||||
const cleanups = new Set;
|
const cleanups = new Set;
|
||||||
const previousOwner = currentOwner;
|
const mounts = [];
|
||||||
const container = createEl("div");
|
const previousOwner = activeOwner;
|
||||||
|
const previousEffect = activeEffect;
|
||||||
|
const container = doc.createElement("div");
|
||||||
container.style.display = "contents";
|
container.style.display = "contents";
|
||||||
currentOwner = { cleanups };
|
container.setAttribute("role", "presentation");
|
||||||
|
activeOwner = { _cleanups: cleanups, _mounts: mounts };
|
||||||
|
activeEffect = null;
|
||||||
const processResult = (result) => {
|
const processResult = (result) => {
|
||||||
if (!result)
|
if (!result)
|
||||||
return;
|
return;
|
||||||
@@ -120,14 +449,16 @@
|
|||||||
} else if (isArr(result)) {
|
} else if (isArr(result)) {
|
||||||
result.forEach(processResult);
|
result.forEach(processResult);
|
||||||
} else {
|
} else {
|
||||||
container.appendChild(result instanceof Node ? result : createText(result));
|
container.appendChild(result instanceof Node ? result : doc.createTextNode(String(result == null ? "" : result)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
||||||
} finally {
|
} finally {
|
||||||
currentOwner = previousOwner;
|
activeOwner = previousOwner;
|
||||||
|
activeEffect = previousEffect;
|
||||||
}
|
}
|
||||||
|
mounts.forEach((fn) => fn());
|
||||||
return {
|
return {
|
||||||
_isRuntime: true,
|
_isRuntime: true,
|
||||||
container,
|
container,
|
||||||
@@ -138,335 +469,103 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
var $ = (initialValue, storageKey = null) => {
|
var If = (cond, ifYes, ifNot = null) => {
|
||||||
const subscribers = new Set;
|
const anchor = doc.createTextNode("");
|
||||||
if (isFunc(initialValue)) {
|
const root = Tag("div", { style: "display:contents" }, [anchor]);
|
||||||
let cachedValue, isDirty = true;
|
let currentView = null;
|
||||||
const effect = () => {
|
Watch(() => !!(isFunc(cond) ? cond() : cond), (show) => {
|
||||||
if (effect._deleted)
|
if (currentView) {
|
||||||
return;
|
currentView.destroy();
|
||||||
effect._deps.forEach((dep) => dep.delete(effect));
|
|
||||||
effect._deps.clear();
|
|
||||||
runWithContext(effect, () => {
|
|
||||||
const newValue = initialValue();
|
|
||||||
if (!Object.is(cachedValue, newValue) || isDirty) {
|
|
||||||
cachedValue = newValue;
|
|
||||||
isDirty = false;
|
|
||||||
triggerUpdate(subscribers);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
assign(effect, {
|
|
||||||
_deps: new Set,
|
|
||||||
_isComputed: true,
|
|
||||||
_subs: subscribers,
|
|
||||||
_deleted: false,
|
|
||||||
markDirty: () => isDirty = true,
|
|
||||||
stop: () => {
|
|
||||||
effect._deleted = true;
|
|
||||||
effect._deps.forEach((dep) => dep.delete(effect));
|
|
||||||
subscribers.clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (currentOwner)
|
|
||||||
currentOwner.cleanups.add(effect.stop);
|
|
||||||
return () => {
|
|
||||||
if (isDirty)
|
|
||||||
effect();
|
|
||||||
trackSubscription(subscribers);
|
|
||||||
return cachedValue;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let value = initialValue;
|
|
||||||
if (storageKey) {
|
|
||||||
try {
|
|
||||||
const saved = localStorage.getItem(storageKey);
|
|
||||||
if (saved !== null)
|
|
||||||
value = JSON.parse(saved);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("SigPro Storage Lock", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (...args) => {
|
|
||||||
if (args.length) {
|
|
||||||
const nextValue = isFunc(args[0]) ? args[0](value) : args[0];
|
|
||||||
if (!Object.is(value, nextValue)) {
|
|
||||||
value = nextValue;
|
|
||||||
if (storageKey)
|
|
||||||
localStorage.setItem(storageKey, JSON.stringify(value));
|
|
||||||
triggerUpdate(subscribers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trackSubscription(subscribers);
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
var $$ = (object, cache = new WeakMap) => {
|
|
||||||
if (!isObj(object))
|
|
||||||
return object;
|
|
||||||
if (cache.has(object))
|
|
||||||
return cache.get(object);
|
|
||||||
const keySubscribers = {};
|
|
||||||
const proxy = new Proxy(object, {
|
|
||||||
get(target, key) {
|
|
||||||
if (activeEffect)
|
|
||||||
trackSubscription(keySubscribers[key] ??= new Set);
|
|
||||||
const value = Reflect.get(target, key);
|
|
||||||
return isObj(value) ? $$(value, cache) : value;
|
|
||||||
},
|
|
||||||
set(target, key, value) {
|
|
||||||
if (Object.is(target[key], value))
|
|
||||||
return true;
|
|
||||||
const success = Reflect.set(target, key, value);
|
|
||||||
if (keySubscribers[key])
|
|
||||||
triggerUpdate(keySubscribers[key]);
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cache.set(object, proxy);
|
|
||||||
return proxy;
|
|
||||||
};
|
|
||||||
var Watch = (target, callbackFn) => {
|
|
||||||
const isExplicit = isArr(target);
|
|
||||||
const callback = isExplicit ? callbackFn : target;
|
|
||||||
if (!isFunc(callback))
|
|
||||||
return () => {};
|
|
||||||
const owner = currentOwner;
|
|
||||||
const runner = () => {
|
|
||||||
if (runner._deleted)
|
|
||||||
return;
|
|
||||||
runner._deps.forEach((dep) => dep.delete(runner));
|
|
||||||
runner._deps.clear();
|
|
||||||
runner._cleanups.forEach((cleanup) => cleanup());
|
|
||||||
runner._cleanups.clear();
|
|
||||||
const previousOwner = currentOwner;
|
|
||||||
runner.depth = activeEffect ? activeEffect.depth + 1 : 0;
|
|
||||||
runWithContext(runner, () => {
|
|
||||||
currentOwner = { cleanups: runner._cleanups };
|
|
||||||
if (isExplicit) {
|
|
||||||
runWithContext(null, callback);
|
|
||||||
target.forEach((dep) => isFunc(dep) && dep());
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
currentOwner = previousOwner;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
assign(runner, {
|
|
||||||
_deps: new Set,
|
|
||||||
_cleanups: new Set,
|
|
||||||
_deleted: false,
|
|
||||||
stop: () => {
|
|
||||||
if (runner._deleted)
|
|
||||||
return;
|
|
||||||
runner._deleted = true;
|
|
||||||
effectQueue.delete(runner);
|
|
||||||
runner._deps.forEach((dep) => dep.delete(runner));
|
|
||||||
runner._cleanups.forEach((cleanup) => cleanup());
|
|
||||||
if (owner)
|
|
||||||
owner.cleanups.delete(runner.stop);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (owner)
|
|
||||||
owner.cleanups.add(runner.stop);
|
|
||||||
runner();
|
|
||||||
return runner.stop;
|
|
||||||
};
|
|
||||||
var Tag = (tag, props = {}, children = []) => {
|
|
||||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
|
||||||
children = props;
|
|
||||||
props = {};
|
|
||||||
}
|
|
||||||
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
|
|
||||||
const element = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : createEl(tag);
|
|
||||||
element._cleanups = new Set;
|
|
||||||
element.onUnmount = (fn) => element._cleanups.add(fn);
|
|
||||||
const booleanAttributes = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
|
||||||
const updateAttribute = (name, value) => {
|
|
||||||
const sanitized = (name === "src" || name === "href") && String(value).toLowerCase().includes("javascript:") ? "#" : value;
|
|
||||||
if (booleanAttributes.includes(name)) {
|
|
||||||
element[name] = !!sanitized;
|
|
||||||
sanitized ? element.setAttribute(name, "") : element.removeAttribute(name);
|
|
||||||
} else {
|
|
||||||
sanitized == null ? element.removeAttribute(name) : element.setAttribute(name, sanitized);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (let [key, value] of Object.entries(props)) {
|
|
||||||
if (key === "ref") {
|
|
||||||
isFunc(value) ? value(element) : value.current = element;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const isSignal = isFunc(value);
|
|
||||||
if (key.startsWith("on")) {
|
|
||||||
const eventName = key.slice(2).toLowerCase().split(".")[0];
|
|
||||||
element.addEventListener(eventName, value);
|
|
||||||
element._cleanups.add(() => element.removeEventListener(eventName, value));
|
|
||||||
} else if (isSignal) {
|
|
||||||
element._cleanups.add(Watch(() => {
|
|
||||||
const currentVal = value();
|
|
||||||
key === "class" ? element.className = currentVal || "" : updateAttribute(key, currentVal);
|
|
||||||
}));
|
|
||||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName) && (key === "value" || key === "checked")) {
|
|
||||||
const event = key === "checked" ? "change" : "input";
|
|
||||||
const handler = (e) => value(e.target[key]);
|
|
||||||
element.addEventListener(event, handler);
|
|
||||||
element._cleanups.add(() => element.removeEventListener(event, handler));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateAttribute(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const appendChildNode = (child) => {
|
|
||||||
if (isArr(child))
|
|
||||||
return child.forEach(appendChildNode);
|
|
||||||
if (isFunc(child)) {
|
|
||||||
const marker = createText("");
|
|
||||||
element.appendChild(marker);
|
|
||||||
let currentNodes = [];
|
|
||||||
element._cleanups.add(Watch(() => {
|
|
||||||
const result = child();
|
|
||||||
const nextNodes = (isArr(result) ? result : [result]).map((node) => node?._isRuntime ? node.container : node instanceof Node ? node : createText(node));
|
|
||||||
currentNodes.forEach((node) => {
|
|
||||||
cleanupNode(node);
|
|
||||||
node.remove();
|
|
||||||
});
|
|
||||||
nextNodes.forEach((node) => marker.parentNode?.insertBefore(node, marker));
|
|
||||||
currentNodes = nextNodes;
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
element.appendChild(child instanceof Node ? child : createText(child));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
appendChildNode(children);
|
|
||||||
return element;
|
|
||||||
};
|
|
||||||
var If = (condition, thenVal, otherwiseVal = null, transition = null) => {
|
|
||||||
const marker = createText("");
|
|
||||||
const container = Tag("div", { style: "display:contents" }, [marker]);
|
|
||||||
let currentView = null, lastState = null;
|
|
||||||
Watch(() => {
|
|
||||||
const state = !!(isFunc(condition) ? condition() : condition);
|
|
||||||
if (state === lastState)
|
|
||||||
return;
|
|
||||||
lastState = state;
|
|
||||||
const dispose = () => {
|
|
||||||
if (currentView)
|
|
||||||
currentView.destroy();
|
|
||||||
currentView = null;
|
currentView = null;
|
||||||
};
|
|
||||||
if (currentView && !state && transition?.out) {
|
|
||||||
transition.out(currentView.container, dispose);
|
|
||||||
} else {
|
|
||||||
dispose();
|
|
||||||
}
|
}
|
||||||
const branch = state ? thenVal : otherwiseVal;
|
const content = show ? ifYes : ifNot;
|
||||||
if (branch) {
|
if (content) {
|
||||||
currentView = Render(() => isFunc(branch) ? branch() : branch);
|
currentView = Render(() => isFunc(content) ? content() : content);
|
||||||
container.insertBefore(currentView.container, marker);
|
root.insertBefore(currentView.container, anchor);
|
||||||
if (state && transition?.in)
|
|
||||||
transition.in(currentView.container);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return container;
|
onUnmount(() => currentView?.destroy());
|
||||||
|
return root;
|
||||||
};
|
};
|
||||||
var For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
|
var For = (src, itemFn, keyFn) => {
|
||||||
const marker = createText("");
|
const anchor = doc.createTextNode("");
|
||||||
const container = Tag(tag, props, [marker]);
|
const root = Tag("div", { style: "display:contents" }, [anchor]);
|
||||||
let viewCache = new Map;
|
let cache = new Map;
|
||||||
Watch(() => {
|
Watch(() => (isFunc(src) ? src() : src) || [], (items) => {
|
||||||
const items = (isFunc(source) ? source() : source) || [];
|
|
||||||
const nextCache = new Map;
|
const nextCache = new Map;
|
||||||
const order = [];
|
const nextOrder = [];
|
||||||
for (let i = 0;i < items.length; i++) {
|
const newItems = items || [];
|
||||||
const item = items[i];
|
for (let i = 0;i < newItems.length; i++) {
|
||||||
const key = keyFn ? keyFn(item, i) : i;
|
const item = newItems[i];
|
||||||
let view = viewCache.get(key);
|
const key = keyFn ? keyFn(item, i) : item?.id ?? i;
|
||||||
if (!view) {
|
let view = cache.get(key);
|
||||||
const result = renderFn(item, i);
|
if (!view)
|
||||||
view = result instanceof Node ? { container: result, destroy: () => {
|
view = Render(() => itemFn(item, i));
|
||||||
cleanupNode(result);
|
else
|
||||||
result.remove();
|
cache.delete(key);
|
||||||
} } : Render(() => result);
|
|
||||||
}
|
|
||||||
viewCache.delete(key);
|
|
||||||
nextCache.set(key, view);
|
nextCache.set(key, view);
|
||||||
order.push(key);
|
nextOrder.push(view);
|
||||||
}
|
}
|
||||||
viewCache.forEach((v) => v.destroy());
|
cache.forEach((view) => view.destroy());
|
||||||
let anchor = marker;
|
let lastRef = anchor;
|
||||||
for (let i = order.length - 1;i >= 0; i--) {
|
for (let i = nextOrder.length - 1;i >= 0; i--) {
|
||||||
const view = nextCache.get(order[i]);
|
const view = nextOrder[i];
|
||||||
if (view.container.nextSibling !== anchor) {
|
const node = view.container;
|
||||||
container.insertBefore(view.container, anchor);
|
if (node.nextSibling !== lastRef)
|
||||||
}
|
root.insertBefore(node, lastRef);
|
||||||
anchor = view.container;
|
lastRef = node;
|
||||||
}
|
}
|
||||||
viewCache = nextCache;
|
cache = nextCache;
|
||||||
});
|
});
|
||||||
return container;
|
return root;
|
||||||
};
|
};
|
||||||
var Router = (routes) => {
|
var Router = (routes) => {
|
||||||
const currentPath = $(window.location.hash.replace(/^#/, "") || "/");
|
const getHash = () => window.location.hash.slice(1) || "/";
|
||||||
window.addEventListener("hashchange", () => currentPath(window.location.hash.replace(/^#/, "") || "/"));
|
const path = $(getHash());
|
||||||
const outlet = Tag("div", { class: "router-transition" });
|
const handler = () => path(getHash());
|
||||||
|
window.addEventListener("hashchange", handler);
|
||||||
|
onUnmount(() => window.removeEventListener("hashchange", handler));
|
||||||
|
const hook = Tag("div", { class: "router-hook" });
|
||||||
let currentView = null;
|
let currentView = null;
|
||||||
Watch([currentPath], async () => {
|
Watch([path], () => {
|
||||||
const path = currentPath();
|
const cur = path();
|
||||||
const route = routes.find((r) => {
|
const route = routes.find((r) => {
|
||||||
const routeParts = r.path.split("/").filter(Boolean);
|
const p1 = r.path.split("/").filter(Boolean);
|
||||||
const pathParts = path.split("/").filter(Boolean);
|
const p2 = cur.split("/").filter(Boolean);
|
||||||
return routeParts.length === pathParts.length && routeParts.every((part, i) => part.startsWith(":") || part === pathParts[i]);
|
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]);
|
||||||
}) || routes.find((r) => r.path === "*");
|
}) || routes.find((r) => r.path === "*");
|
||||||
if (route) {
|
if (route) {
|
||||||
let component = route.component;
|
currentView?.destroy();
|
||||||
if (isFunc(component) && component.toString().includes("import")) {
|
|
||||||
component = (await component()).default || await component();
|
|
||||||
}
|
|
||||||
const params = {};
|
const params = {};
|
||||||
route.path.split("/").filter(Boolean).forEach((part, i) => {
|
route.path.split("/").filter(Boolean).forEach((p, i) => {
|
||||||
if (part.startsWith(":"))
|
if (p[0] === ":")
|
||||||
params[part.slice(1)] = path.split("/").filter(Boolean)[i];
|
params[p.slice(1)] = cur.split("/").filter(Boolean)[i];
|
||||||
});
|
});
|
||||||
if (currentView)
|
Router.params(params);
|
||||||
currentView.destroy();
|
currentView = Render(() => isFunc(route.component) ? route.component(params) : route.component);
|
||||||
if (Router.params)
|
hook.replaceChildren(currentView.container);
|
||||||
Router.params(params);
|
|
||||||
currentView = Render(() => {
|
|
||||||
try {
|
|
||||||
return isFunc(component) ? component(params) : component;
|
|
||||||
} catch (e) {
|
|
||||||
return Tag("div", { class: "p-4 text-error" }, "Error loading view");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
outlet.appendChild(currentView.container);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return outlet;
|
return hook;
|
||||||
};
|
};
|
||||||
Router.params = $({});
|
Router.params = $({});
|
||||||
Router.to = (path) => window.location.hash = path.replace(/^#?\/?/, "#/");
|
Router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||||
Router.back = () => window.history.back();
|
Router.back = () => window.history.back();
|
||||||
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||||
var Mount = (component, target) => {
|
var Mount = (comp, target) => {
|
||||||
const targetEl = typeof target === "string" ? doc.querySelector(target) : target;
|
const t = typeof target === "string" ? doc.querySelector(target) : target;
|
||||||
if (!targetEl)
|
if (!t)
|
||||||
return;
|
return;
|
||||||
if (MOUNTED_NODES.has(targetEl))
|
if (MOUNTED_NODES.has(t))
|
||||||
MOUNTED_NODES.get(targetEl).destroy();
|
MOUNTED_NODES.get(t).destroy();
|
||||||
const instance = Render(isFunc(component) ? component : () => component);
|
const inst = Render(isFunc(comp) ? comp : () => comp);
|
||||||
targetEl.replaceChildren(instance.container);
|
t.replaceChildren(inst.container);
|
||||||
MOUNTED_NODES.set(targetEl, instance);
|
MOUNTED_NODES.set(t, inst);
|
||||||
return instance;
|
return inst;
|
||||||
};
|
};
|
||||||
var SigPro = { $, $$, Render, Watch, Tag, If, For, Router, Mount };
|
var SigPro = Object.freeze({ $, $$, Watch, Tag, Render, If, For, Router, Mount, onMount, onUnmount, Batch });
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
assign(window, SigPro);
|
Object.assign(window, SigPro);
|
||||||
const tags = `div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(" ");
|
"div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg".split(" ").forEach((t) => window[t[0].toUpperCase() + t.slice(1)] = (p, c) => SigPro.Tag(t, p, c));
|
||||||
tags.forEach((tag) => {
|
|
||||||
const helper = tag[0].toUpperCase() + tag.slice(1);
|
|
||||||
if (!(helper in window))
|
|
||||||
window[helper] = (p, c) => Tag(tag, p, c);
|
|
||||||
});
|
|
||||||
window.SigPro = Object.freeze(SigPro);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
2
dist/sigpro.min.js
vendored
2
dist/sigpro.min.js
vendored
File diff suppressed because one or more lines are too long
809
docs/sigpro.js
809
docs/sigpro.js
@@ -30,6 +30,8 @@
|
|||||||
// index.js
|
// index.js
|
||||||
var exports_sigpro = {};
|
var exports_sigpro = {};
|
||||||
__export(exports_sigpro, {
|
__export(exports_sigpro, {
|
||||||
|
onUnmount: () => onUnmount,
|
||||||
|
onMount: () => onMount,
|
||||||
Watch: () => Watch,
|
Watch: () => Watch,
|
||||||
Tag: () => Tag,
|
Tag: () => Tag,
|
||||||
Router: () => Router,
|
Router: () => Router,
|
||||||
@@ -37,80 +39,407 @@
|
|||||||
Mount: () => Mount,
|
Mount: () => Mount,
|
||||||
If: () => If,
|
If: () => If,
|
||||||
For: () => For,
|
For: () => For,
|
||||||
|
Batch: () => Batch,
|
||||||
$$: () => $$,
|
$$: () => $$,
|
||||||
$: () => $
|
$: () => $
|
||||||
});
|
});
|
||||||
|
|
||||||
// sigpro.js
|
// sigpro.js
|
||||||
var activeEffect = null;
|
|
||||||
var currentOwner = null;
|
|
||||||
var effectQueue = new Set;
|
|
||||||
var isFlushing = false;
|
|
||||||
var MOUNTED_NODES = new WeakMap;
|
|
||||||
var doc = document;
|
|
||||||
var isArr = Array.isArray;
|
|
||||||
var assign = Object.assign;
|
|
||||||
var createEl = (t) => doc.createElement(t);
|
|
||||||
var createText = (t) => doc.createTextNode(String(t ?? ""));
|
|
||||||
var isFunc = (f) => typeof f === "function";
|
var isFunc = (f) => typeof f === "function";
|
||||||
var isObj = (o) => typeof o === "object" && o !== null;
|
var isObj = (o) => o && typeof o === "object";
|
||||||
var runWithContext = (effect, callback) => {
|
var isArr = Array.isArray;
|
||||||
const previousEffect = activeEffect;
|
var doc = typeof document !== "undefined" ? document : null;
|
||||||
activeEffect = effect;
|
var ensureNode = (n) => n?._isRuntime ? n.container : n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n));
|
||||||
|
var activeEffect = null;
|
||||||
|
var activeOwner = null;
|
||||||
|
var isFlushing = false;
|
||||||
|
var batchDepth = 0;
|
||||||
|
var effectQueue = new Set;
|
||||||
|
var proxyCache = new WeakMap;
|
||||||
|
var ITER = Symbol("iter");
|
||||||
|
var MOUNTED_NODES = new WeakMap;
|
||||||
|
var dispose = (eff) => {
|
||||||
|
if (!eff || eff._disposed)
|
||||||
|
return;
|
||||||
|
eff._disposed = true;
|
||||||
|
const stack = [eff];
|
||||||
|
while (stack.length) {
|
||||||
|
const e = stack.pop();
|
||||||
|
if (e._cleanups) {
|
||||||
|
e._cleanups.forEach((fn) => fn());
|
||||||
|
e._cleanups.clear();
|
||||||
|
}
|
||||||
|
if (e._children) {
|
||||||
|
e._children.forEach((child) => stack.push(child));
|
||||||
|
e._children.clear();
|
||||||
|
}
|
||||||
|
if (e._deps) {
|
||||||
|
e._deps.forEach((depSet) => depSet.delete(e));
|
||||||
|
e._deps.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var onMount = (fn) => {
|
||||||
|
if (activeOwner)
|
||||||
|
(activeOwner._mounts ||= []).push(fn);
|
||||||
|
};
|
||||||
|
var onUnmount = (fn) => {
|
||||||
|
if (activeOwner)
|
||||||
|
(activeOwner._cleanups ||= new Set).add(fn);
|
||||||
|
};
|
||||||
|
var untrack = (fn) => {
|
||||||
|
const p = activeEffect;
|
||||||
|
activeEffect = null;
|
||||||
try {
|
try {
|
||||||
return callback();
|
return fn();
|
||||||
} finally {
|
} finally {
|
||||||
activeEffect = previousEffect;
|
activeEffect = p;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var cleanupNode = (node) => {
|
var createEffect = (fn, isComputed = false) => {
|
||||||
if (node._cleanups) {
|
const effect = () => {
|
||||||
node._cleanups.forEach((dispose) => dispose());
|
if (effect._disposed)
|
||||||
node._cleanups.clear();
|
return;
|
||||||
}
|
if (effect._deps)
|
||||||
node.childNodes?.forEach(cleanupNode);
|
effect._deps.forEach((s) => s.delete(effect));
|
||||||
|
if (effect._cleanups) {
|
||||||
|
effect._cleanups.forEach((c) => c());
|
||||||
|
effect._cleanups.clear();
|
||||||
|
}
|
||||||
|
const prevEffect = activeEffect;
|
||||||
|
const prevOwner = activeOwner;
|
||||||
|
activeEffect = activeOwner = effect;
|
||||||
|
try {
|
||||||
|
return effect._result = fn();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[SigPro]", e);
|
||||||
|
} finally {
|
||||||
|
activeEffect = prevEffect;
|
||||||
|
activeOwner = prevOwner;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
effect._deps = effect._cleanups = effect._children = null;
|
||||||
|
effect._disposed = false;
|
||||||
|
effect._isComputed = isComputed;
|
||||||
|
effect._depth = activeEffect ? activeEffect._depth + 1 : 0;
|
||||||
|
effect._mounts = [];
|
||||||
|
effect._parent = activeOwner;
|
||||||
|
if (activeOwner)
|
||||||
|
(activeOwner._children ||= new Set).add(effect);
|
||||||
|
return effect;
|
||||||
};
|
};
|
||||||
var flushEffects = () => {
|
var flush = () => {
|
||||||
if (isFlushing)
|
if (isFlushing)
|
||||||
return;
|
return;
|
||||||
isFlushing = true;
|
isFlushing = true;
|
||||||
while (effectQueue.size > 0) {
|
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth);
|
||||||
const sortedEffects = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
|
effectQueue.clear();
|
||||||
effectQueue.clear();
|
for (const e of sorted)
|
||||||
for (const effect of sortedEffects) {
|
if (!e._disposed)
|
||||||
if (!effect._deleted)
|
e();
|
||||||
effect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isFlushing = false;
|
isFlushing = false;
|
||||||
};
|
};
|
||||||
var trackSubscription = (subscribers) => {
|
var Batch = (fn) => {
|
||||||
if (activeEffect && !activeEffect._deleted) {
|
batchDepth++;
|
||||||
subscribers.add(activeEffect);
|
try {
|
||||||
activeEffect._deps.add(subscribers);
|
return fn();
|
||||||
|
} finally {
|
||||||
|
batchDepth--;
|
||||||
|
if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) {
|
||||||
|
flush();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var triggerUpdate = (subscribers) => {
|
var trackUpdate = (subs, trigger = false) => {
|
||||||
subscribers.forEach((effect) => {
|
if (!trigger && activeEffect && !activeEffect._disposed) {
|
||||||
if (effect === activeEffect || effect._deleted)
|
subs.add(activeEffect);
|
||||||
return;
|
(activeEffect._deps ||= new Set).add(subs);
|
||||||
if (effect._isComputed) {
|
} else if (trigger) {
|
||||||
effect.markDirty();
|
let hasQueue = false;
|
||||||
if (effect._subs)
|
subs.forEach((e) => {
|
||||||
triggerUpdate(effect._subs);
|
if (e === activeEffect || e._disposed)
|
||||||
} else {
|
return;
|
||||||
effectQueue.add(effect);
|
if (e._isComputed) {
|
||||||
|
e._dirty = true;
|
||||||
|
if (e._subs)
|
||||||
|
trackUpdate(e._subs, true);
|
||||||
|
} else {
|
||||||
|
effectQueue.add(e);
|
||||||
|
hasQueue = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (hasQueue && !isFlushing && batchDepth === 0)
|
||||||
|
queueMicrotask(flush);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var $ = (val, key = null) => {
|
||||||
|
const subs = new Set;
|
||||||
|
if (isFunc(val)) {
|
||||||
|
let cache, dirty = true;
|
||||||
|
const computed = () => {
|
||||||
|
if (dirty) {
|
||||||
|
const prev = activeEffect;
|
||||||
|
activeEffect = computed;
|
||||||
|
try {
|
||||||
|
const next = val();
|
||||||
|
if (!Object.is(cache, next)) {
|
||||||
|
cache = next;
|
||||||
|
dirty = false;
|
||||||
|
trackUpdate(subs, true);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
activeEffect = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackUpdate(subs);
|
||||||
|
return cache;
|
||||||
|
};
|
||||||
|
computed._isComputed = true;
|
||||||
|
computed._subs = subs;
|
||||||
|
computed._dirty = true;
|
||||||
|
computed._deps = null;
|
||||||
|
computed._disposed = false;
|
||||||
|
computed.markDirty = () => {
|
||||||
|
dirty = true;
|
||||||
|
};
|
||||||
|
computed.stop = () => {
|
||||||
|
computed._disposed = true;
|
||||||
|
if (computed._deps) {
|
||||||
|
computed._deps.forEach((depSet) => depSet.delete(computed));
|
||||||
|
computed._deps.clear();
|
||||||
|
}
|
||||||
|
subs.clear();
|
||||||
|
};
|
||||||
|
if (activeOwner)
|
||||||
|
onUnmount(computed.stop);
|
||||||
|
return computed;
|
||||||
|
}
|
||||||
|
if (key)
|
||||||
|
try {
|
||||||
|
val = JSON.parse(localStorage.getItem(key)) ?? val;
|
||||||
|
} catch (e) {}
|
||||||
|
return (...args) => {
|
||||||
|
if (args.length) {
|
||||||
|
const next = isFunc(args[0]) ? args[0](val) : args[0];
|
||||||
|
if (!Object.is(val, next)) {
|
||||||
|
val = next;
|
||||||
|
if (key)
|
||||||
|
localStorage.setItem(key, JSON.stringify(val));
|
||||||
|
trackUpdate(subs, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackUpdate(subs);
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
var $$ = (target) => {
|
||||||
|
if (!isObj(target))
|
||||||
|
return target;
|
||||||
|
if (proxyCache.has(target))
|
||||||
|
return proxyCache.get(target);
|
||||||
|
const subsMap = new Map;
|
||||||
|
const getSubs = (k) => {
|
||||||
|
let s = subsMap.get(k);
|
||||||
|
if (!s)
|
||||||
|
subsMap.set(k, s = new Set);
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
const proxy = new Proxy(target, {
|
||||||
|
get(t, k) {
|
||||||
|
trackUpdate(getSubs(k));
|
||||||
|
return $$(t[k]);
|
||||||
|
},
|
||||||
|
set(t, k, v) {
|
||||||
|
const isNew = !(k in t);
|
||||||
|
if (!Object.is(t[k], v)) {
|
||||||
|
t[k] = v;
|
||||||
|
trackUpdate(getSubs(k), true);
|
||||||
|
if (isNew)
|
||||||
|
trackUpdate(getSubs(ITER), true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deleteProperty(t, k) {
|
||||||
|
const res = Reflect.deleteProperty(t, k);
|
||||||
|
if (res) {
|
||||||
|
trackUpdate(getSubs(k), true);
|
||||||
|
trackUpdate(getSubs(ITER), true);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
ownKeys(t) {
|
||||||
|
trackUpdate(getSubs(ITER));
|
||||||
|
return Reflect.ownKeys(t);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!isFlushing)
|
proxyCache.set(target, proxy);
|
||||||
queueMicrotask(flushEffects);
|
return proxy;
|
||||||
|
};
|
||||||
|
var Watch = (sources, cb) => {
|
||||||
|
if (cb === undefined) {
|
||||||
|
const effect2 = createEffect(sources);
|
||||||
|
effect2();
|
||||||
|
return () => dispose(effect2);
|
||||||
|
}
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const vals = Array.isArray(sources) ? sources.map((s) => s()) : sources();
|
||||||
|
untrack(() => cb(vals));
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
return () => dispose(effect);
|
||||||
|
};
|
||||||
|
var cleanupNode = (node) => {
|
||||||
|
if (node._cleanups) {
|
||||||
|
node._cleanups.forEach((fn) => fn());
|
||||||
|
node._cleanups.clear();
|
||||||
|
}
|
||||||
|
if (node._ownerEffect)
|
||||||
|
dispose(node._ownerEffect);
|
||||||
|
if (node.childNodes)
|
||||||
|
node.childNodes.forEach(cleanupNode);
|
||||||
|
};
|
||||||
|
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||||
|
var isDangerousAttr = (key) => key === "src" || key === "href" || key.startsWith("on");
|
||||||
|
var validateAttr = (key, val) => {
|
||||||
|
if (val == null || val === false)
|
||||||
|
return null;
|
||||||
|
if (isDangerousAttr(key)) {
|
||||||
|
const sVal = String(val);
|
||||||
|
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
||||||
|
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`);
|
||||||
|
return "#";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
var Tag = (tag, props = {}, children = []) => {
|
||||||
|
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||||
|
children = props;
|
||||||
|
props = {};
|
||||||
|
}
|
||||||
|
if (isFunc(tag)) {
|
||||||
|
const ctx = { _mounts: [], _cleanups: new Set };
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const result2 = tag(props, {
|
||||||
|
children,
|
||||||
|
emit: (ev, ...args) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...args)
|
||||||
|
});
|
||||||
|
effect._result = result2;
|
||||||
|
return result2;
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
const result = effect._result;
|
||||||
|
if (result == null)
|
||||||
|
return null;
|
||||||
|
const node = result instanceof Node || isArr(result) && result.every((n) => n instanceof Node) ? result : doc.createTextNode(String(result));
|
||||||
|
const attach = (n) => {
|
||||||
|
if (isObj(n) && !n._isRuntime) {
|
||||||
|
n._mounts = effect._mounts || [];
|
||||||
|
n._cleanups = effect._cleanups || new Set;
|
||||||
|
n._ownerEffect = effect;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
isArr(node) ? node.forEach(attach) : attach(node);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
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 in props) {
|
||||||
|
if (!props.hasOwnProperty(k))
|
||||||
|
continue;
|
||||||
|
let v = props[k];
|
||||||
|
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);
|
||||||
|
const off = () => el.removeEventListener(ev, v);
|
||||||
|
el._cleanups.add(off);
|
||||||
|
onUnmount(off);
|
||||||
|
} else if (isFunc(v)) {
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const val = validateAttr(k, v());
|
||||||
|
if (k === "class")
|
||||||
|
el.className = val || "";
|
||||||
|
else if (val == null)
|
||||||
|
el.removeAttribute(k);
|
||||||
|
else if (k in el && !isSVG)
|
||||||
|
el[k] = val;
|
||||||
|
else
|
||||||
|
el.setAttribute(k, val === true ? "" : val);
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
el._cleanups.add(() => dispose(effect));
|
||||||
|
onUnmount(() => dispose(effect));
|
||||||
|
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||||
|
const evType = k === "checked" ? "change" : "input";
|
||||||
|
el.addEventListener(evType, (ev) => v(ev.target[k]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const val = validateAttr(k, v);
|
||||||
|
if (val != null) {
|
||||||
|
if (k in el && !isSVG)
|
||||||
|
el[k] = val;
|
||||||
|
else
|
||||||
|
el.setAttribute(k, val === true ? "" : val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const append = (c) => {
|
||||||
|
if (isArr(c))
|
||||||
|
return c.forEach(append);
|
||||||
|
if (isFunc(c)) {
|
||||||
|
const anchor = doc.createTextNode("");
|
||||||
|
el.appendChild(anchor);
|
||||||
|
let currentNodes = [];
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const res = c();
|
||||||
|
const next = (isArr(res) ? res : [res]).map(ensureNode);
|
||||||
|
currentNodes.forEach((n) => {
|
||||||
|
if (n._isRuntime)
|
||||||
|
n.destroy();
|
||||||
|
else
|
||||||
|
cleanupNode(n);
|
||||||
|
if (n.parentNode)
|
||||||
|
n.remove();
|
||||||
|
});
|
||||||
|
let ref = anchor;
|
||||||
|
for (let i = next.length - 1;i >= 0; i--) {
|
||||||
|
const node = next[i];
|
||||||
|
if (node.parentNode !== ref.parentNode)
|
||||||
|
ref.parentNode?.insertBefore(node, ref);
|
||||||
|
if (node._mounts)
|
||||||
|
node._mounts.forEach((fn) => fn());
|
||||||
|
ref = node;
|
||||||
|
}
|
||||||
|
currentNodes = next;
|
||||||
|
});
|
||||||
|
effect();
|
||||||
|
el._cleanups.add(() => dispose(effect));
|
||||||
|
onUnmount(() => dispose(effect));
|
||||||
|
} else {
|
||||||
|
const node = ensureNode(c);
|
||||||
|
el.appendChild(node);
|
||||||
|
if (node._mounts)
|
||||||
|
node._mounts.forEach((fn) => fn());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
append(children);
|
||||||
|
return el;
|
||||||
};
|
};
|
||||||
var Render = (renderFn) => {
|
var Render = (renderFn) => {
|
||||||
const cleanups = new Set;
|
const cleanups = new Set;
|
||||||
const previousOwner = currentOwner;
|
const mounts = [];
|
||||||
const container = createEl("div");
|
const previousOwner = activeOwner;
|
||||||
|
const previousEffect = activeEffect;
|
||||||
|
const container = doc.createElement("div");
|
||||||
container.style.display = "contents";
|
container.style.display = "contents";
|
||||||
currentOwner = { cleanups };
|
container.setAttribute("role", "presentation");
|
||||||
|
activeOwner = { _cleanups: cleanups, _mounts: mounts };
|
||||||
|
activeEffect = null;
|
||||||
const processResult = (result) => {
|
const processResult = (result) => {
|
||||||
if (!result)
|
if (!result)
|
||||||
return;
|
return;
|
||||||
@@ -120,14 +449,16 @@
|
|||||||
} else if (isArr(result)) {
|
} else if (isArr(result)) {
|
||||||
result.forEach(processResult);
|
result.forEach(processResult);
|
||||||
} else {
|
} else {
|
||||||
container.appendChild(result instanceof Node ? result : createText(result));
|
container.appendChild(result instanceof Node ? result : doc.createTextNode(String(result == null ? "" : result)));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
||||||
} finally {
|
} finally {
|
||||||
currentOwner = previousOwner;
|
activeOwner = previousOwner;
|
||||||
|
activeEffect = previousEffect;
|
||||||
}
|
}
|
||||||
|
mounts.forEach((fn) => fn());
|
||||||
return {
|
return {
|
||||||
_isRuntime: true,
|
_isRuntime: true,
|
||||||
container,
|
container,
|
||||||
@@ -138,335 +469,103 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
var $ = (initialValue, storageKey = null) => {
|
var If = (cond, ifYes, ifNot = null) => {
|
||||||
const subscribers = new Set;
|
const anchor = doc.createTextNode("");
|
||||||
if (isFunc(initialValue)) {
|
const root = Tag("div", { style: "display:contents" }, [anchor]);
|
||||||
let cachedValue, isDirty = true;
|
let currentView = null;
|
||||||
const effect = () => {
|
Watch(() => !!(isFunc(cond) ? cond() : cond), (show) => {
|
||||||
if (effect._deleted)
|
if (currentView) {
|
||||||
return;
|
currentView.destroy();
|
||||||
effect._deps.forEach((dep) => dep.delete(effect));
|
|
||||||
effect._deps.clear();
|
|
||||||
runWithContext(effect, () => {
|
|
||||||
const newValue = initialValue();
|
|
||||||
if (!Object.is(cachedValue, newValue) || isDirty) {
|
|
||||||
cachedValue = newValue;
|
|
||||||
isDirty = false;
|
|
||||||
triggerUpdate(subscribers);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
assign(effect, {
|
|
||||||
_deps: new Set,
|
|
||||||
_isComputed: true,
|
|
||||||
_subs: subscribers,
|
|
||||||
_deleted: false,
|
|
||||||
markDirty: () => isDirty = true,
|
|
||||||
stop: () => {
|
|
||||||
effect._deleted = true;
|
|
||||||
effect._deps.forEach((dep) => dep.delete(effect));
|
|
||||||
subscribers.clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (currentOwner)
|
|
||||||
currentOwner.cleanups.add(effect.stop);
|
|
||||||
return () => {
|
|
||||||
if (isDirty)
|
|
||||||
effect();
|
|
||||||
trackSubscription(subscribers);
|
|
||||||
return cachedValue;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
let value = initialValue;
|
|
||||||
if (storageKey) {
|
|
||||||
try {
|
|
||||||
const saved = localStorage.getItem(storageKey);
|
|
||||||
if (saved !== null)
|
|
||||||
value = JSON.parse(saved);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("SigPro Storage Lock", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (...args) => {
|
|
||||||
if (args.length) {
|
|
||||||
const nextValue = isFunc(args[0]) ? args[0](value) : args[0];
|
|
||||||
if (!Object.is(value, nextValue)) {
|
|
||||||
value = nextValue;
|
|
||||||
if (storageKey)
|
|
||||||
localStorage.setItem(storageKey, JSON.stringify(value));
|
|
||||||
triggerUpdate(subscribers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trackSubscription(subscribers);
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
var $$ = (object, cache = new WeakMap) => {
|
|
||||||
if (!isObj(object))
|
|
||||||
return object;
|
|
||||||
if (cache.has(object))
|
|
||||||
return cache.get(object);
|
|
||||||
const keySubscribers = {};
|
|
||||||
const proxy = new Proxy(object, {
|
|
||||||
get(target, key) {
|
|
||||||
if (activeEffect)
|
|
||||||
trackSubscription(keySubscribers[key] ??= new Set);
|
|
||||||
const value = Reflect.get(target, key);
|
|
||||||
return isObj(value) ? $$(value, cache) : value;
|
|
||||||
},
|
|
||||||
set(target, key, value) {
|
|
||||||
if (Object.is(target[key], value))
|
|
||||||
return true;
|
|
||||||
const success = Reflect.set(target, key, value);
|
|
||||||
if (keySubscribers[key])
|
|
||||||
triggerUpdate(keySubscribers[key]);
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
cache.set(object, proxy);
|
|
||||||
return proxy;
|
|
||||||
};
|
|
||||||
var Watch = (target, callbackFn) => {
|
|
||||||
const isExplicit = isArr(target);
|
|
||||||
const callback = isExplicit ? callbackFn : target;
|
|
||||||
if (!isFunc(callback))
|
|
||||||
return () => {};
|
|
||||||
const owner = currentOwner;
|
|
||||||
const runner = () => {
|
|
||||||
if (runner._deleted)
|
|
||||||
return;
|
|
||||||
runner._deps.forEach((dep) => dep.delete(runner));
|
|
||||||
runner._deps.clear();
|
|
||||||
runner._cleanups.forEach((cleanup) => cleanup());
|
|
||||||
runner._cleanups.clear();
|
|
||||||
const previousOwner = currentOwner;
|
|
||||||
runner.depth = activeEffect ? activeEffect.depth + 1 : 0;
|
|
||||||
runWithContext(runner, () => {
|
|
||||||
currentOwner = { cleanups: runner._cleanups };
|
|
||||||
if (isExplicit) {
|
|
||||||
runWithContext(null, callback);
|
|
||||||
target.forEach((dep) => isFunc(dep) && dep());
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
currentOwner = previousOwner;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
assign(runner, {
|
|
||||||
_deps: new Set,
|
|
||||||
_cleanups: new Set,
|
|
||||||
_deleted: false,
|
|
||||||
stop: () => {
|
|
||||||
if (runner._deleted)
|
|
||||||
return;
|
|
||||||
runner._deleted = true;
|
|
||||||
effectQueue.delete(runner);
|
|
||||||
runner._deps.forEach((dep) => dep.delete(runner));
|
|
||||||
runner._cleanups.forEach((cleanup) => cleanup());
|
|
||||||
if (owner)
|
|
||||||
owner.cleanups.delete(runner.stop);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (owner)
|
|
||||||
owner.cleanups.add(runner.stop);
|
|
||||||
runner();
|
|
||||||
return runner.stop;
|
|
||||||
};
|
|
||||||
var Tag = (tag, props = {}, children = []) => {
|
|
||||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
|
||||||
children = props;
|
|
||||||
props = {};
|
|
||||||
}
|
|
||||||
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
|
|
||||||
const element = isSVG ? doc.createElementNS("http://www.w3.org/2000/svg", tag) : createEl(tag);
|
|
||||||
element._cleanups = new Set;
|
|
||||||
element.onUnmount = (fn) => element._cleanups.add(fn);
|
|
||||||
const booleanAttributes = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
|
||||||
const updateAttribute = (name, value) => {
|
|
||||||
const sanitized = (name === "src" || name === "href") && String(value).toLowerCase().includes("javascript:") ? "#" : value;
|
|
||||||
if (booleanAttributes.includes(name)) {
|
|
||||||
element[name] = !!sanitized;
|
|
||||||
sanitized ? element.setAttribute(name, "") : element.removeAttribute(name);
|
|
||||||
} else {
|
|
||||||
sanitized == null ? element.removeAttribute(name) : element.setAttribute(name, sanitized);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
for (let [key, value] of Object.entries(props)) {
|
|
||||||
if (key === "ref") {
|
|
||||||
isFunc(value) ? value(element) : value.current = element;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const isSignal = isFunc(value);
|
|
||||||
if (key.startsWith("on")) {
|
|
||||||
const eventName = key.slice(2).toLowerCase().split(".")[0];
|
|
||||||
element.addEventListener(eventName, value);
|
|
||||||
element._cleanups.add(() => element.removeEventListener(eventName, value));
|
|
||||||
} else if (isSignal) {
|
|
||||||
element._cleanups.add(Watch(() => {
|
|
||||||
const currentVal = value();
|
|
||||||
key === "class" ? element.className = currentVal || "" : updateAttribute(key, currentVal);
|
|
||||||
}));
|
|
||||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName) && (key === "value" || key === "checked")) {
|
|
||||||
const event = key === "checked" ? "change" : "input";
|
|
||||||
const handler = (e) => value(e.target[key]);
|
|
||||||
element.addEventListener(event, handler);
|
|
||||||
element._cleanups.add(() => element.removeEventListener(event, handler));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateAttribute(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const appendChildNode = (child) => {
|
|
||||||
if (isArr(child))
|
|
||||||
return child.forEach(appendChildNode);
|
|
||||||
if (isFunc(child)) {
|
|
||||||
const marker = createText("");
|
|
||||||
element.appendChild(marker);
|
|
||||||
let currentNodes = [];
|
|
||||||
element._cleanups.add(Watch(() => {
|
|
||||||
const result = child();
|
|
||||||
const nextNodes = (isArr(result) ? result : [result]).map((node) => node?._isRuntime ? node.container : node instanceof Node ? node : createText(node));
|
|
||||||
currentNodes.forEach((node) => {
|
|
||||||
cleanupNode(node);
|
|
||||||
node.remove();
|
|
||||||
});
|
|
||||||
nextNodes.forEach((node) => marker.parentNode?.insertBefore(node, marker));
|
|
||||||
currentNodes = nextNodes;
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
element.appendChild(child instanceof Node ? child : createText(child));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
appendChildNode(children);
|
|
||||||
return element;
|
|
||||||
};
|
|
||||||
var If = (condition, thenVal, otherwiseVal = null, transition = null) => {
|
|
||||||
const marker = createText("");
|
|
||||||
const container = Tag("div", { style: "display:contents" }, [marker]);
|
|
||||||
let currentView = null, lastState = null;
|
|
||||||
Watch(() => {
|
|
||||||
const state = !!(isFunc(condition) ? condition() : condition);
|
|
||||||
if (state === lastState)
|
|
||||||
return;
|
|
||||||
lastState = state;
|
|
||||||
const dispose = () => {
|
|
||||||
if (currentView)
|
|
||||||
currentView.destroy();
|
|
||||||
currentView = null;
|
currentView = null;
|
||||||
};
|
|
||||||
if (currentView && !state && transition?.out) {
|
|
||||||
transition.out(currentView.container, dispose);
|
|
||||||
} else {
|
|
||||||
dispose();
|
|
||||||
}
|
}
|
||||||
const branch = state ? thenVal : otherwiseVal;
|
const content = show ? ifYes : ifNot;
|
||||||
if (branch) {
|
if (content) {
|
||||||
currentView = Render(() => isFunc(branch) ? branch() : branch);
|
currentView = Render(() => isFunc(content) ? content() : content);
|
||||||
container.insertBefore(currentView.container, marker);
|
root.insertBefore(currentView.container, anchor);
|
||||||
if (state && transition?.in)
|
|
||||||
transition.in(currentView.container);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return container;
|
onUnmount(() => currentView?.destroy());
|
||||||
|
return root;
|
||||||
};
|
};
|
||||||
var For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
|
var For = (src, itemFn, keyFn) => {
|
||||||
const marker = createText("");
|
const anchor = doc.createTextNode("");
|
||||||
const container = Tag(tag, props, [marker]);
|
const root = Tag("div", { style: "display:contents" }, [anchor]);
|
||||||
let viewCache = new Map;
|
let cache = new Map;
|
||||||
Watch(() => {
|
Watch(() => (isFunc(src) ? src() : src) || [], (items) => {
|
||||||
const items = (isFunc(source) ? source() : source) || [];
|
|
||||||
const nextCache = new Map;
|
const nextCache = new Map;
|
||||||
const order = [];
|
const nextOrder = [];
|
||||||
for (let i = 0;i < items.length; i++) {
|
const newItems = items || [];
|
||||||
const item = items[i];
|
for (let i = 0;i < newItems.length; i++) {
|
||||||
const key = keyFn ? keyFn(item, i) : i;
|
const item = newItems[i];
|
||||||
let view = viewCache.get(key);
|
const key = keyFn ? keyFn(item, i) : item?.id ?? i;
|
||||||
if (!view) {
|
let view = cache.get(key);
|
||||||
const result = renderFn(item, i);
|
if (!view)
|
||||||
view = result instanceof Node ? { container: result, destroy: () => {
|
view = Render(() => itemFn(item, i));
|
||||||
cleanupNode(result);
|
else
|
||||||
result.remove();
|
cache.delete(key);
|
||||||
} } : Render(() => result);
|
|
||||||
}
|
|
||||||
viewCache.delete(key);
|
|
||||||
nextCache.set(key, view);
|
nextCache.set(key, view);
|
||||||
order.push(key);
|
nextOrder.push(view);
|
||||||
}
|
}
|
||||||
viewCache.forEach((v) => v.destroy());
|
cache.forEach((view) => view.destroy());
|
||||||
let anchor = marker;
|
let lastRef = anchor;
|
||||||
for (let i = order.length - 1;i >= 0; i--) {
|
for (let i = nextOrder.length - 1;i >= 0; i--) {
|
||||||
const view = nextCache.get(order[i]);
|
const view = nextOrder[i];
|
||||||
if (view.container.nextSibling !== anchor) {
|
const node = view.container;
|
||||||
container.insertBefore(view.container, anchor);
|
if (node.nextSibling !== lastRef)
|
||||||
}
|
root.insertBefore(node, lastRef);
|
||||||
anchor = view.container;
|
lastRef = node;
|
||||||
}
|
}
|
||||||
viewCache = nextCache;
|
cache = nextCache;
|
||||||
});
|
});
|
||||||
return container;
|
return root;
|
||||||
};
|
};
|
||||||
var Router = (routes) => {
|
var Router = (routes) => {
|
||||||
const currentPath = $(window.location.hash.replace(/^#/, "") || "/");
|
const getHash = () => window.location.hash.slice(1) || "/";
|
||||||
window.addEventListener("hashchange", () => currentPath(window.location.hash.replace(/^#/, "") || "/"));
|
const path = $(getHash());
|
||||||
const outlet = Tag("div", { class: "router-transition" });
|
const handler = () => path(getHash());
|
||||||
|
window.addEventListener("hashchange", handler);
|
||||||
|
onUnmount(() => window.removeEventListener("hashchange", handler));
|
||||||
|
const hook = Tag("div", { class: "router-hook" });
|
||||||
let currentView = null;
|
let currentView = null;
|
||||||
Watch([currentPath], async () => {
|
Watch([path], () => {
|
||||||
const path = currentPath();
|
const cur = path();
|
||||||
const route = routes.find((r) => {
|
const route = routes.find((r) => {
|
||||||
const routeParts = r.path.split("/").filter(Boolean);
|
const p1 = r.path.split("/").filter(Boolean);
|
||||||
const pathParts = path.split("/").filter(Boolean);
|
const p2 = cur.split("/").filter(Boolean);
|
||||||
return routeParts.length === pathParts.length && routeParts.every((part, i) => part.startsWith(":") || part === pathParts[i]);
|
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]);
|
||||||
}) || routes.find((r) => r.path === "*");
|
}) || routes.find((r) => r.path === "*");
|
||||||
if (route) {
|
if (route) {
|
||||||
let component = route.component;
|
currentView?.destroy();
|
||||||
if (isFunc(component) && component.toString().includes("import")) {
|
|
||||||
component = (await component()).default || await component();
|
|
||||||
}
|
|
||||||
const params = {};
|
const params = {};
|
||||||
route.path.split("/").filter(Boolean).forEach((part, i) => {
|
route.path.split("/").filter(Boolean).forEach((p, i) => {
|
||||||
if (part.startsWith(":"))
|
if (p[0] === ":")
|
||||||
params[part.slice(1)] = path.split("/").filter(Boolean)[i];
|
params[p.slice(1)] = cur.split("/").filter(Boolean)[i];
|
||||||
});
|
});
|
||||||
if (currentView)
|
Router.params(params);
|
||||||
currentView.destroy();
|
currentView = Render(() => isFunc(route.component) ? route.component(params) : route.component);
|
||||||
if (Router.params)
|
hook.replaceChildren(currentView.container);
|
||||||
Router.params(params);
|
|
||||||
currentView = Render(() => {
|
|
||||||
try {
|
|
||||||
return isFunc(component) ? component(params) : component;
|
|
||||||
} catch (e) {
|
|
||||||
return Tag("div", { class: "p-4 text-error" }, "Error loading view");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
outlet.appendChild(currentView.container);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return outlet;
|
return hook;
|
||||||
};
|
};
|
||||||
Router.params = $({});
|
Router.params = $({});
|
||||||
Router.to = (path) => window.location.hash = path.replace(/^#?\/?/, "#/");
|
Router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||||
Router.back = () => window.history.back();
|
Router.back = () => window.history.back();
|
||||||
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||||
var Mount = (component, target) => {
|
var Mount = (comp, target) => {
|
||||||
const targetEl = typeof target === "string" ? doc.querySelector(target) : target;
|
const t = typeof target === "string" ? doc.querySelector(target) : target;
|
||||||
if (!targetEl)
|
if (!t)
|
||||||
return;
|
return;
|
||||||
if (MOUNTED_NODES.has(targetEl))
|
if (MOUNTED_NODES.has(t))
|
||||||
MOUNTED_NODES.get(targetEl).destroy();
|
MOUNTED_NODES.get(t).destroy();
|
||||||
const instance = Render(isFunc(component) ? component : () => component);
|
const inst = Render(isFunc(comp) ? comp : () => comp);
|
||||||
targetEl.replaceChildren(instance.container);
|
t.replaceChildren(inst.container);
|
||||||
MOUNTED_NODES.set(targetEl, instance);
|
MOUNTED_NODES.set(t, inst);
|
||||||
return instance;
|
return inst;
|
||||||
};
|
};
|
||||||
var SigPro = { $, $$, Render, Watch, Tag, If, For, Router, Mount };
|
var SigPro = Object.freeze({ $, $$, Watch, Tag, Render, If, For, Router, Mount, onMount, onUnmount, Batch });
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
assign(window, SigPro);
|
Object.assign(window, SigPro);
|
||||||
const tags = `div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(" ");
|
"div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg".split(" ").forEach((t) => window[t[0].toUpperCase() + t.slice(1)] = (p, c) => SigPro.Tag(t, p, c));
|
||||||
tags.forEach((tag) => {
|
|
||||||
const helper = tag[0].toUpperCase() + tag.slice(1);
|
|
||||||
if (!(helper in window))
|
|
||||||
window[helper] = (p, c) => Tag(tag, p, c);
|
|
||||||
});
|
|
||||||
window.SigPro = Object.freeze(SigPro);
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sigpro",
|
"name": "sigpro",
|
||||||
"version": "1.1.22",
|
"version": "1.2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./dist/sigpro.esm.min.js",
|
"main": "./dist/sigpro.esm.min.js",
|
||||||
|
|||||||
232
sigpro-lite.js
232
sigpro-lite.js
@@ -1,232 +0,0 @@
|
|||||||
// ============================================================
|
|
||||||
// SIGPRO CORE · Simple, Fast, Memory‑Safe
|
|
||||||
// ============================================================
|
|
||||||
let currentContext = null
|
|
||||||
|
|
||||||
const getContext = () => currentContext
|
|
||||||
const runInContext = (ctx, fn) => {
|
|
||||||
const prev = currentContext
|
|
||||||
currentContext = ctx
|
|
||||||
try { return fn() }
|
|
||||||
finally { currentContext = prev }
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCleanup = (fn) => {
|
|
||||||
const ctx = getContext()
|
|
||||||
if (!ctx) throw new Error('onCleanup must be called within a reactive root')
|
|
||||||
ctx.cleanups.add(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onMount = (fn) => {
|
|
||||||
const ctx = getContext()
|
|
||||||
if (!ctx) throw new Error('onMount must be called within a reactive root')
|
|
||||||
queueMicrotask(() => runInContext(ctx, fn))
|
|
||||||
}
|
|
||||||
|
|
||||||
const createRoot = (fn) => {
|
|
||||||
const ctx = { cleanups: new Set(), parent: currentContext }
|
|
||||||
return runInContext(ctx, () => {
|
|
||||||
const result = fn(ctx)
|
|
||||||
const destroy = () => { ctx.cleanups.forEach(c => c()); ctx.cleanups.clear() }
|
|
||||||
if (result instanceof Node) result._destroy = destroy
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const createComponent = (fn) => (...args) => {
|
|
||||||
const parent = getContext()
|
|
||||||
const ctx = { cleanups: new Set(), parent }
|
|
||||||
if (parent) parent.cleanups.add(() => { ctx.cleanups.forEach(c => c()); ctx.cleanups.clear() })
|
|
||||||
return runInContext(ctx, () => fn(...args))
|
|
||||||
}
|
|
||||||
|
|
||||||
const createSignal = (initialValue) => {
|
|
||||||
const subscribers = new Set()
|
|
||||||
let value = initialValue
|
|
||||||
const signal = (...args) => {
|
|
||||||
if (args.length === 0) {
|
|
||||||
const ctx = getContext()
|
|
||||||
if (ctx?.activeEffect) {
|
|
||||||
subscribers.add(ctx.activeEffect)
|
|
||||||
ctx.activeEffect.deps.add(subscribers)
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
const next = typeof args[0] === 'function' ? args[0](value) : args[0]
|
|
||||||
if (!Object.is(value, next)) {
|
|
||||||
value = next
|
|
||||||
;[...subscribers].forEach(e => e.run())
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return signal
|
|
||||||
}
|
|
||||||
|
|
||||||
const createEffect = (fn) => {
|
|
||||||
const ctx = getContext()
|
|
||||||
if (!ctx) throw new Error('createEffect must be called within a reactive root')
|
|
||||||
const effect = {
|
|
||||||
deps: new Set(),
|
|
||||||
run: () => {
|
|
||||||
effect.deps.forEach(d => d.delete(effect))
|
|
||||||
effect.deps.clear()
|
|
||||||
const prev = ctx.activeEffect
|
|
||||||
ctx.activeEffect = effect
|
|
||||||
try { fn() }
|
|
||||||
finally { ctx.activeEffect = prev }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onCleanup(() => {
|
|
||||||
effect.deps.forEach(d => d.delete(effect))
|
|
||||||
effect.deps.clear()
|
|
||||||
})
|
|
||||||
effect.run()
|
|
||||||
}
|
|
||||||
|
|
||||||
const mount = (component, selector) => {
|
|
||||||
const root = document.querySelector(selector)
|
|
||||||
if (!root) throw new Error(`Selector "${selector}" not found`)
|
|
||||||
if (root._destroy) root._destroy()
|
|
||||||
root.innerHTML = ''
|
|
||||||
const app = createRoot(component)
|
|
||||||
root.appendChild(app)
|
|
||||||
root._destroy = app._destroy
|
|
||||||
return app
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper para crear elementos (con soporte SVG)
|
|
||||||
const h = (tag, props, ...children) => {
|
|
||||||
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/i.test(tag)
|
|
||||||
const el = isSVG
|
|
||||||
? document.createElementNS('http://www.w3.org/2000/svg', tag)
|
|
||||||
: document.createElement(tag)
|
|
||||||
|
|
||||||
if (props) {
|
|
||||||
Object.entries(props).forEach(([k, v]) => {
|
|
||||||
if (k.startsWith('on')) {
|
|
||||||
const event = k.slice(2).toLowerCase()
|
|
||||||
el.addEventListener(event, v)
|
|
||||||
onCleanup(() => el.removeEventListener(event, v))
|
|
||||||
} else if (k === 'style' && typeof v === 'object') {
|
|
||||||
Object.assign(el.style, v)
|
|
||||||
} else {
|
|
||||||
el.setAttribute(k, v)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
children.flat().forEach(c => {
|
|
||||||
if (c instanceof Node) el.appendChild(c)
|
|
||||||
else el.appendChild(document.createTextNode(String(c ?? '')))
|
|
||||||
})
|
|
||||||
return el
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== IF / FOR / ROUTER (API IDÉNTICA A B) =====
|
|
||||||
const If = createComponent((cond, thenFn) => {
|
|
||||||
const anchor = document.createTextNode('')
|
|
||||||
const container = h('div', { style: 'display:contents' }, anchor)
|
|
||||||
let current = null
|
|
||||||
createEffect(() => {
|
|
||||||
const show = !!cond()
|
|
||||||
if (show && !current) {
|
|
||||||
current = createRoot(() => thenFn())
|
|
||||||
container.insertBefore(current, anchor)
|
|
||||||
} else if (!show && current) {
|
|
||||||
if (current._destroy) current._destroy()
|
|
||||||
else current.remove()
|
|
||||||
current = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onCleanup(() => current?._destroy?.())
|
|
||||||
return container
|
|
||||||
})
|
|
||||||
|
|
||||||
const For = createComponent((source, itemFn) => {
|
|
||||||
const container = h('div', { style: 'display:contents' })
|
|
||||||
const marker = document.createTextNode('')
|
|
||||||
container.appendChild(marker)
|
|
||||||
let cache = new Map()
|
|
||||||
createEffect(() => {
|
|
||||||
const items = source() || []
|
|
||||||
const newCache = new Map()
|
|
||||||
const order = []
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i]
|
|
||||||
const key = (item && typeof item === 'object' && 'id' in item) ? item.id : i
|
|
||||||
let view = cache.get(key)
|
|
||||||
if (!view) view = createRoot(() => itemFn(item, i))
|
|
||||||
newCache.set(key, view)
|
|
||||||
order.push(key)
|
|
||||||
cache.delete(key)
|
|
||||||
}
|
|
||||||
cache.forEach(v => v._destroy ? v._destroy() : v.remove?.())
|
|
||||||
cache = newCache
|
|
||||||
let anchor = marker
|
|
||||||
for (let i = order.length - 1; i >= 0; i--) {
|
|
||||||
const view = newCache.get(order[i])
|
|
||||||
if (view instanceof Node && view.nextSibling !== anchor)
|
|
||||||
container.insertBefore(view, anchor)
|
|
||||||
anchor = view
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onCleanup(() => cache.forEach(v => v._destroy ? v._destroy() : v.remove?.()))
|
|
||||||
return container
|
|
||||||
})
|
|
||||||
|
|
||||||
const Router = createComponent(({ routes }) => {
|
|
||||||
const getHash = () => window.location.hash.slice(1) || '/'
|
|
||||||
const path = createSignal(getHash())
|
|
||||||
const handler = () => path(getHash())
|
|
||||||
window.addEventListener('hashchange', handler)
|
|
||||||
onCleanup(() => window.removeEventListener('hashchange', handler))
|
|
||||||
const outlet = h('div', {})
|
|
||||||
let currentView = null
|
|
||||||
createEffect(() => {
|
|
||||||
const cur = path()
|
|
||||||
const match = routes.find(r => {
|
|
||||||
const rParts = r.path.split('/').filter(Boolean)
|
|
||||||
const pParts = cur.split('/').filter(Boolean)
|
|
||||||
return rParts.length === pParts.length && rParts.every((p, i) => p.startsWith(':') || p === pParts[i])
|
|
||||||
}) || routes.find(r => r.path === '*')
|
|
||||||
if (match) {
|
|
||||||
if (currentView?._destroy) currentView._destroy()
|
|
||||||
else if (currentView) currentView.remove()
|
|
||||||
const params = {}
|
|
||||||
match.path.split('/').filter(Boolean).forEach((p, i) => {
|
|
||||||
if (p.startsWith(':')) params[p.slice(1)] = cur.split('/').filter(Boolean)[i]
|
|
||||||
})
|
|
||||||
currentView = createRoot(() => match.component(params))
|
|
||||||
outlet.appendChild(currentView)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
onCleanup(() => currentView?._destroy?.())
|
|
||||||
return outlet
|
|
||||||
})
|
|
||||||
Router.to = (path) => window.location.hash = path.replace(/^#?\/?/, '#/')
|
|
||||||
Router.back = () => window.history.back()
|
|
||||||
Router.path = () => window.location.hash.slice(1) || '/'
|
|
||||||
|
|
||||||
// ===== API PÚBLICA =====
|
|
||||||
const SigPro = {
|
|
||||||
createRoot, createComponent, createSignal, createEffect,
|
|
||||||
onCleanup, onMount, mount, h,
|
|
||||||
If, For, Router
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== EXPOSICIÓN GLOBAL (OPCIONAL, COMO EN B) =====
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
Object.assign(window, SigPro)
|
|
||||||
const tags = 'div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg'.split(' ')
|
|
||||||
tags.forEach(t => {
|
|
||||||
const helper = t[0].toUpperCase() + t.slice(1)
|
|
||||||
window[helper] = (props, ...children) => h(t, props, children)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
createRoot, createComponent, createSignal, createEffect,
|
|
||||||
onCleanup, onMount, mount, h,
|
|
||||||
If, For, Router
|
|
||||||
}
|
|
||||||
export default SigPro
|
|
||||||
727
sigpro.d.ts
vendored
727
sigpro.d.ts
vendored
@@ -1,188 +1,581 @@
|
|||||||
// sigpro.d.ts
|
/**
|
||||||
|
* SigPro 1.2.0
|
||||||
|
* A minimalistic reactive UI library with fine-grained reactivity,
|
||||||
|
* deep reactive proxies, and intuitive component composition.
|
||||||
|
*/
|
||||||
|
|
||||||
declare const SIG_BRAND: unique symbol;
|
// ============================================================================
|
||||||
|
// Core Reactivity
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export interface Signal<T = any> {
|
/**
|
||||||
readonly [SIG_BRAND]: true;
|
* A reactive signal that holds a value and automatically tracks dependencies.
|
||||||
(): T;
|
* Signals are the foundation of SigPro's reactivity system.
|
||||||
(value: T): T;
|
*
|
||||||
(updater: (prev: T) => T): T;
|
* @typeParam T - The type of the value stored in the signal
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Basic usage
|
||||||
|
* const count = $(0)
|
||||||
|
* console.log(count()) // 0
|
||||||
|
* count(5)
|
||||||
|
* console.log(count()) // 5
|
||||||
|
* count(c => c + 1)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Computed signal
|
||||||
|
* const double = $(() => count() * 2)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Persistent signal (synced with localStorage)
|
||||||
|
* const name = $("Guest", "user-name")
|
||||||
|
*/
|
||||||
|
export function $<T>(value: T, persistentKey?: string): Signal<T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A deeply reactive proxy that wraps an object or array, tracking property access
|
||||||
|
* and mutations with fine-grained precision. Only effects that depend on changed
|
||||||
|
* properties will re-run.
|
||||||
|
*
|
||||||
|
* @typeParam T - The type of the object/array being wrapped
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const state = $$({ user: { name: 'Ana', age: 30 }, items: [1, 2, 3] })
|
||||||
|
*
|
||||||
|
* // Reading a property (reactive)
|
||||||
|
* Watch(() => console.log(state.user.name)) // logs 'Ana'
|
||||||
|
*
|
||||||
|
* // Mutating a property (triggers dependent effects)
|
||||||
|
* state.user.name = 'María'
|
||||||
|
*
|
||||||
|
* // Adding/deleting properties also notifies iteration dependencies
|
||||||
|
* state.newProp = true
|
||||||
|
* delete state.items
|
||||||
|
*
|
||||||
|
* // Arrays work with iteration tracking
|
||||||
|
* Object.keys(state) // tracked via internal symbol
|
||||||
|
*/
|
||||||
|
export function $$<T extends object>(target: T): DeepReactive<T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reactive signal type. Calling the signal returns its current value.
|
||||||
|
* Passing an argument updates the value.
|
||||||
|
*
|
||||||
|
* @typeParam T - The type of the value
|
||||||
|
*/
|
||||||
|
export type Signal<T> = {
|
||||||
|
(): T
|
||||||
|
(value: T | ((prev: T) => T)): void
|
||||||
|
|
||||||
|
// Internal properties (not meant for direct use)
|
||||||
|
_isComputed?: boolean
|
||||||
|
_subs?: Set<Effect>
|
||||||
|
_dirty?: boolean
|
||||||
|
_deps?: Set<Set<Effect>>
|
||||||
|
_disposed?: boolean
|
||||||
|
markDirty?: () => void
|
||||||
|
stop?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Computed<T = any> {
|
/**
|
||||||
readonly [SIG_BRAND]: true;
|
* A deeply reactive object where all property access and mutations are tracked.
|
||||||
(): T;
|
* Works recursively on nested objects and arrays.
|
||||||
|
*/
|
||||||
|
export type DeepReactive<T> = T extends object
|
||||||
|
? {
|
||||||
|
[K in keyof T]: T[K] extends object ? DeepReactive<T[K]> : T[K]
|
||||||
|
} & {
|
||||||
|
[Symbol.iterator]?: T extends Iterable<infer U> ? () => Iterator<DeepReactive<U>> : never
|
||||||
|
}
|
||||||
|
: T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal effect representation.
|
||||||
|
*/
|
||||||
|
interface Effect {
|
||||||
|
(): any
|
||||||
|
_deps: Set<Set<Effect>> | null
|
||||||
|
_cleanups: Set<() => void> | null
|
||||||
|
_children: Set<Effect> | null
|
||||||
|
_disposed: boolean
|
||||||
|
_isComputed: boolean
|
||||||
|
_depth: number
|
||||||
|
_mounts: Array<() => void>
|
||||||
|
_parent: Effect | null
|
||||||
|
_result?: any
|
||||||
|
_dirty?: boolean
|
||||||
|
_subs?: Set<Effect>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Effects and Watching
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reactive effect that tracks signal dependencies and re-runs when they change.
|
||||||
|
* Returns a cleanup function to stop the effect.
|
||||||
|
*
|
||||||
|
* @param sources - A signal, array of signals, or a function that reads from signals
|
||||||
|
* @param callback - Optional callback that receives the current values
|
||||||
|
* @returns A cleanup function that stops the effect
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Auto-tracking with a function
|
||||||
|
* const stop = Watch(() => {
|
||||||
|
* console.log(`Count is: ${count()}`)
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Explicit sources with callback
|
||||||
|
* Watch([count, name], ([c, n]) => {
|
||||||
|
* console.log(`Count: ${c}, Name: ${n}`)
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Cleanup
|
||||||
|
* stop() // or call the returned function
|
||||||
|
*/
|
||||||
|
export function Watch(
|
||||||
|
sources: (() => void) | Signal<any> | Array<Signal<any>>
|
||||||
|
): () => void
|
||||||
|
export function Watch<T>(
|
||||||
|
sources: Signal<T> | Array<Signal<any>>,
|
||||||
|
callback: (values: T | any[]) => void
|
||||||
|
): () => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batches multiple signal updates into a single reactive update cycle.
|
||||||
|
* Use this when performing many updates in sequence to avoid unnecessary re-renders.
|
||||||
|
*
|
||||||
|
* @param fn - Function containing batched updates
|
||||||
|
* @returns The return value of the batched function
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Batch(() => {
|
||||||
|
* for (let i = 0; i < 1000; i++) {
|
||||||
|
* items(prev => [...prev, i])
|
||||||
|
* }
|
||||||
|
* })
|
||||||
|
* // Effects will run only once after the batch completes
|
||||||
|
*/
|
||||||
|
export function Batch<T>(fn: () => T): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback to run when the current component mounts.
|
||||||
|
* Must be called within a component function or Render context.
|
||||||
|
*
|
||||||
|
* @param fn - Function to execute on mount
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const MyComponent = () => {
|
||||||
|
* onMount(() => console.log('Component mounted'))
|
||||||
|
* return Div("Hello")
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function onMount(fn: () => void): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a callback to run when the current component unmounts.
|
||||||
|
* Useful for cleanup (event listeners, intervals, etc.).
|
||||||
|
* Must be called within a component function or Render context.
|
||||||
|
*
|
||||||
|
* @param fn - Function to execute on unmount
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const MyComponent = () => {
|
||||||
|
* onUnmount(() => console.log('Component unmounted'))
|
||||||
|
* return Div("Hello")
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function onUnmount(fn: () => void): void
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Component & Rendering
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a DOM element or component. The Swiss Army knife of SigPro templating.
|
||||||
|
*
|
||||||
|
* @param tag - HTML tag name, SVG tag name, or a component function
|
||||||
|
* @param props - Element properties/attributes (optional)
|
||||||
|
* @param children - Child elements (optional)
|
||||||
|
* @returns A DOM Node or DocumentFragment
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // HTML element
|
||||||
|
* Tag("div", { class: "container" }, [
|
||||||
|
* Tag("h1", "Hello World"),
|
||||||
|
* Tag("button", { onclick: () => alert('clicked') }, "Click me")
|
||||||
|
* ])
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Component
|
||||||
|
* const Greeting = ({ name }) => Tag("p", `Hello, ${name}`)
|
||||||
|
* Tag(Greeting, { name: "Ana" })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Reactive attributes
|
||||||
|
* Tag("div", { class: () => isActive() ? "active" : "" })
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // SVG
|
||||||
|
* Tag("svg", { width: 100, height: 100 }, [
|
||||||
|
* Tag("circle", { cx: 50, cy: 50, r: 40, fill: "red" })
|
||||||
|
* ])
|
||||||
|
*/
|
||||||
|
export function Tag(
|
||||||
|
tag: string | ((props: any, ctx: ComponentContext) => any),
|
||||||
|
props?: Record<string, any> | Node | Array<any>,
|
||||||
|
children?: any
|
||||||
|
): Node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context object passed to component functions.
|
||||||
|
*/
|
||||||
|
export interface ComponentContext {
|
||||||
|
/** Child elements passed to the component */
|
||||||
|
children: any
|
||||||
|
/** Emit an event to the parent component */
|
||||||
|
emit: (event: string, ...args: any[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a component or template function and returns a runtime instance
|
||||||
|
* that can be mounted and destroyed.
|
||||||
|
*
|
||||||
|
* @param renderFn - Function that returns DOM nodes or components
|
||||||
|
* @returns A runtime instance with container and destroy method
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const app = Render(() => Div({ class: "app" }, "Hello"))
|
||||||
|
* document.body.appendChild(app.container)
|
||||||
|
*
|
||||||
|
* // Later: app.destroy()
|
||||||
|
*/
|
||||||
|
export function Render(
|
||||||
|
renderFn: (ctx: { onCleanup: (fn: () => void) => void }) => any
|
||||||
|
): RuntimeInstance
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A runtime instance returned by Render.
|
||||||
|
*/
|
||||||
export interface RuntimeInstance {
|
export interface RuntimeInstance {
|
||||||
readonly _isRuntime: true;
|
_isRuntime: true
|
||||||
readonly container: HTMLElement;
|
/** The container element that holds the rendered content */
|
||||||
destroy(): void;
|
container: HTMLDivElement
|
||||||
}
|
/** Destroys the instance and cleans up all reactive effects */
|
||||||
|
destroy: () => void
|
||||||
export interface TransitionOptions {
|
|
||||||
on?: (el: HTMLElement) => void;
|
|
||||||
off?: (el: HTMLElement, destroy: () => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Route {
|
|
||||||
path: string;
|
|
||||||
component: (params: Record<string, string>) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RenderContext {
|
|
||||||
onCleanup: (fn: () => void) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TagProps extends Record<string, any> {
|
|
||||||
ref?: ((el: HTMLElement) => void) | { current: HTMLElement | null };
|
|
||||||
class?: string | (() => string);
|
|
||||||
style?: string | (() => string);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ReactiveObject<T extends object> = {
|
|
||||||
[K in keyof T]: T[K] extends object ? ReactiveObject<T[K]> : T[K];
|
|
||||||
};
|
|
||||||
|
|
||||||
export function $<T>(val: T, key?: string | null): Signal<T>;
|
|
||||||
export function $<T>(val: () => T): Computed<T>;
|
|
||||||
|
|
||||||
export function $$<T>(fn: () => T): Computed<T>;
|
|
||||||
|
|
||||||
export function $_<T extends object>(obj: T): ReactiveObject<T>;
|
|
||||||
|
|
||||||
export function untrack<T>(fn: () => T): T;
|
|
||||||
|
|
||||||
export function Watch(cb: () => void): () => void;
|
|
||||||
export function Watch(cb: () => () => void): () => void;
|
|
||||||
|
|
||||||
export function Render<T>(fn: (ctx: RenderContext) => T): RuntimeInstance;
|
|
||||||
|
|
||||||
export function Tag(tag: string, props?: TagProps | null, children?: any[]): HTMLElement;
|
|
||||||
export function Tag(tag: string, children?: any[]): HTMLElement;
|
|
||||||
|
|
||||||
export function If(
|
|
||||||
cond: boolean | (() => boolean),
|
|
||||||
a: any | (() => any),
|
|
||||||
b?: any | (() => any) | null,
|
|
||||||
options?: TransitionOptions
|
|
||||||
): HTMLElement;
|
|
||||||
|
|
||||||
export function For<T>(
|
|
||||||
source: T[] | (() => T[]),
|
|
||||||
renderFn: (item: T, index: number) => any,
|
|
||||||
keyFn?: (item: T, index: number) => any,
|
|
||||||
tag?: string,
|
|
||||||
props?: TagProps
|
|
||||||
): HTMLElement;
|
|
||||||
|
|
||||||
export function Router(routes: Route[]): HTMLElement;
|
|
||||||
|
|
||||||
export namespace Router {
|
|
||||||
const params: Signal<Record<string, string>>;
|
|
||||||
function to(path: string): void;
|
|
||||||
function back(): void;
|
|
||||||
function path(): string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mounts a component to a DOM element.
|
||||||
|
*
|
||||||
|
* @param component - Component function or element to mount
|
||||||
|
* @param target - CSS selector string or DOM element
|
||||||
|
* @returns The runtime instance, or undefined if target not found
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Mount to element with ID 'app'
|
||||||
|
* Mount(() => Div("Hello SigPro"), "#app")
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Mount to existing element
|
||||||
|
* Mount(MyComponent, document.body)
|
||||||
|
*/
|
||||||
export function Mount(
|
export function Mount(
|
||||||
component: (() => any) | any,
|
component: (() => any) | any,
|
||||||
target: string | HTMLElement
|
target: string | Element
|
||||||
): RuntimeInstance;
|
): RuntimeInstance | undefined
|
||||||
|
|
||||||
export function Share<T>(key: string, value: T): void;
|
// ============================================================================
|
||||||
|
// Control Flow Components
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
export function Use<T>(key: string, defaultValue?: T): T | undefined;
|
/**
|
||||||
|
* Conditionally renders content based on a reactive condition.
|
||||||
|
*
|
||||||
|
* @param cond - Boolean value or signal returning boolean
|
||||||
|
* @param ifYes - Content to render when condition is true
|
||||||
|
* @param ifNot - Content to render when condition is false (optional)
|
||||||
|
* @returns A container element that manages the conditional content
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* If($show,
|
||||||
|
* () => Div("Visible content"),
|
||||||
|
* () => Div("Hidden state placeholder")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
export function If(
|
||||||
|
cond: boolean | (() => boolean) | Signal<boolean>,
|
||||||
|
ifYes: any | (() => any),
|
||||||
|
ifNot?: any | (() => any)
|
||||||
|
): HTMLDivElement
|
||||||
|
|
||||||
// Funciones JSX (etiquetas globales)
|
/**
|
||||||
export const Div: (props?: TagProps, children?: any[]) => HTMLElement;
|
* Renders a list of items efficiently, updating only changed items.
|
||||||
export const Span: (props?: TagProps, children?: any[]) => HTMLElement;
|
*
|
||||||
export const P: (props?: TagProps, children?: any[]) => HTMLElement;
|
* @param src - Array or signal returning array of items
|
||||||
export const H1: (props?: TagProps, children?: any[]) => HTMLElement;
|
* @param itemFn - Function that renders each item
|
||||||
export const H2: (props?: TagProps, children?: any[]) => HTMLElement;
|
* @param keyFn - Optional function to generate stable keys for items
|
||||||
export const H3: (props?: TagProps, children?: any[]) => HTMLElement;
|
* @returns A container element that manages the list
|
||||||
export const H4: (props?: TagProps, children?: any[]) => HTMLElement;
|
*
|
||||||
export const H5: (props?: TagProps, children?: any[]) => HTMLElement;
|
* @example
|
||||||
export const H6: (props?: TagProps, children?: any[]) => HTMLElement;
|
* const items = $([1, 2, 3])
|
||||||
export const Button: (props?: TagProps, children?: any[]) => HTMLElement;
|
* For(items, (item, index) => Li(`Item ${item}`), item => item)
|
||||||
export const A: (props?: TagProps, children?: any[]) => HTMLElement;
|
*/
|
||||||
export const Img: (props?: TagProps, children?: any[]) => HTMLElement;
|
export function For<T>(
|
||||||
export const Input: (props?: TagProps, children?: any[]) => HTMLElement;
|
src: T[] | (() => T[]) | Signal<T[]>,
|
||||||
export const Textarea: (props?: TagProps, children?: any[]) => HTMLElement;
|
itemFn: (item: T, index: number) => any,
|
||||||
export const Select: (props?: TagProps, children?: any[]) => HTMLElement;
|
keyFn?: (item: T, index: number) => string | number
|
||||||
export const Option: (props?: TagProps, children?: any[]) => HTMLElement;
|
): HTMLDivElement
|
||||||
export const Form: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Label: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Ul: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Ol: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Li: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Table: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Tr: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Td: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Th: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Section: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Article: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Aside: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Nav: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Header: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Footer: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
export const Main: (props?: TagProps, children?: any[]) => HTMLElement;
|
|
||||||
|
|
||||||
export interface SigProAPI {
|
// ============================================================================
|
||||||
$: typeof $;
|
// Router
|
||||||
$$: typeof $$;
|
// ============================================================================
|
||||||
$_: typeof $_;
|
|
||||||
untrack: typeof untrack;
|
/**
|
||||||
Render: typeof Render;
|
* Hash-based router component for single-page applications.
|
||||||
Watch: typeof Watch;
|
*
|
||||||
Tag: typeof Tag;
|
* @param routes - Array of route definitions
|
||||||
If: typeof If;
|
* @returns A router container element
|
||||||
For: typeof For;
|
*
|
||||||
Router: typeof Router;
|
* @example
|
||||||
Mount: typeof Mount;
|
* Router([
|
||||||
Share: typeof Share;
|
* { path: "/", component: HomePage },
|
||||||
Use: typeof Use;
|
* { path: "/about", component: AboutPage },
|
||||||
|
* { path: "/user/:id", component: UserPage },
|
||||||
|
* { path: "*", component: NotFoundPage }
|
||||||
|
* ])
|
||||||
|
*/
|
||||||
|
export function Router(routes: RouteDefinition[]): HTMLDivElement
|
||||||
|
|
||||||
|
export namespace Router {
|
||||||
|
/**
|
||||||
|
* Reactive signal containing current route parameters.
|
||||||
|
* @example
|
||||||
|
* const params = Router.params()
|
||||||
|
* console.log(params.id) // from "/user/:id"
|
||||||
|
*/
|
||||||
|
export const params: Signal<Record<string, string>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigate to a path.
|
||||||
|
* @example
|
||||||
|
* Router.to("/about")
|
||||||
|
*/
|
||||||
|
export function to(path: string): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go back in browser history.
|
||||||
|
*/
|
||||||
|
export function back(): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current path.
|
||||||
|
*/
|
||||||
|
export function path(): string
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const SigPro: SigProAPI;
|
/**
|
||||||
export default SigPro;
|
* Route definition for the Router.
|
||||||
|
*/
|
||||||
|
export interface RouteDefinition {
|
||||||
|
/** Path pattern with optional :param placeholders. "*" for catch-all. */
|
||||||
|
path: string
|
||||||
|
/** Component to render when route matches */
|
||||||
|
component: any | ((params: Record<string, string>) => any)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// HTML Tag Helpers
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience functions for creating HTML elements.
|
||||||
|
* Available globally when the library is loaded in a browser.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Div({ class: "container" }, [
|
||||||
|
* H1("Title"),
|
||||||
|
* P("Paragraph text"),
|
||||||
|
* Button({ onclick: handleClick }, "Click me")
|
||||||
|
* ])
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Creates a `<div>` element */
|
||||||
|
export function Div(props?: Record<string, any>, ...children: any[]): HTMLDivElement
|
||||||
|
|
||||||
|
/** Creates a `<span>` element */
|
||||||
|
export function Span(props?: Record<string, any>, ...children: any[]): HTMLSpanElement
|
||||||
|
|
||||||
|
/** Creates a `<p>` element */
|
||||||
|
export function P(props?: Record<string, any>, ...children: any[]): HTMLParagraphElement
|
||||||
|
|
||||||
|
/** Creates an `<h1>` element */
|
||||||
|
export function H1(props?: Record<string, any>, ...children: any[]): HTMLHeadingElement
|
||||||
|
|
||||||
|
/** Creates an `<h2>` element */
|
||||||
|
export function H2(props?: Record<string, any>, ...children: any[]): HTMLHeadingElement
|
||||||
|
|
||||||
|
/** Creates an `<h3>` element */
|
||||||
|
export function H3(props?: Record<string, any>, ...children: any[]): HTMLHeadingElement
|
||||||
|
|
||||||
|
/** Creates an `<h4>` element */
|
||||||
|
export function H4(props?: Record<string, any>, ...children: any[]): HTMLHeadingElement
|
||||||
|
|
||||||
|
/** Creates an `<h5>` element */
|
||||||
|
export function H5(props?: Record<string, any>, ...children: any[]): HTMLHeadingElement
|
||||||
|
|
||||||
|
/** Creates an `<h6>` element */
|
||||||
|
export function H6(props?: Record<string, any>, ...children: any[]): HTMLHeadingElement
|
||||||
|
|
||||||
|
/** Creates a `<br>` element */
|
||||||
|
export function Br(props?: Record<string, any>): HTMLBRElement
|
||||||
|
|
||||||
|
/** Creates an `<hr>` element */
|
||||||
|
export function Hr(props?: Record<string, any>): HTMLHRElement
|
||||||
|
|
||||||
|
/** Creates a `<section>` element */
|
||||||
|
export function Section(props?: Record<string, any>, ...children: any[]): HTMLElement
|
||||||
|
|
||||||
|
/** Creates an `<article>` element */
|
||||||
|
export function Article(props?: Record<string, any>, ...children: any[]): HTMLElement
|
||||||
|
|
||||||
|
/** Creates an `<aside>` element */
|
||||||
|
export function Aside(props?: Record<string, any>, ...children: any[]): HTMLElement
|
||||||
|
|
||||||
|
/** Creates a `<nav>` element */
|
||||||
|
export function Nav(props?: Record<string, any>, ...children: any[]): HTMLElement
|
||||||
|
|
||||||
|
/** Creates a `<main>` element */
|
||||||
|
export function Main(props?: Record<string, any>, ...children: any[]): HTMLElement
|
||||||
|
|
||||||
|
/** Creates a `<header>` element */
|
||||||
|
export function Header(props?: Record<string, any>, ...children: any[]): HTMLElement
|
||||||
|
|
||||||
|
/** Creates a `<footer>` element */
|
||||||
|
export function Footer(props?: Record<string, any>, ...children: any[]): HTMLElement
|
||||||
|
|
||||||
|
/** Creates a `<ul>` element */
|
||||||
|
export function Ul(props?: Record<string, any>, ...children: any[]): HTMLUListElement
|
||||||
|
|
||||||
|
/** Creates an `<ol>` element */
|
||||||
|
export function Ol(props?: Record<string, any>, ...children: any[]): HTMLOListElement
|
||||||
|
|
||||||
|
/** Creates a `<li>` element */
|
||||||
|
export function Li(props?: Record<string, any>, ...children: any[]): HTMLLIElement
|
||||||
|
|
||||||
|
/** Creates an `<a>` element */
|
||||||
|
export function A(props?: Record<string, any>, ...children: any[]): HTMLAnchorElement
|
||||||
|
|
||||||
|
/** Creates an `<em>` element */
|
||||||
|
export function Em(props?: Record<string, any>, ...children: any[]): HTMLElement
|
||||||
|
|
||||||
|
/** Creates a `<strong>` element */
|
||||||
|
export function Strong(props?: Record<string, any>, ...children: any[]): HTMLElement
|
||||||
|
|
||||||
|
/** Creates a `<pre>` element */
|
||||||
|
export function Pre(props?: Record<string, any>, ...children: any[]): HTMLPreElement
|
||||||
|
|
||||||
|
/** Creates a `<code>` element */
|
||||||
|
export function Code(props?: Record<string, any>, ...children: any[]): HTMLElement
|
||||||
|
|
||||||
|
/** Creates a `<form>` element */
|
||||||
|
export function Form(props?: Record<string, any>, ...children: any[]): HTMLFormElement
|
||||||
|
|
||||||
|
/** Creates a `<label>` element */
|
||||||
|
export function Label(props?: Record<string, any>, ...children: any[]): HTMLLabelElement
|
||||||
|
|
||||||
|
/** Creates an `<input>` element */
|
||||||
|
export function Input(props?: Record<string, any>): HTMLInputElement
|
||||||
|
|
||||||
|
/** Creates a `<textarea>` element */
|
||||||
|
export function Textarea(props?: Record<string, any>): HTMLTextAreaElement
|
||||||
|
|
||||||
|
/** Creates a `<select>` element */
|
||||||
|
export function Select(props?: Record<string, any>, ...children: any[]): HTMLSelectElement
|
||||||
|
|
||||||
|
/** Creates a `<button>` element */
|
||||||
|
export function Button(props?: Record<string, any>, ...children: any[]): HTMLButtonElement
|
||||||
|
|
||||||
|
/** Creates an `<img>` element */
|
||||||
|
export function Img(props?: Record<string, any>): HTMLImageElement
|
||||||
|
|
||||||
|
/** Creates an `<svg>` element */
|
||||||
|
export function Svg(props?: Record<string, any>, ...children: any[]): SVGSVGElement
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Default Export
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
declare const SigPro: {
|
||||||
|
$: typeof $
|
||||||
|
$$: typeof $$
|
||||||
|
Watch: typeof Watch
|
||||||
|
Tag: typeof Tag
|
||||||
|
Render: typeof Render
|
||||||
|
If: typeof If
|
||||||
|
For: typeof For
|
||||||
|
Router: typeof Router
|
||||||
|
Mount: typeof Mount
|
||||||
|
onMount: typeof onMount
|
||||||
|
onUnmount: typeof onUnmount
|
||||||
|
Batch: typeof Batch
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SigPro
|
||||||
|
|
||||||
|
// Global augmentation for browser environments
|
||||||
declare global {
|
declare global {
|
||||||
namespace JSX {
|
interface Window {
|
||||||
interface IntrinsicElements {
|
$: typeof $
|
||||||
div: TagProps;
|
$$: typeof $$
|
||||||
span: TagProps;
|
Watch: typeof Watch
|
||||||
p: TagProps;
|
Tag: typeof Tag
|
||||||
h1: TagProps;
|
Render: typeof Render
|
||||||
h2: TagProps;
|
If: typeof If
|
||||||
h3: TagProps;
|
For: typeof For
|
||||||
h4: TagProps;
|
Router: typeof Router
|
||||||
h5: TagProps;
|
Mount: typeof Mount
|
||||||
h6: TagProps;
|
onMount: typeof onMount
|
||||||
button: TagProps;
|
onUnmount: typeof onUnmount
|
||||||
a: TagProps;
|
Batch: typeof Batch
|
||||||
img: TagProps;
|
SigPro: typeof SigPro
|
||||||
input: TagProps;
|
|
||||||
textarea: TagProps;
|
// Tag helpers
|
||||||
select: TagProps;
|
Div: typeof Div
|
||||||
option: TagProps;
|
Span: typeof Span
|
||||||
form: TagProps;
|
P: typeof P
|
||||||
label: TagProps;
|
H1: typeof H1
|
||||||
ul: TagProps;
|
H2: typeof H2
|
||||||
ol: TagProps;
|
H3: typeof H3
|
||||||
li: TagProps;
|
H4: typeof H4
|
||||||
table: TagProps;
|
H5: typeof H5
|
||||||
tr: TagProps;
|
H6: typeof H6
|
||||||
td: TagProps;
|
Br: typeof Br
|
||||||
th: TagProps;
|
Hr: typeof Hr
|
||||||
section: TagProps;
|
Section: typeof Section
|
||||||
article: TagProps;
|
Article: typeof Article
|
||||||
aside: TagProps;
|
Aside: typeof Aside
|
||||||
nav: TagProps;
|
Nav: typeof Nav
|
||||||
header: TagProps;
|
Main: typeof Main
|
||||||
footer: TagProps;
|
Header: typeof Header
|
||||||
main: TagProps;
|
Footer: typeof Footer
|
||||||
}
|
Ul: typeof Ul
|
||||||
interface Element extends HTMLElement {}
|
Ol: typeof Ol
|
||||||
|
Li: typeof Li
|
||||||
|
A: typeof A
|
||||||
|
Em: typeof Em
|
||||||
|
Strong: typeof Strong
|
||||||
|
Pre: typeof Pre
|
||||||
|
Code: typeof Code
|
||||||
|
Form: typeof Form
|
||||||
|
Label: typeof Label
|
||||||
|
Input: typeof Input
|
||||||
|
Textarea: typeof Textarea
|
||||||
|
Select: typeof Select
|
||||||
|
Button: typeof Button
|
||||||
|
Img: typeof Img
|
||||||
|
Svg: typeof Svg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
898
sigpro.js
898
sigpro.js
@@ -1,440 +1,518 @@
|
|||||||
let activeEffect = null;
|
// sigpro 1.2.0
|
||||||
let currentOwner = null;
|
const isFunc = f => typeof f === "function"
|
||||||
const effectQueue = new Set();
|
const isObj = o => o && typeof o === "object"
|
||||||
let isFlushing = false;
|
const isArr = Array.isArray
|
||||||
const MOUNTED_NODES = new WeakMap();
|
const doc = typeof document !== "undefined" ? document : null
|
||||||
|
const ensureNode = n => n?._isRuntime ? n.container : (n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n)))
|
||||||
|
|
||||||
const doc = document;
|
let activeEffect = null
|
||||||
const isArr = Array.isArray;
|
let activeOwner = null
|
||||||
const assign = Object.assign;
|
let isFlushing = false
|
||||||
const createEl = (t) => doc.createElement(t);
|
let batchDepth = 0
|
||||||
const createText = (t) => doc.createTextNode(String(t ?? ""));
|
const effectQueue = new Set()
|
||||||
const isFunc = (f) => typeof f === "function";
|
const proxyCache = new WeakMap()
|
||||||
const isObj = (o) => typeof o === "object" && o !== null;
|
const ITER = Symbol('iter')
|
||||||
|
const MOUNTED_NODES = new WeakMap()
|
||||||
|
|
||||||
const runWithContext = (effect, callback) => {
|
const dispose = eff => {
|
||||||
const previousEffect = activeEffect;
|
if (!eff || eff._disposed) return
|
||||||
activeEffect = effect;
|
eff._disposed = true
|
||||||
try { return callback(); }
|
const stack = [eff]
|
||||||
finally { activeEffect = previousEffect; }
|
while (stack.length) {
|
||||||
};
|
const e = stack.pop()
|
||||||
|
if (e._cleanups) {
|
||||||
|
e._cleanups.forEach(fn => fn())
|
||||||
|
e._cleanups.clear()
|
||||||
|
}
|
||||||
|
if (e._children) {
|
||||||
|
e._children.forEach(child => stack.push(child))
|
||||||
|
e._children.clear()
|
||||||
|
}
|
||||||
|
if (e._deps) {
|
||||||
|
e._deps.forEach(depSet => depSet.delete(e))
|
||||||
|
e._deps.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const cleanupNode = (node) => {
|
const onMount = fn => {
|
||||||
|
if (activeOwner) (activeOwner._mounts ||= []).push(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUnmount = fn => {
|
||||||
|
if (activeOwner) (activeOwner._cleanups ||= new Set()).add(fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
const untrack = fn => {
|
||||||
|
const p = activeEffect
|
||||||
|
activeEffect = null
|
||||||
|
try { return fn() } finally { activeEffect = p }
|
||||||
|
}
|
||||||
|
|
||||||
|
const createEffect = (fn, isComputed = false) => {
|
||||||
|
const effect = () => {
|
||||||
|
if (effect._disposed) return
|
||||||
|
if (effect._deps) effect._deps.forEach(s => s.delete(effect))
|
||||||
|
if (effect._cleanups) {
|
||||||
|
effect._cleanups.forEach(c => c())
|
||||||
|
effect._cleanups.clear()
|
||||||
|
}
|
||||||
|
const prevEffect = activeEffect
|
||||||
|
const prevOwner = activeOwner
|
||||||
|
activeEffect = activeOwner = effect
|
||||||
|
try {
|
||||||
|
return effect._result = fn()
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[SigPro]", e)
|
||||||
|
} finally {
|
||||||
|
activeEffect = prevEffect
|
||||||
|
activeOwner = prevOwner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
effect._deps = effect._cleanups = effect._children = null
|
||||||
|
effect._disposed = false
|
||||||
|
effect._isComputed = isComputed
|
||||||
|
effect._depth = activeEffect ? activeEffect._depth + 1 : 0
|
||||||
|
effect._mounts = []
|
||||||
|
effect._parent = activeOwner
|
||||||
|
if (activeOwner) (activeOwner._children ||= new Set()).add(effect)
|
||||||
|
return effect
|
||||||
|
}
|
||||||
|
|
||||||
|
const flush = () => {
|
||||||
|
if (isFlushing) return
|
||||||
|
isFlushing = true
|
||||||
|
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth)
|
||||||
|
effectQueue.clear()
|
||||||
|
for (const e of sorted) if (!e._disposed) e()
|
||||||
|
isFlushing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const Batch = fn => {
|
||||||
|
batchDepth++
|
||||||
|
try {
|
||||||
|
return fn()
|
||||||
|
} finally {
|
||||||
|
batchDepth--
|
||||||
|
if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) {
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackUpdate = (subs, trigger = false) => {
|
||||||
|
if (!trigger && activeEffect && !activeEffect._disposed) {
|
||||||
|
subs.add(activeEffect)
|
||||||
|
; (activeEffect._deps ||= new Set()).add(subs)
|
||||||
|
} else if (trigger) {
|
||||||
|
let hasQueue = false
|
||||||
|
subs.forEach(e => {
|
||||||
|
if (e === activeEffect || e._disposed) return
|
||||||
|
if (e._isComputed) {
|
||||||
|
e._dirty = true
|
||||||
|
if (e._subs) trackUpdate(e._subs, true)
|
||||||
|
} else {
|
||||||
|
effectQueue.add(e)
|
||||||
|
hasQueue = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (hasQueue && !isFlushing && batchDepth === 0) queueMicrotask(flush)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const $ = (val, key = null) => {
|
||||||
|
const subs = new Set()
|
||||||
|
if (isFunc(val)) {
|
||||||
|
let cache, dirty = true
|
||||||
|
const computed = () => {
|
||||||
|
if (dirty) {
|
||||||
|
const prev = activeEffect
|
||||||
|
activeEffect = computed
|
||||||
|
try {
|
||||||
|
const next = val()
|
||||||
|
if (!Object.is(cache, next)) {
|
||||||
|
cache = next
|
||||||
|
dirty = false
|
||||||
|
trackUpdate(subs, true)
|
||||||
|
}
|
||||||
|
} finally { activeEffect = prev }
|
||||||
|
}
|
||||||
|
trackUpdate(subs)
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
computed._isComputed = true
|
||||||
|
computed._subs = subs
|
||||||
|
computed._dirty = true
|
||||||
|
computed._deps = null
|
||||||
|
computed._disposed = false
|
||||||
|
computed.markDirty = () => { dirty = true }
|
||||||
|
computed.stop = () => {
|
||||||
|
computed._disposed = true
|
||||||
|
if (computed._deps) {
|
||||||
|
computed._deps.forEach(depSet => depSet.delete(computed))
|
||||||
|
computed._deps.clear()
|
||||||
|
}
|
||||||
|
subs.clear()
|
||||||
|
}
|
||||||
|
if (activeOwner) onUnmount(computed.stop)
|
||||||
|
return computed
|
||||||
|
}
|
||||||
|
if (key) try { val = JSON.parse(localStorage.getItem(key)) ?? val } catch (e) { }
|
||||||
|
return (...args) => {
|
||||||
|
if (args.length) {
|
||||||
|
const next = isFunc(args[0]) ? args[0](val) : args[0]
|
||||||
|
if (!Object.is(val, next)) {
|
||||||
|
val = next
|
||||||
|
if (key) localStorage.setItem(key, JSON.stringify(val))
|
||||||
|
trackUpdate(subs, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
trackUpdate(subs)
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const $$ = (target) => {
|
||||||
|
if (!isObj(target)) return target
|
||||||
|
|
||||||
|
if (proxyCache.has(target)) return proxyCache.get(target)
|
||||||
|
|
||||||
|
const subsMap = new Map()
|
||||||
|
const getSubs = (k) => {
|
||||||
|
let s = subsMap.get(k)
|
||||||
|
if (!s) subsMap.set(k, s = new Set())
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxy = new Proxy(target, {
|
||||||
|
get(t, k) {
|
||||||
|
trackUpdate(getSubs(k))
|
||||||
|
return $$(t[k])
|
||||||
|
},
|
||||||
|
set(t, k, v) {
|
||||||
|
const isNew = !(k in t)
|
||||||
|
if (!Object.is(t[k], v)) {
|
||||||
|
t[k] = v
|
||||||
|
trackUpdate(getSubs(k), true)
|
||||||
|
if (isNew) trackUpdate(getSubs(ITER), true)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
deleteProperty(t, k) {
|
||||||
|
const res = Reflect.deleteProperty(t, k)
|
||||||
|
if (res) {
|
||||||
|
trackUpdate(getSubs(k), true)
|
||||||
|
trackUpdate(getSubs(ITER), true)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
},
|
||||||
|
ownKeys(t) {
|
||||||
|
trackUpdate(getSubs(ITER))
|
||||||
|
return Reflect.ownKeys(t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
proxyCache.set(target, proxy)
|
||||||
|
return proxy
|
||||||
|
}
|
||||||
|
|
||||||
|
const Watch = (sources, cb) => {
|
||||||
|
if (cb === undefined) {
|
||||||
|
const effect = createEffect(sources)
|
||||||
|
effect()
|
||||||
|
return () => dispose(effect)
|
||||||
|
}
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const vals = Array.isArray(sources) ? sources.map(s => s()) : sources()
|
||||||
|
untrack(() => cb(vals))
|
||||||
|
})
|
||||||
|
effect()
|
||||||
|
return () => dispose(effect)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupNode = node => {
|
||||||
if (node._cleanups) {
|
if (node._cleanups) {
|
||||||
node._cleanups.forEach((dispose) => dispose());
|
node._cleanups.forEach(fn => fn())
|
||||||
node._cleanups.clear();
|
node._cleanups.clear()
|
||||||
}
|
}
|
||||||
node.childNodes?.forEach(cleanupNode);
|
if (node._ownerEffect) dispose(node._ownerEffect)
|
||||||
};
|
if (node.childNodes) node.childNodes.forEach(cleanupNode)
|
||||||
|
}
|
||||||
|
|
||||||
const flushEffects = () => {
|
const DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i
|
||||||
if (isFlushing) return;
|
const isDangerousAttr = key => key === 'src' || key === 'href' || key.startsWith('on')
|
||||||
isFlushing = true;
|
|
||||||
while (effectQueue.size > 0) {
|
const validateAttr = (key, val) => {
|
||||||
const sortedEffects = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
|
if (val == null || val === false) return null
|
||||||
effectQueue.clear();
|
if (isDangerousAttr(key)) {
|
||||||
for (const effect of sortedEffects) {
|
const sVal = String(val)
|
||||||
if (!effect._deleted) effect();
|
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
||||||
|
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`)
|
||||||
|
return '#'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isFlushing = false;
|
return val
|
||||||
};
|
}
|
||||||
|
|
||||||
const trackSubscription = (subscribers) => {
|
const Tag = (tag, props = {}, children = []) => {
|
||||||
if (activeEffect && !activeEffect._deleted) {
|
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||||
subscribers.add(activeEffect);
|
children = props
|
||||||
activeEffect._deps.add(subscribers);
|
props = {}
|
||||||
}
|
}
|
||||||
};
|
if (isFunc(tag)) {
|
||||||
|
const ctx = { _mounts: [], _cleanups: new Set() }
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const result = tag(props, {
|
||||||
|
children,
|
||||||
|
emit: (ev, ...args) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...args)
|
||||||
|
})
|
||||||
|
effect._result = result
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
effect()
|
||||||
|
|
||||||
const triggerUpdate = (subscribers) => {
|
const result = effect._result
|
||||||
subscribers.forEach((effect) => {
|
if (result == null) return null
|
||||||
if (effect === activeEffect || effect._deleted) return;
|
|
||||||
if (effect._isComputed) {
|
const node = (result instanceof Node || (isArr(result) && result.every(n => n instanceof Node)))
|
||||||
effect.markDirty();
|
? result
|
||||||
if (effect._subs) triggerUpdate(effect._subs);
|
: doc.createTextNode(String(result))
|
||||||
|
|
||||||
|
const attach = n => {
|
||||||
|
if (isObj(n) && !n._isRuntime) {
|
||||||
|
n._mounts = effect._mounts || []
|
||||||
|
n._cleanups = effect._cleanups || new Set()
|
||||||
|
n._ownerEffect = effect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isArr(node) ? node.forEach(attach) : attach(node)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
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 in props) {
|
||||||
|
if (!props.hasOwnProperty(k)) continue
|
||||||
|
let v = props[k]
|
||||||
|
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)
|
||||||
|
const off = () => el.removeEventListener(ev, v)
|
||||||
|
el._cleanups.add(off)
|
||||||
|
onUnmount(off)
|
||||||
|
} else if (isFunc(v)) {
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const val = validateAttr(k, v())
|
||||||
|
if (k === "class") el.className = val || ""
|
||||||
|
else if (val == null) el.removeAttribute(k)
|
||||||
|
else if (k in el && !isSVG) el[k] = val
|
||||||
|
else el.setAttribute(k, val === true ? "" : val)
|
||||||
|
})
|
||||||
|
effect()
|
||||||
|
el._cleanups.add(() => dispose(effect))
|
||||||
|
onUnmount(() => dispose(effect))
|
||||||
|
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||||
|
const evType = k === "checked" ? "change" : "input"
|
||||||
|
el.addEventListener(evType, ev => v(ev.target[k]))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
effectQueue.add(effect);
|
const val = validateAttr(k, v)
|
||||||
|
if (val != null) {
|
||||||
|
if (k in el && !isSVG) el[k] = val
|
||||||
|
else el.setAttribute(k, val === true ? "" : val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
if (!isFlushing) queueMicrotask(flushEffects);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Render = (renderFn) => {
|
const append = c => {
|
||||||
const cleanups = new Set();
|
if (isArr(c)) return c.forEach(append)
|
||||||
const previousOwner = currentOwner;
|
if (isFunc(c)) {
|
||||||
const container = createEl("div");
|
const anchor = doc.createTextNode("")
|
||||||
container.style.display = "contents";
|
el.appendChild(anchor)
|
||||||
currentOwner = { cleanups };
|
let currentNodes = []
|
||||||
|
const effect = createEffect(() => {
|
||||||
|
const res = c()
|
||||||
|
const next = (isArr(res) ? res : [res]).map(ensureNode)
|
||||||
|
currentNodes.forEach(n => {
|
||||||
|
if (n._isRuntime) n.destroy()
|
||||||
|
else cleanupNode(n)
|
||||||
|
if (n.parentNode) n.remove()
|
||||||
|
})
|
||||||
|
let ref = anchor
|
||||||
|
for (let i = next.length - 1; i >= 0; i--) {
|
||||||
|
const node = next[i]
|
||||||
|
if (node.parentNode !== ref.parentNode) ref.parentNode?.insertBefore(node, ref)
|
||||||
|
if (node._mounts) node._mounts.forEach(fn => fn())
|
||||||
|
ref = node
|
||||||
|
}
|
||||||
|
currentNodes = next
|
||||||
|
})
|
||||||
|
effect()
|
||||||
|
el._cleanups.add(() => dispose(effect))
|
||||||
|
onUnmount(() => dispose(effect))
|
||||||
|
} else {
|
||||||
|
const node = ensureNode(c)
|
||||||
|
el.appendChild(node)
|
||||||
|
if (node._mounts) node._mounts.forEach(fn => fn())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
append(children)
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
const processResult = (result) => {
|
const Render = renderFn => {
|
||||||
if (!result) return;
|
const cleanups = new Set()
|
||||||
|
const mounts = []
|
||||||
|
const previousOwner = activeOwner
|
||||||
|
const previousEffect = activeEffect
|
||||||
|
const container = doc.createElement("div")
|
||||||
|
container.style.display = "contents"
|
||||||
|
container.setAttribute("role", "presentation")
|
||||||
|
activeOwner = { _cleanups: cleanups, _mounts: mounts }
|
||||||
|
activeEffect = null
|
||||||
|
|
||||||
|
const processResult = result => {
|
||||||
|
if (!result) return
|
||||||
if (result._isRuntime) {
|
if (result._isRuntime) {
|
||||||
cleanups.add(result.destroy);
|
cleanups.add(result.destroy)
|
||||||
container.appendChild(result.container);
|
container.appendChild(result.container)
|
||||||
} else if (isArr(result)) {
|
} else if (isArr(result)) {
|
||||||
result.forEach(processResult);
|
result.forEach(processResult)
|
||||||
} else {
|
} else {
|
||||||
container.appendChild(result instanceof Node ? result : createText(result));
|
container.appendChild(result instanceof Node ? result : doc.createTextNode(String(result == null ? "" : result)))
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
processResult(renderFn({ onCleanup: fn => cleanups.add(fn) }))
|
||||||
} finally { currentOwner = previousOwner; }
|
} finally {
|
||||||
|
activeOwner = previousOwner
|
||||||
|
activeEffect = previousEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
mounts.forEach(fn => fn())
|
||||||
return {
|
return {
|
||||||
_isRuntime: true,
|
_isRuntime: true,
|
||||||
container,
|
container,
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
cleanups.forEach((fn) => fn());
|
cleanups.forEach(fn => fn())
|
||||||
cleanupNode(container);
|
cleanupNode(container)
|
||||||
container.remove();
|
container.remove()
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const $ = (initialValue, storageKey = null) => {
|
|
||||||
const subscribers = new Set();
|
|
||||||
|
|
||||||
if (isFunc(initialValue)) {
|
|
||||||
let cachedValue, isDirty = true;
|
|
||||||
const effect = () => {
|
|
||||||
if (effect._deleted) return;
|
|
||||||
effect._deps.forEach((dep) => dep.delete(effect));
|
|
||||||
effect._deps.clear();
|
|
||||||
|
|
||||||
runWithContext(effect, () => {
|
|
||||||
const newValue = initialValue();
|
|
||||||
if (!Object.is(cachedValue, newValue) || isDirty) {
|
|
||||||
cachedValue = newValue;
|
|
||||||
isDirty = false;
|
|
||||||
triggerUpdate(subscribers);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
assign(effect, {
|
|
||||||
_deps: new Set(),
|
|
||||||
_isComputed: true,
|
|
||||||
_subs: subscribers,
|
|
||||||
_deleted: false,
|
|
||||||
markDirty: () => (isDirty = true),
|
|
||||||
stop: () => {
|
|
||||||
effect._deleted = true;
|
|
||||||
effect._deps.forEach((dep) => dep.delete(effect));
|
|
||||||
subscribers.clear();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentOwner) currentOwner.cleanups.add(effect.stop);
|
|
||||||
return () => { if (isDirty) effect(); trackSubscription(subscribers); return cachedValue; };
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = initialValue;
|
|
||||||
if (storageKey) {
|
|
||||||
try {
|
|
||||||
const saved = localStorage.getItem(storageKey);
|
|
||||||
if (saved !== null) value = JSON.parse(saved);
|
|
||||||
} catch (e) { console.warn("SigPro Storage Lock", e); }
|
|
||||||
}
|
|
||||||
|
|
||||||
return (...args) => {
|
|
||||||
if (args.length) {
|
|
||||||
const nextValue = isFunc(args[0]) ? args[0](value) : args[0];
|
|
||||||
if (!Object.is(value, nextValue)) {
|
|
||||||
value = nextValue;
|
|
||||||
if (storageKey) localStorage.setItem(storageKey, JSON.stringify(value));
|
|
||||||
triggerUpdate(subscribers);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
trackSubscription(subscribers);
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const $$ = (object, cache = new WeakMap()) => {
|
|
||||||
if (!isObj(object)) return object;
|
|
||||||
if (cache.has(object)) return cache.get(object);
|
|
||||||
|
|
||||||
const keySubscribers = {};
|
|
||||||
const proxy = new Proxy(object, {
|
|
||||||
get(target, key) {
|
|
||||||
if (activeEffect) trackSubscription(keySubscribers[key] ??= new Set());
|
|
||||||
const value = Reflect.get(target, key);
|
|
||||||
return isObj(value) ? $$(value, cache) : value;
|
|
||||||
},
|
|
||||||
set(target, key, value) {
|
|
||||||
if (Object.is(target[key], value)) return true;
|
|
||||||
const success = Reflect.set(target, key, value);
|
|
||||||
if (keySubscribers[key]) triggerUpdate(keySubscribers[key]);
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
cache.set(object, proxy);
|
|
||||||
return proxy;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Watch = (target, callbackFn) => {
|
|
||||||
const isExplicit = isArr(target);
|
|
||||||
const callback = isExplicit ? callbackFn : target;
|
|
||||||
if (!isFunc(callback)) return () => { };
|
|
||||||
|
|
||||||
const owner = currentOwner;
|
|
||||||
const runner = () => {
|
|
||||||
if (runner._deleted) return;
|
|
||||||
runner._deps.forEach((dep) => dep.delete(runner));
|
|
||||||
runner._deps.clear();
|
|
||||||
runner._cleanups.forEach((cleanup) => cleanup());
|
|
||||||
runner._cleanups.clear();
|
|
||||||
|
|
||||||
const previousOwner = currentOwner;
|
|
||||||
runner.depth = activeEffect ? activeEffect.depth + 1 : 0;
|
|
||||||
|
|
||||||
runWithContext(runner, () => {
|
|
||||||
currentOwner = { cleanups: runner._cleanups };
|
|
||||||
if (isExplicit) {
|
|
||||||
runWithContext(null, callback);
|
|
||||||
target.forEach((dep) => isFunc(dep) && dep());
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
currentOwner = previousOwner;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
assign(runner, {
|
|
||||||
_deps: new Set(),
|
|
||||||
_cleanups: new Set(),
|
|
||||||
_deleted: false,
|
|
||||||
stop: () => {
|
|
||||||
if (runner._deleted) return;
|
|
||||||
runner._deleted = true;
|
|
||||||
effectQueue.delete(runner);
|
|
||||||
runner._deps.forEach((dep) => dep.delete(runner));
|
|
||||||
runner._cleanups.forEach((cleanup) => cleanup());
|
|
||||||
if (owner) owner.cleanups.delete(runner.stop);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (owner) owner.cleanups.add(runner.stop);
|
|
||||||
runner();
|
|
||||||
return runner.stop;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Tag = (tag, props = {}, children = []) => {
|
|
||||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
|
||||||
children = props; props = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
|
|
||||||
const element = isSVG
|
|
||||||
? doc.createElementNS("http://www.w3.org/2000/svg", tag)
|
|
||||||
: createEl(tag);
|
|
||||||
|
|
||||||
element._cleanups = new Set();
|
|
||||||
element.onUnmount = (fn) => element._cleanups.add(fn);
|
|
||||||
const booleanAttributes = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
|
||||||
|
|
||||||
const updateAttribute = (name, value) => {
|
|
||||||
const sanitized = (name === 'src' || name === 'href') && String(value).toLowerCase().includes('javascript:') ? '#' : value;
|
|
||||||
if (booleanAttributes.includes(name)) {
|
|
||||||
element[name] = !!sanitized;
|
|
||||||
sanitized ? element.setAttribute(name, "") : element.removeAttribute(name);
|
|
||||||
} else {
|
|
||||||
sanitized == null ? element.removeAttribute(name) : element.setAttribute(name, sanitized);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (let [key, value] of Object.entries(props)) {
|
|
||||||
if (key === "ref") { (isFunc(value) ? value(element) : (value.current = element)); continue; }
|
|
||||||
|
|
||||||
const isSignal = isFunc(value);
|
|
||||||
if (key.startsWith("on")) {
|
|
||||||
const eventName = key.slice(2).toLowerCase().split(".")[0];
|
|
||||||
element.addEventListener(eventName, value);
|
|
||||||
element._cleanups.add(() => element.removeEventListener(eventName, value));
|
|
||||||
} else if (isSignal) {
|
|
||||||
element._cleanups.add(Watch(() => {
|
|
||||||
const currentVal = value();
|
|
||||||
key === "class" ? (element.className = currentVal || "") : updateAttribute(key, currentVal);
|
|
||||||
}));
|
|
||||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(element.tagName) && (key === "value" || key === "checked")) {
|
|
||||||
const event = key === "checked" ? "change" : "input";
|
|
||||||
const handler = (e) => value(e.target[key]);
|
|
||||||
element.addEventListener(event, handler);
|
|
||||||
element._cleanups.add(() => element.removeEventListener(event, handler));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateAttribute(key, value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const appendChildNode = (child) => {
|
|
||||||
if (isArr(child)) return child.forEach(appendChildNode);
|
|
||||||
if (isFunc(child)) {
|
|
||||||
const marker = createText("");
|
|
||||||
element.appendChild(marker);
|
|
||||||
let currentNodes = [];
|
|
||||||
element._cleanups.add(Watch(() => {
|
|
||||||
const result = child();
|
|
||||||
const nextNodes = (isArr(result) ? result : [result]).map((node) =>
|
|
||||||
node?._isRuntime ? node.container : node instanceof Node ? node : createText(node)
|
|
||||||
);
|
|
||||||
currentNodes.forEach((node) => { cleanupNode(node); node.remove(); });
|
|
||||||
nextNodes.forEach((node) => marker.parentNode?.insertBefore(node, marker));
|
|
||||||
currentNodes = nextNodes;
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
element.appendChild(child instanceof Node ? child : createText(child));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
appendChildNode(children);
|
|
||||||
return element;
|
|
||||||
};
|
|
||||||
|
|
||||||
const If = (condition, thenVal, otherwiseVal = null, transition = null) => {
|
|
||||||
const marker = createText("");
|
|
||||||
const container = Tag("div", { style: "display:contents" }, [marker]);
|
|
||||||
let currentView = null, lastState = null;
|
|
||||||
|
|
||||||
Watch(() => {
|
|
||||||
const state = !!(isFunc(condition) ? condition() : condition);
|
|
||||||
if (state === lastState) return;
|
|
||||||
lastState = state;
|
|
||||||
|
|
||||||
const dispose = () => { if (currentView) currentView.destroy(); currentView = null; };
|
|
||||||
|
|
||||||
if (currentView && !state && transition?.out) {
|
|
||||||
transition.out(currentView.container, dispose);
|
|
||||||
} else {
|
|
||||||
dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
const branch = state ? thenVal : otherwiseVal;
|
|
||||||
if (branch) {
|
|
||||||
currentView = Render(() => isFunc(branch) ? branch() : branch);
|
|
||||||
container.insertBefore(currentView.container, marker);
|
|
||||||
if (state && transition?.in) transition.in(currentView.container);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return container;
|
|
||||||
};
|
|
||||||
|
|
||||||
const For = (source, renderFn, keyFn, tag = "div", props = { style: "display:contents" }) => {
|
|
||||||
const marker = createText("");
|
|
||||||
const container = Tag(tag, props, [marker]);
|
|
||||||
let viewCache = new Map();
|
|
||||||
|
|
||||||
Watch(() => {
|
|
||||||
const items = (isFunc(source) ? source() : source) || [];
|
|
||||||
const nextCache = new Map();
|
|
||||||
const order = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
const item = items[i];
|
|
||||||
const key = keyFn ? keyFn(item, i) : i;
|
|
||||||
let view = viewCache.get(key);
|
|
||||||
|
|
||||||
if (!view) {
|
|
||||||
const result = renderFn(item, i);
|
|
||||||
view = result instanceof Node
|
|
||||||
? { container: result, destroy: () => { cleanupNode(result); result.remove(); } }
|
|
||||||
: Render(() => result);
|
|
||||||
}
|
|
||||||
|
|
||||||
viewCache.delete(key);
|
|
||||||
nextCache.set(key, view);
|
|
||||||
order.push(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
viewCache.forEach(v => v.destroy());
|
|
||||||
|
|
||||||
let anchor = marker;
|
|
||||||
for (let i = order.length - 1; i >= 0; i--) {
|
|
||||||
const view = nextCache.get(order[i]);
|
|
||||||
if (view.container.nextSibling !== anchor) {
|
|
||||||
container.insertBefore(view.container, anchor);
|
|
||||||
}
|
|
||||||
anchor = view.container;
|
|
||||||
}
|
|
||||||
viewCache = nextCache;
|
|
||||||
});
|
|
||||||
|
|
||||||
return container;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Router = (routes) => {
|
|
||||||
const currentPath = $(window.location.hash.replace(/^#/, "") || "/");
|
|
||||||
window.addEventListener("hashchange", () => currentPath(window.location.hash.replace(/^#/, "") || "/"));
|
|
||||||
const outlet = Tag("div", { class: "router-transition" });
|
|
||||||
let currentView = null;
|
|
||||||
|
|
||||||
Watch([currentPath], async () => {
|
|
||||||
const path = currentPath();
|
|
||||||
const route = routes.find(r => {
|
|
||||||
const routeParts = r.path.split("/").filter(Boolean);
|
|
||||||
const pathParts = path.split("/").filter(Boolean);
|
|
||||||
return routeParts.length === pathParts.length && routeParts.every((part, i) => part.startsWith(":") || part === pathParts[i]);
|
|
||||||
}) || routes.find(r => r.path === "*");
|
|
||||||
|
|
||||||
if (route) {
|
|
||||||
let component = route.component;
|
|
||||||
if (isFunc(component) && component.toString().includes('import')) {
|
|
||||||
component = (await component()).default || (await component());
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = {};
|
|
||||||
route.path.split("/").filter(Boolean).forEach((part, i) => {
|
|
||||||
if (part.startsWith(":")) params[part.slice(1)] = path.split("/").filter(Boolean)[i];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentView) currentView.destroy();
|
|
||||||
if (Router.params) Router.params(params);
|
|
||||||
|
|
||||||
currentView = Render(() => {
|
|
||||||
try {
|
|
||||||
return isFunc(component) ? component(params) : component;
|
|
||||||
} catch (e) {
|
|
||||||
return Tag("div", { class: "p-4 text-error" }, "Error loading view");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
outlet.appendChild(currentView.container);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return outlet;
|
|
||||||
};
|
|
||||||
|
|
||||||
Router.params = $({});
|
|
||||||
Router.to = (path) => (window.location.hash = path.replace(/^#?\/?/, "#/"));
|
|
||||||
Router.back = () => window.history.back();
|
|
||||||
Router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
|
||||||
|
|
||||||
const Mount = (component, target) => {
|
|
||||||
const targetEl = typeof target === "string" ? doc.querySelector(target) : target;
|
|
||||||
if (!targetEl) return;
|
|
||||||
if (MOUNTED_NODES.has(targetEl)) MOUNTED_NODES.get(targetEl).destroy();
|
|
||||||
const instance = Render(isFunc(component) ? component : () => component);
|
|
||||||
targetEl.replaceChildren(instance.container);
|
|
||||||
MOUNTED_NODES.set(targetEl, instance);
|
|
||||||
return instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const SigPro = { $, $$, Render, Watch, Tag, If, For, Router, Mount };
|
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
assign(window, SigPro);
|
|
||||||
const tags = `div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer address ul ol li dl dt dd a em strong small i b u mark time sub sup pre code blockquote details summary dialog form label input textarea select button option fieldset legend table thead tbody tfoot tr th td caption img video audio canvas svg iframe picture source progress meter`.split(" ");
|
|
||||||
tags.forEach((tag) => {
|
|
||||||
const helper = tag[0].toUpperCase() + tag.slice(1);
|
|
||||||
if (!(helper in window)) window[helper] = (p, c) => Tag(tag, p, c);
|
|
||||||
});
|
|
||||||
window.SigPro = Object.freeze(SigPro);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { $, $$, Render, Watch, Tag, If, For, Router, Mount };
|
const If = (cond, ifYes, ifNot = null) => {
|
||||||
export default SigPro;
|
const anchor = doc.createTextNode("")
|
||||||
|
const root = Tag("div", { style: "display:contents" }, [anchor])
|
||||||
|
let currentView = null
|
||||||
|
|
||||||
|
Watch(
|
||||||
|
() => !!(isFunc(cond) ? cond() : cond),
|
||||||
|
show => {
|
||||||
|
if (currentView) {
|
||||||
|
currentView.destroy()
|
||||||
|
currentView = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = show ? ifYes : ifNot
|
||||||
|
if (content) {
|
||||||
|
currentView = Render(() => isFunc(content) ? content() : content)
|
||||||
|
root.insertBefore(currentView.container, anchor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onUnmount(() => currentView?.destroy())
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
const For = (src, itemFn, keyFn) => {
|
||||||
|
const anchor = doc.createTextNode("")
|
||||||
|
const root = Tag("div", { style: "display:contents" }, [anchor])
|
||||||
|
let cache = new Map()
|
||||||
|
Watch(() => (isFunc(src) ? src() : src) || [], items => {
|
||||||
|
const nextCache = new Map()
|
||||||
|
const nextOrder = []
|
||||||
|
const newItems = items || []
|
||||||
|
for (let i = 0; i < newItems.length; i++) {
|
||||||
|
const item = newItems[i]
|
||||||
|
const key = keyFn ? keyFn(item, i) : (item?.id ?? i)
|
||||||
|
let view = cache.get(key)
|
||||||
|
if (!view) view = Render(() => itemFn(item, i))
|
||||||
|
else cache.delete(key)
|
||||||
|
nextCache.set(key, view)
|
||||||
|
nextOrder.push(view)
|
||||||
|
}
|
||||||
|
cache.forEach(view => view.destroy())
|
||||||
|
let lastRef = anchor
|
||||||
|
for (let i = nextOrder.length - 1; i >= 0; i--) {
|
||||||
|
const view = nextOrder[i]
|
||||||
|
const node = view.container
|
||||||
|
if (node.nextSibling !== lastRef) root.insertBefore(node, lastRef)
|
||||||
|
lastRef = node
|
||||||
|
}
|
||||||
|
cache = nextCache
|
||||||
|
})
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
const Router = routes => {
|
||||||
|
const getHash = () => window.location.hash.slice(1) || "/"
|
||||||
|
const path = $(getHash())
|
||||||
|
const handler = () => path(getHash())
|
||||||
|
window.addEventListener("hashchange", handler)
|
||||||
|
onUnmount(() => window.removeEventListener("hashchange", handler))
|
||||||
|
const hook = Tag("div", { class: "router-hook" })
|
||||||
|
let currentView = null
|
||||||
|
Watch([path], () => {
|
||||||
|
const cur = path()
|
||||||
|
const route = routes.find(r => {
|
||||||
|
const p1 = r.path.split("/").filter(Boolean)
|
||||||
|
const p2 = cur.split("/").filter(Boolean)
|
||||||
|
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i])
|
||||||
|
}) || routes.find(r => r.path === "*")
|
||||||
|
if (route) {
|
||||||
|
currentView?.destroy()
|
||||||
|
const params = {}
|
||||||
|
route.path.split("/").filter(Boolean).forEach((p, i) => {
|
||||||
|
if (p[0] === ":") params[p.slice(1)] = cur.split("/").filter(Boolean)[i]
|
||||||
|
})
|
||||||
|
Router.params(params)
|
||||||
|
currentView = Render(() => isFunc(route.component) ? route.component(params) : route.component)
|
||||||
|
hook.replaceChildren(currentView.container)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return hook
|
||||||
|
}
|
||||||
|
Router.params = $({})
|
||||||
|
Router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/")
|
||||||
|
Router.back = () => window.history.back()
|
||||||
|
Router.path = () => window.location.hash.replace(/^#/, "") || "/"
|
||||||
|
|
||||||
|
const Mount = (comp, target) => {
|
||||||
|
const t = typeof target === "string" ? doc.querySelector(target) : target
|
||||||
|
if (!t) return
|
||||||
|
if (MOUNTED_NODES.has(t)) MOUNTED_NODES.get(t).destroy()
|
||||||
|
const inst = Render(isFunc(comp) ? comp : () => comp)
|
||||||
|
t.replaceChildren(inst.container)
|
||||||
|
MOUNTED_NODES.set(t, inst)
|
||||||
|
return inst
|
||||||
|
}
|
||||||
|
|
||||||
|
const SigPro = Object.freeze({ $, $$, Watch, Tag, Render, If, For, Router, Mount, onMount, onUnmount, Batch })
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
Object.assign(window, SigPro)
|
||||||
|
"div span p h1 h2 h3 h4 h5 h6 br hr section article aside nav main header footer ul ol li a em strong pre code form label input textarea select button img svg"
|
||||||
|
.split(" ").forEach(t => window[t[0].toUpperCase() + t.slice(1)] = (p, c) => SigPro.Tag(t, p, c))
|
||||||
|
}
|
||||||
|
|
||||||
|
export { $, $$, Watch, Tag, Render, If, For, Router, Mount, onMount, onUnmount, Batch }
|
||||||
|
export default SigPro
|
||||||
1
sigpro2.min.js
vendored
1
sigpro2.min.js
vendored
File diff suppressed because one or more lines are too long
1
sigworkPro.min.js
vendored
1
sigworkPro.min.js
vendored
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user