Files
sigpro/sigworkPro.js
2026-04-09 21:59:59 +02:00

438 lines
14 KiB
JavaScript

const isFunction = (v) => typeof v === 'function';
const isNode = (v) => v instanceof Node;
const doc = typeof document !== "undefined" ? document : null;
let activeEffect = null;
const pendingEffects = new Set();
let flushScheduled = false;
const flushEffects = () => {
if (pendingEffects.size === 0) return;
const all = Array.from(pendingEffects);
pendingEffects.clear();
all.sort((a, b) => a.depth - b.depth);
for (let i = 0; i < all.length; i++) {
const e = all[i];
if (!e.disposed) e.execute();
}
flushScheduled = false;
};
const scheduleFlush = () => {
if (!flushScheduled) {
flushScheduled = true;
queueMicrotask(flushEffects);
}
};
const disposeEffectTree = (effect) => {
if (effect.disposed) return;
effect.disposed = true;
const stack = [effect];
while (stack.length) {
const cur = stack.pop();
if (cur.cleanups) {
for (const fn of cur.cleanups) fn();
cur.cleanups.clear();
}
if (cur.dependencies) {
for (const depSet of cur.dependencies) depSet.delete(cur);
cur.dependencies.clear();
}
if (cur.children) {
for (const child of cur.children) stack.push(child);
cur.children.clear();
}
}
};
const createEffect = (fn) => {
const effect = {
execute: null,
dependencies: new Set(),
cleanups: new Set(),
children: new Set(),
depth: activeEffect ? activeEffect.depth + 1 : 0,
disposed: false,
};
effect.execute = () => {
if (effect.disposed) return;
if (effect.dependencies) {
for (const depSet of effect.dependencies) depSet.delete(effect);
effect.dependencies.clear();
}
if (effect.cleanups) {
for (const fn of effect.cleanups) fn();
effect.cleanups.clear();
}
const prev = activeEffect;
activeEffect = effect;
try {
const cleanup = fn();
if (isFunction(cleanup)) effect.cleanups.add(cleanup);
} finally {
activeEffect = prev;
}
};
if (activeEffect) activeEffect.children.add(effect);
effect.execute();
return () => disposeEffectTree(effect);
};
export const Watch = createEffect;
export const effect = Watch;
export const scope = Watch;
const track = (subs) => {
if (activeEffect && !activeEffect.disposed) {
subs.add(activeEffect);
activeEffect.dependencies.add(subs);
}
};
const trigger = (subs) => {
if (!subs) return;
for (const eff of subs) {
if (eff !== activeEffect && !eff.disposed) pendingEffects.add(eff);
}
scheduleFlush();
};
export const $ = (initialValue) => {
const subs = new Set();
return {
get value() {
track(subs);
return initialValue;
},
set value(newVal) {
if (Object.is(newVal, initialValue)) return;
initialValue = newVal;
trigger(subs);
},
};
};
export const signal = $;
export const persistent = (initialValue, storageKey) => {
let stored = initialValue;
try {
const item = localStorage.getItem(storageKey);
if (item !== null) stored = JSON.parse(item);
} catch (e) {}
const sig = $(stored);
Watch(() => {
const val = sig.value;
try { localStorage.setItem(storageKey, JSON.stringify(val)); } catch (e) {}
});
return sig;
};
export const computed = (fn) => {
const s = $();
Watch(() => { s.value = fn(); });
return { get value() { return s.value; } };
};
export const untrack = (fn) => {
const prev = activeEffect;
activeEffect = null;
try { return fn(); } finally { activeEffect = prev; }
};
export const watch = (source, callback) => {
let first = true, oldVal;
return Watch(() => {
const newVal = isFunction(source) ? source() : source.value;
if (!first) untrack(() => callback(newVal, oldVal));
else first = false;
oldVal = newVal;
});
};
let currentComponentContext = null;
export const onMount = (fn) => {
if (currentComponentContext) currentComponentContext.mount.push(fn);
};
export const onUnmount = (fn) => {
if (currentComponentContext) currentComponentContext.unmount.push(fn);
};
export const provide = (key, val) => {
if (currentComponentContext) currentComponentContext.provisions[key] = val;
};
export const inject = (key, def) => {
if (currentComponentContext && key in currentComponentContext.provisions)
return currentComponentContext.provisions[key];
return def;
};
const setProperty = (el, key, val, isSVG) => {
if ((key === 'src' || key === 'href') && typeof val === 'string') {
const lower = val.toLowerCase();
if (lower.startsWith('javascript:') || lower.startsWith('data:text/html')) {
console.warn(`Bloqueado ${key}`);
val = '#';
}
}
if (key === 'class' || key === 'className') {
el.className = val || '';
} else if (key === 'style' && typeof val === 'object') {
Object.assign(el.style, val);
} else if (key in el && !isSVG) {
el[key] = val;
} else {
if (val == null || val === false) el.removeAttribute(key);
else if (val === true) el.setAttribute(key, '');
else el.setAttribute(key, val);
}
};
const appendChildNode = (parent, child) => {
if (child == null) return;
if (isFunction(child)) {
const anchor = doc.createTextNode('');
parent.appendChild(anchor);
let currentNodes = [];
Watch(() => {
const raw = child();
const next = (Array.isArray(raw) ? raw : [raw])
.flat(Infinity)
.filter(v => v != null)
.map(v => isNode(v) ? v : doc.createTextNode(String(v)));
for (let i = 0; i < currentNodes.length; i++) {
const n = currentNodes[i];
if (!next.includes(n)) removeNode(n);
}
let ref = anchor;
for (let i = next.length - 1; i >= 0; i--) {
const n = next[i];
if (n.parentNode !== parent) {
parent.insertBefore(n, ref);
if (n.componentContext) n.componentContext.mount.forEach(fn => fn());
}
ref = n;
}
currentNodes = next;
});
} else if (isNode(child)) {
parent.appendChild(child);
} else {
parent.appendChild(doc.createTextNode(String(child)));
}
};
const removeNode = (node) => {
if (node.componentStop) node.componentStop();
if (node.componentContext) node.componentContext.unmount.forEach(fn => fn());
if (node.leaveTransition) {
node.leaveTransition(() => node.remove());
} else {
node.remove();
}
};
export const Tag = (tag, props, ...children) => {
props = props || {};
children = children.flat(Infinity);
if (isFunction(tag)) {
const prevCtx = currentComponentContext;
const ctx = {
mount: [],
unmount: [],
provisions: { ...(prevCtx?.provisions || {}) },
};
currentComponentContext = ctx;
let rendered;
const stop = scope(() => {
rendered = tag(props, {
children,
emit: (ev, ...args) => {
const handler = props[`on${ev[0].toUpperCase()}${ev.slice(1)}`];
if (isFunction(handler)) handler(...args);
},
});
});
currentComponentContext = prevCtx;
if (isNode(rendered) || isFunction(rendered)) {
rendered.componentContext = ctx;
rendered.componentStop = stop;
}
return rendered;
}
if (!tag) return () => children;
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|use)$/.test(tag);
const el = isSVG
? doc.createElementNS('http://www.w3.org/2000/svg', tag)
: doc.createElement(tag);
for (const [k, v] of Object.entries(props)) {
if (k === 'ref') {
if (isFunction(v)) v(el);
else if (v && 'value' in v) v.value = el;
continue;
}
if (k.startsWith('on')) {
const ev = k.slice(2).toLowerCase();
el.addEventListener(ev, v);
onUnmount(() => el.removeEventListener(ev, v));
continue;
}
if (isFunction(v)) {
Watch(() => setProperty(el, k, v(), isSVG));
} else {
setProperty(el, k, v, isSVG);
}
}
for (const child of children) appendChildNode(el, child);
return el;
};
export const h = Tag;
export const If = ({ when, children }) => {
return () => (isFunction(when) ? when() : when) ? children[0] : children[1] || null;
};
export const For = ({ each, key, children }) => {
let cache = new Map();
return () => {
const items = isFunction(each) ? each() : each || [];
const newCache = new Map();
const nodes = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const itemKey = key ? (isFunction(key) ? key(item, i) : item[key]) : i;
let node = cache.get(itemKey);
if (!node) {
const childFn = children[0];
node = Tag(childFn, { item, index: i });
}
newCache.set(itemKey, node);
nodes.push(node);
}
cache = newCache;
return nodes;
};
};
export const Transition = ({ enter, leave, children }) => {
const decorate = (el) => {
if (!isNode(el)) return el;
if (enter) {
const [from, active, to] = enter;
requestAnimationFrame(() => {
el.classList.add(active);
requestAnimationFrame(() => {
el.classList.add(from);
el.classList.remove(active);
el.classList.add(to);
const onEnd = () => {
el.classList.remove(to, from);
el.removeEventListener('transitionend', onEnd);
};
el.addEventListener('transitionend', onEnd, { once: true });
});
});
}
if (leave) {
const [from, active, to] = leave;
el.leaveTransition = (done) => {
el.classList.add(active);
requestAnimationFrame(() => {
el.classList.add(from);
el.classList.remove(active);
el.classList.add(to);
const onEnd = () => {
el.classList.remove(to, from);
el.removeEventListener('transitionend', onEnd);
done();
};
el.addEventListener('transitionend', onEnd, { once: true });
});
};
}
return el;
};
const child = children[0];
if (!child) return null;
return isFunction(child) ? () => decorate(child()) : decorate(child);
};
const currentPath = $((window.location.hash.slice(1) || '/'));
window.addEventListener('hashchange', () => {
currentPath.value = window.location.hash.slice(1) || '/';
});
export const Router = ({ routes }) => {
const outlet = Tag('div', { class: 'router-outlet' });
let currentView = null;
Watch(() => {
const path = currentPath.value;
const segments = path.split('/').filter(Boolean);
const matched = routes.find(route => {
const rSeg = route.path.split('/').filter(Boolean);
return rSeg.length === segments.length && rSeg.every((s, i) => s[0] === ':' || s === segments[i]);
}) || routes.find(r => r.path === '*');
if (matched) {
if (currentView && currentView.componentStop) currentView.componentStop();
const params = {};
const rSeg = matched.path.split('/').filter(Boolean);
rSeg.forEach((s, i) => { if (s[0] === ':') params[s.slice(1)] = segments[i]; });
currentView = Tag(matched.component, { params });
outlet.innerHTML = '';
outlet.appendChild(currentView);
}
});
return outlet;
};
export const navigate = (to) => { window.location.hash = to.replace(/^#?\/?/, '#/'); };
export const back = () => window.history.back();
export const getCurrentPath = () => currentPath.value;
export const $$ = (obj, cache = new WeakMap()) => {
if (!obj || typeof obj !== 'object') return obj;
if (cache.has(obj)) return cache.get(obj);
const subs = {};
const proxy = new Proxy(obj, {
get: (t, k) => {
track(subs[k] ??= new Set());
const val = t[k];
return (val && typeof val === 'object') ? $$(val, cache) : val;
},
set: (t, k, v) => {
if (Object.is(t[k], v)) return true;
t[k] = v;
if (subs[k]) trigger(subs[k]);
return true;
},
});
cache.set(obj, proxy);
return proxy;
};
export const reactive = $$;
export const createApp = (Root, rootProps = {}) => (selector) => {
const target = typeof selector === 'string' ? doc.querySelector(selector) : selector;
if (!target) throw new Error(`No se encontró ${selector}`);
if (target.appUnmount) target.appUnmount();
const app = Tag(Root, rootProps);
target.appendChild(app);
if (app.componentContext) app.componentContext.mount.forEach(fn => fn());
target.appUnmount = () => removeNode(app);
return target.appUnmount;
};
const tags = 'div span p a button input form label ul li ol header footer main section article nav aside h1 h2 h3 h4 h5 h6 img svg path circle rect line polyline polygon g defs text use br hr pre code strong em table tr td th thead tbody tfoot select option textarea iframe video audio canvas'.split(' ');
tags.forEach(tag => {
const name = tag[0].toUpperCase() + tag.slice(1);
globalThis[name] = (props, ...children) => Tag(tag, props, ...children);
});
export const createElement = Tag;
export const Fragment = (props) => props.children;
export default {
$, signal, $$, reactive, persistent, computed, Watch, effect, scope, watch, untrack,
onMount, onUnmount, provide, inject, Tag, h, createElement, Fragment, If, For, Transition,
Router, navigate, back, getCurrentPath, createApp,
};