Actualizar hibrido.js
This commit is contained in:
733
hibrido.js
733
hibrido.js
@@ -1,468 +1,453 @@
|
||||
type EffectFn = () => void;
|
||||
type CleanupFn = () => void;
|
||||
// SigPro.ts - Versión simplificada (sin If/For) y tipada
|
||||
|
||||
let currentEffect: EffectFn | null = null;
|
||||
const effectStack: EffectFn[] = [];
|
||||
type EffectFn = {
|
||||
(): void;
|
||||
_deps: Set<Set<EffectFn>>;
|
||||
_deleted?: boolean;
|
||||
_isComputed?: boolean;
|
||||
_subs?: Set<EffectFn>;
|
||||
depth?: number;
|
||||
stop?: () => void;
|
||||
_cleanups?: Set<() => void>;
|
||||
};
|
||||
|
||||
let pendingBatch = false;
|
||||
const batchQueue = new Set < EffectFn > ();
|
||||
type Owner = { cleanups: Set<() => void> } | null;
|
||||
|
||||
function flushBatch() {
|
||||
const effects = Array.from(batchQueue);
|
||||
batchQueue.clear();
|
||||
pendingBatch = false;
|
||||
effects.forEach(e => e());
|
||||
}
|
||||
|
||||
function scheduleEffect(effect: EffectFn) {
|
||||
batchQueue.add(effect);
|
||||
if (!pendingBatch) {
|
||||
pendingBatch = true;
|
||||
queueMicrotask(flushBatch);
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupEffect(effect: EffectFn & { deps: Set<Set<EffectFn>> }) {
|
||||
effect.deps.forEach(dep => dep.delete(effect));
|
||||
effect.deps.clear();
|
||||
}
|
||||
|
||||
export interface Signal<T> {
|
||||
type Signal<T> = {
|
||||
(): T;
|
||||
(value: T | ((prev: T) => T)): T;
|
||||
_subs: Set<EffectFn & { deps: Set<Set<EffectFn>> }>;
|
||||
_value: T;
|
||||
_storageKey?: string;
|
||||
(next: T | ((prev: T) => T)): T;
|
||||
_isSignal?: boolean;
|
||||
};
|
||||
|
||||
type ComputedSignal<T> = Signal<T> & { stop: () => void };
|
||||
|
||||
type Runtime = {
|
||||
_isRuntime: true;
|
||||
container: HTMLElement;
|
||||
destroy: () => void;
|
||||
};
|
||||
|
||||
type Component = (props?: Record<string, any>, children?: any[]) => any;
|
||||
|
||||
type TagFunction = (props?: any, children?: any) => HTMLElement;
|
||||
|
||||
// --- Estado interno ---
|
||||
let activeEffect: EffectFn | null = null;
|
||||
let currentOwner: Owner = null;
|
||||
const effectQueue = new Set<EffectFn>();
|
||||
let isFlushing = false;
|
||||
const MOUNTED_NODES = new WeakMap<Element, Runtime>();
|
||||
|
||||
// --- Helpers ---
|
||||
const doc = document;
|
||||
const isArr = Array.isArray;
|
||||
const assign = Object.assign;
|
||||
const createEl = (t: string) => doc.createElement(t);
|
||||
const createText = (t: any) => doc.createTextNode(String(t ?? ""));
|
||||
const isFunc = (f: any): f is Function => typeof f === "function";
|
||||
const isObj = (o: any): o is object => typeof o === "object" && o !== null;
|
||||
|
||||
const runWithContext = <T>(effect: EffectFn | null, callback: () => T): T => {
|
||||
const prev = activeEffect;
|
||||
activeEffect = effect;
|
||||
try {
|
||||
return callback();
|
||||
} finally {
|
||||
activeEffect = prev;
|
||||
}
|
||||
};
|
||||
|
||||
const cleanupNode = (node: Node) => {
|
||||
if ((node as any)._cleanups) {
|
||||
(node as any)._cleanups.forEach((dispose: () => void) => dispose());
|
||||
(node as any)._cleanups.clear();
|
||||
}
|
||||
node.childNodes?.forEach(cleanupNode);
|
||||
};
|
||||
|
||||
const flushEffects = () => {
|
||||
if (isFlushing) return;
|
||||
isFlushing = true;
|
||||
while (effectQueue.size) {
|
||||
const sorted = Array.from(effectQueue).sort((a, b) => (a.depth || 0) - (b.depth || 0));
|
||||
effectQueue.clear();
|
||||
for (const eff of sorted) {
|
||||
if (!eff._deleted) eff();
|
||||
}
|
||||
}
|
||||
isFlushing = false;
|
||||
};
|
||||
|
||||
const trackSubscription = (subscribers: Set<EffectFn>) => {
|
||||
if (activeEffect && !activeEffect._deleted) {
|
||||
subscribers.add(activeEffect);
|
||||
activeEffect._deps.add(subscribers);
|
||||
}
|
||||
};
|
||||
|
||||
const triggerUpdate = (subscribers: Set<EffectFn>) => {
|
||||
subscribers.forEach(effect => {
|
||||
if (effect === activeEffect || effect._deleted) return;
|
||||
if (effect._isComputed) {
|
||||
(effect as any).markDirty?.();
|
||||
if (effect._subs) triggerUpdate(effect._subs);
|
||||
} else {
|
||||
effectQueue.add(effect);
|
||||
}
|
||||
});
|
||||
if (!isFlushing) queueMicrotask(flushEffects);
|
||||
};
|
||||
|
||||
// --- API pública ---
|
||||
|
||||
/**
|
||||
* Crea una señal reactiva o un valor computado.
|
||||
* @param initial - Valor inicial o función computada
|
||||
* @param storageKey - Opcional, persistencia en localStorage
|
||||
*/
|
||||
export function $<T>(initial: T | (() => T), storageKey?: string): Signal<T> {
|
||||
const subscribers = new Set<EffectFn>();
|
||||
|
||||
if (isFunc(initial)) {
|
||||
// Computado
|
||||
let cachedValue: T;
|
||||
let isDirty = true;
|
||||
|
||||
const effect = (() => {
|
||||
if (effect._deleted) return;
|
||||
effect._deps.forEach(dep => dep.delete(effect));
|
||||
effect._deps.clear();
|
||||
|
||||
runWithContext(effect, () => {
|
||||
const newValue = (initial as () => T)();
|
||||
if (!Object.is(cachedValue, newValue) || isDirty) {
|
||||
cachedValue = newValue;
|
||||
isDirty = false;
|
||||
triggerUpdate(subscribers);
|
||||
}
|
||||
});
|
||||
}) as EffectFn & { markDirty: () => void; stop: () => void };
|
||||
|
||||
assign(effect, {
|
||||
_deps: new Set<Set<EffectFn>>(),
|
||||
_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);
|
||||
|
||||
const signal = ((...args: [] | [T | ((prev: T) => T)]) => {
|
||||
if (args.length === 0) {
|
||||
if (isDirty) effect();
|
||||
trackSubscription(subscribers);
|
||||
return cachedValue;
|
||||
} else {
|
||||
// Los computados no tienen setter
|
||||
return cachedValue;
|
||||
}
|
||||
}) as Signal<T>;
|
||||
signal._isSignal = true;
|
||||
return signal;
|
||||
}
|
||||
|
||||
function isFunction<T>(v: any): v is (...args: any[]) => T {
|
||||
return typeof v === 'function';
|
||||
}
|
||||
|
||||
export function $<T>(initialValue: T, storageKey?: string): Signal<T> {
|
||||
let value: T = initialValue;
|
||||
const subs = new Set < EffectFn & { deps: Set < Set < EffectFn >> } > ();
|
||||
|
||||
// Señal normal
|
||||
let value = initial as T;
|
||||
if (storageKey) {
|
||||
try {
|
||||
const saved = localStorage.getItem(storageKey);
|
||||
if (saved !== null) value = JSON.parse(saved);
|
||||
} catch (e) { console.warn("Storage error", e); }
|
||||
} catch (e) {
|
||||
console.warn("SigPro storage error", e);
|
||||
}
|
||||
}
|
||||
|
||||
const signal = ((arg?: T | ((prev: T) => T)): T => {
|
||||
if (arguments.length === 0) {
|
||||
if (currentEffect) {
|
||||
subs.add(currentEffect as any);
|
||||
(currentEffect as any).deps.add(subs);
|
||||
}
|
||||
return value;
|
||||
} else {
|
||||
const next = isFunction(arg) ? (arg as (prev: T) => T)(value) : arg;
|
||||
if (Object.is(value, next)) return value;
|
||||
value = next as T;
|
||||
const signal = ((...args: [] | [T | ((prev: T) => T)]) => {
|
||||
if (args.length) {
|
||||
const next = isFunc(args[0]) ? (args[0] as (prev: T) => T)(value) : args[0];
|
||||
if (!Object.is(value, next)) {
|
||||
value = next;
|
||||
if (storageKey) localStorage.setItem(storageKey, JSON.stringify(value));
|
||||
for (const eff of Array.from(subs)) {
|
||||
if (eff && !eff._deleted) scheduleEffect(eff);
|
||||
triggerUpdate(subscribers);
|
||||
}
|
||||
}
|
||||
trackSubscription(subscribers);
|
||||
return value;
|
||||
}
|
||||
}) as Signal<T>;
|
||||
signal._subs = subs;
|
||||
signal._value = initialValue;
|
||||
if (storageKey) signal._storageKey = storageKey;
|
||||
signal._isSignal = true;
|
||||
return signal;
|
||||
}
|
||||
|
||||
export function Watch(effectFn: () => void, deps?: Array<() => any>): () => void {
|
||||
const runner = (() => {
|
||||
if (runner._deleted) return;
|
||||
cleanupEffect(runner);
|
||||
effectStack.push(runner);
|
||||
const prev = currentEffect;
|
||||
currentEffect = runner;
|
||||
try {
|
||||
effectFn();
|
||||
} finally {
|
||||
currentEffect = prev;
|
||||
effectStack.pop();
|
||||
/**
|
||||
* Convierte un objeto en un proxy reactivo profundo.
|
||||
*/
|
||||
export function $$<T extends object>(object: T, cache = new WeakMap()): T {
|
||||
if (!isObj(object)) return object;
|
||||
if (cache.has(object)) return cache.get(object);
|
||||
|
||||
const keySubscribers: Record<string | symbol, Set<EffectFn>> = {};
|
||||
const proxy = new Proxy(object, {
|
||||
get(target, key, receiver) {
|
||||
if (activeEffect) {
|
||||
const subs = keySubscribers[key] ??= new Set();
|
||||
trackSubscription(subs);
|
||||
}
|
||||
}) as EffectFn & { deps: Set<Set<EffectFn>>; _deleted?: boolean; _cleanups?: Set<() => void> };
|
||||
|
||||
runner.deps = new Set();
|
||||
runner._deleted = false;
|
||||
|
||||
runner();
|
||||
|
||||
return () => {
|
||||
runner._deleted = true;
|
||||
cleanupEffect(runner);
|
||||
if (runner._cleanups) runner._cleanups.forEach(fn => fn());
|
||||
};
|
||||
}
|
||||
|
||||
export function computed<T>(fn: () => T): Signal<T> {
|
||||
const sig = $(undefined as T);
|
||||
let isDirty = true;
|
||||
let cached: T;
|
||||
const stop = Watch(() => {
|
||||
const newVal = fn();
|
||||
if (isDirty || !Object.is(cached, newVal)) {
|
||||
cached = newVal;
|
||||
isDirty = false;
|
||||
sig(cached);
|
||||
const value = Reflect.get(target, key, receiver);
|
||||
return isObj(value) ? $$(value, cache) : value;
|
||||
},
|
||||
set(target, key, value, receiver) {
|
||||
if (Object.is(target[key as keyof T], value)) return true;
|
||||
const success = Reflect.set(target, key, value, receiver);
|
||||
if (keySubscribers[key]) triggerUpdate(keySubscribers[key]);
|
||||
return success;
|
||||
}
|
||||
});
|
||||
(sig as any)._stop = stop;
|
||||
return sig;
|
||||
}
|
||||
|
||||
const proxyCache = new WeakMap < object, any> ();
|
||||
|
||||
export function $$<T extends object>(obj: T): T {
|
||||
if (typeof obj !== 'object' || obj === null) return obj;
|
||||
if (proxyCache.has(obj)) return proxyCache.get(obj);
|
||||
|
||||
const listeners = new Map < string | symbol, Set<EffectFn & { deps: Set < Set < EffectFn >> } >> ();
|
||||
|
||||
const proxy = new Proxy(obj, {
|
||||
get(target, p, receiver) {
|
||||
if (currentEffect) {
|
||||
let set = listeners.get(p);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
listeners.set(p, set);
|
||||
}
|
||||
set.add(currentEffect as any);
|
||||
(currentEffect as any).deps.add(set);
|
||||
}
|
||||
const val = Reflect.get(target, p, receiver);
|
||||
if (typeof val === 'object' && val !== null) return $$(val);
|
||||
return val;
|
||||
},
|
||||
set(target, p, value, receiver) {
|
||||
const old = Reflect.get(target, p, receiver);
|
||||
if (Object.is(old, value)) return true;
|
||||
const res = Reflect.set(target, p, value, receiver);
|
||||
const set = listeners.get(p);
|
||||
if (set) {
|
||||
for (const eff of Array.from(set)) {
|
||||
if (!eff._deleted) scheduleEffect(eff);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
deleteProperty(target, p) {
|
||||
const had = Reflect.has(target, p);
|
||||
const res = Reflect.deleteProperty(target, p);
|
||||
if (had && res) {
|
||||
const set = listeners.get(p);
|
||||
if (set) {
|
||||
for (const eff of Array.from(set)) scheduleEffect(eff);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
});
|
||||
proxyCache.set(obj, proxy);
|
||||
cache.set(object, proxy);
|
||||
return proxy;
|
||||
}
|
||||
|
||||
let currentOwner: { cleanups: Set<CleanupFn> } | null = null;
|
||||
/**
|
||||
* Ejecuta un efecto que se re-ejecuta cuando sus dependencias cambian.
|
||||
* @param target - Función o array de señales/dependencias
|
||||
* @param callback - Si target es array, callback a ejecutar
|
||||
*/
|
||||
export function Watch(target: (() => any) | any[], callback?: () => void): () => void {
|
||||
const isExplicit = isArr(target);
|
||||
const cb = isExplicit ? callback! : (target as () => void);
|
||||
if (!isFunc(cb)) return () => {};
|
||||
|
||||
function runWithOwner<T>(owner: { cleanups: Set<CleanupFn> }, fn: () => T): T {
|
||||
const prev = currentOwner;
|
||||
currentOwner = owner;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
currentOwner = prev;
|
||||
const owner = currentOwner;
|
||||
const runner = (() => {
|
||||
if (runner._deleted) return;
|
||||
runner._deps.forEach(dep => dep.delete(runner));
|
||||
runner._deps.clear();
|
||||
runner._cleanups?.forEach(clean => clean());
|
||||
runner._cleanups?.clear();
|
||||
|
||||
const prevOwner = currentOwner;
|
||||
runner.depth = activeEffect ? activeEffect.depth! + 1 : 0;
|
||||
|
||||
runWithContext(runner, () => {
|
||||
currentOwner = { cleanups: runner._cleanups ??= new Set() };
|
||||
if (isExplicit) {
|
||||
runWithContext(null, cb);
|
||||
(target as any[]).forEach(dep => isFunc(dep) && dep());
|
||||
} else {
|
||||
cb();
|
||||
}
|
||||
currentOwner = prevOwner;
|
||||
});
|
||||
}) as EffectFn & { _cleanups?: Set<() => void>; stop: () => void };
|
||||
|
||||
assign(runner, {
|
||||
_deps: new Set<Set<EffectFn>>(),
|
||||
_cleanups: new Set<() => void>(),
|
||||
_deleted: false,
|
||||
stop: () => {
|
||||
if (runner._deleted) return;
|
||||
runner._deleted = true;
|
||||
effectQueue.delete(runner);
|
||||
runner._deps.forEach(dep => dep.delete(runner));
|
||||
runner._cleanups?.forEach(clean => clean());
|
||||
if (owner) owner.cleanups.delete(runner.stop);
|
||||
}
|
||||
});
|
||||
|
||||
if (owner) owner.cleanups.add(runner.stop);
|
||||
runner();
|
||||
return runner.stop;
|
||||
}
|
||||
|
||||
function addCleanup(fn: CleanupFn) {
|
||||
if (currentOwner) currentOwner.cleanups.add(fn);
|
||||
}
|
||||
/**
|
||||
* Renderiza un componente reactivo con ciclo de vida.
|
||||
*/
|
||||
export function Render(renderFn: (ctx: { onCleanup: (fn: () => void) => void }) => any): Runtime {
|
||||
const cleanups = new Set<() => void>();
|
||||
const prevOwner = currentOwner;
|
||||
const container = createEl("div");
|
||||
container.style.display = "contents";
|
||||
currentOwner = { cleanups };
|
||||
|
||||
export interface Runtime {
|
||||
container: HTMLElement | DocumentFragment;
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export function Render(renderFn: (ctx: { onCleanup: (fn: CleanupFn) => void }) => any): Runtime {
|
||||
const cleanups = new Set < CleanupFn > ();
|
||||
const container = document.createElement('div');
|
||||
container.style.display = 'contents';
|
||||
let currentNodes: Node[] = [];
|
||||
|
||||
const processResult = (result: any): Node[] => {
|
||||
if (result == null) return [];
|
||||
if (Array.isArray(result)) {
|
||||
return result.flatMap(processResult);
|
||||
}
|
||||
const processResult = (result: any) => {
|
||||
if (!result) return;
|
||||
if (result._isRuntime) {
|
||||
cleanups.add(result.destroy);
|
||||
return [result.container];
|
||||
container.appendChild(result.container);
|
||||
} else if (isArr(result)) {
|
||||
result.forEach(processResult);
|
||||
} else {
|
||||
container.appendChild(result instanceof Node ? result : createText(result));
|
||||
}
|
||||
if (result instanceof Node) return [result];
|
||||
return [document.createTextNode(String(result))];
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
for (const node of currentNodes) {
|
||||
cleanupNode(node);
|
||||
node.remove();
|
||||
try {
|
||||
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
||||
} finally {
|
||||
currentOwner = prevOwner;
|
||||
}
|
||||
currentNodes = [];
|
||||
let result: any;
|
||||
runWithOwner({ cleanups }, () => {
|
||||
result = renderFn({ onCleanup: addCleanup });
|
||||
});
|
||||
currentNodes = processResult(result);
|
||||
container.append(...currentNodes);
|
||||
};
|
||||
|
||||
render();
|
||||
|
||||
return {
|
||||
_isRuntime: true,
|
||||
container,
|
||||
destroy: () => {
|
||||
cleanups.forEach(fn => fn());
|
||||
for (const node of currentNodes) cleanupNode(node);
|
||||
cleanupNode(container);
|
||||
container.remove();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function cleanupNode(node: Node) {
|
||||
if ((node as any)._cleanups) {
|
||||
(node as any)._cleanups.forEach((fn: CleanupFn) => fn());
|
||||
(node as any)._cleanups.clear();
|
||||
}
|
||||
node.childNodes.forEach(child => cleanupNode(child));
|
||||
}
|
||||
|
||||
const booleanAttributes = new Set(["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"]);
|
||||
|
||||
function sanitizeURL(url: string): string {
|
||||
const s = String(url).toLowerCase();
|
||||
if (s.includes('javascript:')) return '#';
|
||||
return url;
|
||||
}
|
||||
|
||||
export function Tag(tag: string, propsOrChildren?: any, children?: any): HTMLElement {
|
||||
let props: Record<string, any> = {};
|
||||
let childArray: any[] = [];
|
||||
|
||||
if (propsOrChildren === undefined || propsOrChildren instanceof Node || Array.isArray(propsOrChildren) || typeof propsOrChildren !== 'object') {
|
||||
childArray = propsOrChildren ?? [];
|
||||
} else {
|
||||
props = propsOrChildren;
|
||||
childArray = children ?? [];
|
||||
/**
|
||||
* Crea un elemento DOM con atributos e hijos reactivos.
|
||||
*/
|
||||
export function Tag(tag: string, props: any = {}, children: any = []): HTMLElement {
|
||||
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 el = isSVG
|
||||
? document.createElementNS("http://www.w3.org/2000/svg", tag)
|
||||
: document.createElement(tag);
|
||||
? doc.createElementNS("http://www.w3.org/2000/svg", tag)
|
||||
: createEl(tag) as HTMLElement;
|
||||
|
||||
const cleanups = new Set < CleanupFn > ();
|
||||
(el as any)._cleanups = cleanups;
|
||||
(el as any)._cleanups = new Set<() => void>();
|
||||
(el as any).onUnmount = (fn: () => void) => (el as any)._cleanups.add(fn);
|
||||
|
||||
const updateAttribute = (name: string, value: any) => {
|
||||
if (name === 'href' || name === 'src') {
|
||||
value = sanitizeURL(value);
|
||||
}
|
||||
if (booleanAttributes.has(name)) {
|
||||
const bool = !!value;
|
||||
if (bool) el.setAttribute(name, '');
|
||||
else el.removeAttribute(name);
|
||||
(el as any)[name] = bool;
|
||||
} else if (value === undefined || value === null) {
|
||||
el.removeAttribute(name);
|
||||
const booleanAttrs = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
||||
|
||||
const updateAttr = (name: string, value: any) => {
|
||||
const safe = (name === 'src' || name === 'href') && String(value).includes('javascript:') ? '#' : value;
|
||||
if (booleanAttrs.includes(name)) {
|
||||
(el as any)[name] = !!safe;
|
||||
safe ? el.setAttribute(name, "") : el.removeAttribute(name);
|
||||
} else {
|
||||
el.setAttribute(name, String(value));
|
||||
safe == null ? el.removeAttribute(name) : el.setAttribute(name, String(safe));
|
||||
}
|
||||
};
|
||||
|
||||
for (const [key, val] of Object.entries(props)) {
|
||||
if (key === 'ref') {
|
||||
if (typeof val === 'function') val(el);
|
||||
else if (val && typeof val === 'object') val.current = el;
|
||||
if (key === "ref") {
|
||||
if (isFunc(val)) val(el);
|
||||
else if (val && typeof val === "object") (val as { current: any }).current = el;
|
||||
continue;
|
||||
}
|
||||
if (key.startsWith('on')) {
|
||||
const eventName = key.slice(2).toLowerCase();
|
||||
const handler = val;
|
||||
el.addEventListener(eventName, handler);
|
||||
cleanups.add(() => el.removeEventListener(eventName, handler));
|
||||
continue;
|
||||
}
|
||||
if ((tag === 'input' || tag === 'textarea' || tag === 'select') &&
|
||||
(key === 'value' || key === 'checked') &&
|
||||
typeof val === 'function' && (val as any)._subs) {
|
||||
const eventType = key === 'checked' ? 'change' : 'input';
|
||||
const handler = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const newValue = key === 'checked' ? target.checked : target.value;
|
||||
(val as Signal<any>)(newValue);
|
||||
};
|
||||
el.addEventListener(eventType, handler);
|
||||
cleanups.add(() => el.removeEventListener(eventType, handler));
|
||||
}
|
||||
if (typeof val === 'function' && !(val as any)._isSignal) {
|
||||
if ((val as any)._subs) {
|
||||
const dispose = Watch(() => {
|
||||
const current = val();
|
||||
if (key === 'class') {
|
||||
el.className = current || '';
|
||||
} else if (key === 'style' && typeof current === 'object') {
|
||||
Object.assign(el.style, current);
|
||||
} else {
|
||||
updateAttribute(key, current);
|
||||
}
|
||||
});
|
||||
cleanups.add(dispose);
|
||||
const init = val();
|
||||
if (key === 'class') el.className = init || '';
|
||||
else if (key === 'style' && typeof init === 'object') Object.assign(el.style, init);
|
||||
else updateAttribute(key, init);
|
||||
} else {
|
||||
updateAttribute(key, val);
|
||||
|
||||
const isReactive = isFunc(val) && (val as any)._isSignal === true;
|
||||
if (key.startsWith("on")) {
|
||||
const eventName = key.slice(2).toLowerCase().split(".")[0];
|
||||
el.addEventListener(eventName, val);
|
||||
(el as any)._cleanups.add(() => el.removeEventListener(eventName, val));
|
||||
} else if (isReactive) {
|
||||
(el as any)._cleanups.add(Watch(() => {
|
||||
const currentVal = (val as Signal<any>)();
|
||||
if (key === "class") el.className = currentVal || "";
|
||||
else updateAttr(key, currentVal);
|
||||
}));
|
||||
if (["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName) && (key === "value" || key === "checked")) {
|
||||
const ev = key === "checked" ? "change" : "input";
|
||||
const handler = (e: Event) => (val as Signal<any>)((e.target as any)[key]);
|
||||
el.addEventListener(ev, handler);
|
||||
(el as any)._cleanups.add(() => el.removeEventListener(ev, handler));
|
||||
}
|
||||
} else {
|
||||
updateAttribute(key, val);
|
||||
updateAttr(key, val);
|
||||
}
|
||||
}
|
||||
|
||||
const appendChildNode = (child: any) => {
|
||||
if (child == null || child === false || child === true) return;
|
||||
if (Array.isArray(child)) {
|
||||
child.forEach(appendChildNode);
|
||||
return;
|
||||
}
|
||||
if (typeof child === 'function' && (child as any)._subs) {
|
||||
const marker = document.createComment('sig');
|
||||
if (isArr(child)) return child.forEach(appendChildNode);
|
||||
if (isFunc(child) && (child as any)._isSignal !== true) {
|
||||
const marker = createText("");
|
||||
el.appendChild(marker);
|
||||
let currentNodes: Node[] = [];
|
||||
const dispose = Watch(() => {
|
||||
const value = child();
|
||||
const newNodes: Node[] = [];
|
||||
const process = (v: any) => {
|
||||
if (v == null) return;
|
||||
if (Array.isArray(v)) v.forEach(process);
|
||||
else if (v._isRuntime) {
|
||||
newNodes.push(v.container);
|
||||
cleanups.add(v.destroy);
|
||||
} else if (v instanceof Node) newNodes.push(v);
|
||||
else newNodes.push(document.createTextNode(String(v)));
|
||||
};
|
||||
process(value);
|
||||
for (const n of currentNodes) {
|
||||
cleanupNode(n);
|
||||
n.remove();
|
||||
}
|
||||
for (const n of newNodes) {
|
||||
marker.parentNode?.insertBefore(n, marker);
|
||||
}
|
||||
currentNodes = newNodes;
|
||||
});
|
||||
cleanups.add(dispose);
|
||||
} else if (child && child._isRuntime) {
|
||||
el.appendChild(child.container);
|
||||
cleanups.add(child.destroy);
|
||||
} else if (child instanceof Node) {
|
||||
el.appendChild(child);
|
||||
(el as any)._cleanups.add(Watch(() => {
|
||||
const result = child();
|
||||
const nextNodes = (isArr(result) ? result : [result]).map((node: any) =>
|
||||
node?._isRuntime ? node.container : (node instanceof Node ? node : createText(node))
|
||||
);
|
||||
currentNodes.forEach(n => { cleanupNode(n); n.remove(); });
|
||||
nextNodes.forEach(n => marker.parentNode?.insertBefore(n, marker));
|
||||
currentNodes = nextNodes;
|
||||
}));
|
||||
} else {
|
||||
el.appendChild(document.createTextNode(String(child)));
|
||||
el.appendChild(child instanceof Node ? child : createText(child));
|
||||
}
|
||||
};
|
||||
|
||||
appendChildNode(childArray);
|
||||
appendChildNode(children);
|
||||
return el;
|
||||
}
|
||||
|
||||
// --- Router simple ---
|
||||
export const Router = {
|
||||
params: $({} as Record<string, string>),
|
||||
currentPath: $('/'),
|
||||
routes: [] as Array<{ path: string; component: any }>,
|
||||
|
||||
init(routes: Array<{ path: string; component: any }>) {
|
||||
this.routes = routes;
|
||||
const update = () => {
|
||||
const hash = window.location.hash.slice(1) || '/';
|
||||
this.currentPath(hash);
|
||||
};
|
||||
window.addEventListener('hashchange', update);
|
||||
update();
|
||||
return this.outlet();
|
||||
},
|
||||
|
||||
outlet() {
|
||||
const outlet = Tag('div');
|
||||
to: (path: string) => { window.location.hash = path.replace(/^#?\/?/, "#/"); },
|
||||
back: () => window.history.back(),
|
||||
path: () => window.location.hash.replace(/^#/, "") || "/",
|
||||
outlet: (routes: Array<{ path: string; component: Component }>) => {
|
||||
const currentPath = $(Router.path());
|
||||
window.addEventListener("hashchange", () => currentPath(Router.path()));
|
||||
const outlet = Tag("div", { class: "router-outlet" });
|
||||
let currentView: Runtime | null = null;
|
||||
Watch(() => {
|
||||
const path = this.currentPath();
|
||||
const route = this.routes.find(r => {
|
||||
const parts = r.path.split('/').filter(Boolean);
|
||||
const pathParts = path.split('/').filter(Boolean);
|
||||
if (parts.length !== pathParts.length) return false;
|
||||
return parts.every((part, i) => part.startsWith(':') || part === pathParts[i]);
|
||||
}) || this.routes.find(r => r.path === '*');
|
||||
|
||||
Watch(currentPath, async () => {
|
||||
const path = currentPath();
|
||||
const route = routes.find(r => {
|
||||
const rParts = r.path.split("/").filter(Boolean);
|
||||
const pParts = path.split("/").filter(Boolean);
|
||||
return rParts.length === pParts.length && rParts.every((part, i) => part.startsWith(":") || part === pParts[i]);
|
||||
}) || routes.find(r => r.path === "*");
|
||||
|
||||
if (route) {
|
||||
const params: Record<string, string> = {};
|
||||
const parts = route.path.split('/').filter(Boolean);
|
||||
const pathParts = path.split('/').filter(Boolean);
|
||||
parts.forEach((part, i) => {
|
||||
if (part.startsWith(':')) params[part.slice(1)] = pathParts[i];
|
||||
});
|
||||
this.params(params);
|
||||
let Comp = route.component;
|
||||
if (typeof Comp === 'function' && Comp.toString().includes('import')) {
|
||||
Comp = () => import(/* @vite-ignore */ route.component).then(m => m.default);
|
||||
let comp = route.component;
|
||||
if (isFunc(comp) && comp.toString().includes('import')) {
|
||||
comp = (await (comp as any)()).default || (await (comp as any)());
|
||||
}
|
||||
const view = Render(() => {
|
||||
const C = typeof Comp === 'function' ? Comp : () => Comp;
|
||||
return C(params);
|
||||
const params: Record<string, string> = {};
|
||||
route.path.split("/").filter(Boolean).forEach((part, i) => {
|
||||
if (part.startsWith(":")) params[part.slice(1)] = path.split("/").filter(Boolean)[i];
|
||||
});
|
||||
Router.params(params);
|
||||
if (currentView) currentView.destroy();
|
||||
currentView = view;
|
||||
outlet.innerHTML = '';
|
||||
outlet.appendChild(view.container);
|
||||
currentView = Render(() => isFunc(comp) ? comp(params) : comp);
|
||||
outlet.replaceChildren(currentView.container);
|
||||
}
|
||||
});
|
||||
return outlet;
|
||||
},
|
||||
|
||||
to(path: string) {
|
||||
window.location.hash = path.startsWith('#') ? path : '#' + path;
|
||||
},
|
||||
back() { window.history.back(); },
|
||||
path() { return window.location.hash.slice(1) || '/'; }
|
||||
}
|
||||
};
|
||||
|
||||
const mounted = new WeakMap < HTMLElement, Runtime> ();
|
||||
|
||||
export function Mount(component: (() => any) | any, target: string | HTMLElement): Runtime {
|
||||
const targetEl = typeof target === 'string' ? document.querySelector(target) : target;
|
||||
if (!targetEl) throw new Error('Target not found');
|
||||
const prev = mounted.get(targetEl);
|
||||
if (prev) prev.destroy();
|
||||
const instance = Render(typeof component === 'function' ? component : () => component);
|
||||
/**
|
||||
* Monta un componente en el DOM, limpiando montajes previos.
|
||||
*/
|
||||
export function Mount(component: Component | (() => any), target: string | HTMLElement): Runtime | undefined {
|
||||
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.set(targetEl, instance);
|
||||
MOUNTED_NODES.set(targetEl, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
(window as any).$ = $;
|
||||
(window as any).Watch = Watch;
|
||||
(window as any).$$ = $$;
|
||||
(window as any).Render = Render;
|
||||
(window as any).Tag = Tag;
|
||||
(window as any).Mount = Mount;
|
||||
(window as any).Router = Router;
|
||||
// --- Registro automático de tags JSX y exportación global ---
|
||||
const sigPro = { $, $$, Render, Watch, Tag, Router, Mount };
|
||||
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 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 as any)[helper] = (p?: any, c?: any) => Tag(tag, p, c);
|
||||
});
|
||||
window.SigPro = Object.freeze(sigPro);
|
||||
}
|
||||
|
||||
export { $ as signal, Watch as effect, $$ as reactive, Render, Tag, Mount, Router };
|
||||
export default sigPro;
|
||||
Reference in New Issue
Block a user