Actualizar hibrido.js
This commit is contained in:
733
hibrido.js
733
hibrido.js
@@ -1,468 +1,453 @@
|
|||||||
type EffectFn = () => void;
|
// SigPro.ts - Versión simplificada (sin If/For) y tipada
|
||||||
type CleanupFn = () => void;
|
|
||||||
|
|
||||||
let currentEffect: EffectFn | null = null;
|
type EffectFn = {
|
||||||
const effectStack: EffectFn[] = [];
|
(): void;
|
||||||
|
_deps: Set<Set<EffectFn>>;
|
||||||
|
_deleted?: boolean;
|
||||||
|
_isComputed?: boolean;
|
||||||
|
_subs?: Set<EffectFn>;
|
||||||
|
depth?: number;
|
||||||
|
stop?: () => void;
|
||||||
|
_cleanups?: Set<() => void>;
|
||||||
|
};
|
||||||
|
|
||||||
let pendingBatch = false;
|
type Owner = { cleanups: Set<() => void> } | null;
|
||||||
const batchQueue = new Set < EffectFn > ();
|
|
||||||
|
|
||||||
function flushBatch() {
|
type Signal<T> = {
|
||||||
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> {
|
|
||||||
(): T;
|
(): T;
|
||||||
(value: T | ((prev: T) => T)): T;
|
(next: T | ((prev: T) => T)): T;
|
||||||
_subs: Set<EffectFn & { deps: Set<Set<EffectFn>> }>;
|
_isSignal?: boolean;
|
||||||
_value: T;
|
};
|
||||||
_storageKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFunction<T>(v: any): v is (...args: any[]) => T {
|
type ComputedSignal<T> = Signal<T> & { stop: () => void };
|
||||||
return typeof v === 'function';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function $<T>(initialValue: T, storageKey?: string): Signal<T> {
|
type Runtime = {
|
||||||
let value: T = initialValue;
|
_isRuntime: true;
|
||||||
const subs = new Set < EffectFn & { deps: Set < Set < EffectFn >> } > ();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Señal normal
|
||||||
|
let value = initial as T;
|
||||||
if (storageKey) {
|
if (storageKey) {
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem(storageKey);
|
const saved = localStorage.getItem(storageKey);
|
||||||
if (saved !== null) value = JSON.parse(saved);
|
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 => {
|
const signal = ((...args: [] | [T | ((prev: T) => T)]) => {
|
||||||
if (arguments.length === 0) {
|
if (args.length) {
|
||||||
if (currentEffect) {
|
const next = isFunc(args[0]) ? (args[0] as (prev: T) => T)(value) : args[0];
|
||||||
subs.add(currentEffect as any);
|
if (!Object.is(value, next)) {
|
||||||
(currentEffect as any).deps.add(subs);
|
value = next;
|
||||||
}
|
|
||||||
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;
|
|
||||||
if (storageKey) localStorage.setItem(storageKey, JSON.stringify(value));
|
if (storageKey) localStorage.setItem(storageKey, JSON.stringify(value));
|
||||||
for (const eff of Array.from(subs)) {
|
triggerUpdate(subscribers);
|
||||||
if (eff && !eff._deleted) scheduleEffect(eff);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
trackSubscription(subscribers);
|
||||||
return value;
|
return value;
|
||||||
}
|
|
||||||
}) as Signal<T>;
|
}) as Signal<T>;
|
||||||
signal._subs = subs;
|
signal._isSignal = true;
|
||||||
signal._value = initialValue;
|
|
||||||
if (storageKey) signal._storageKey = storageKey;
|
|
||||||
return signal;
|
return signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Watch(effectFn: () => void, deps?: Array<() => any>): () => void {
|
/**
|
||||||
const runner = (() => {
|
* Convierte un objeto en un proxy reactivo profundo.
|
||||||
if (runner._deleted) return;
|
*/
|
||||||
cleanupEffect(runner);
|
export function $$<T extends object>(object: T, cache = new WeakMap()): T {
|
||||||
effectStack.push(runner);
|
if (!isObj(object)) return object;
|
||||||
const prev = currentEffect;
|
if (cache.has(object)) return cache.get(object);
|
||||||
currentEffect = runner;
|
|
||||||
try {
|
const keySubscribers: Record<string | symbol, Set<EffectFn>> = {};
|
||||||
effectFn();
|
const proxy = new Proxy(object, {
|
||||||
} finally {
|
get(target, key, receiver) {
|
||||||
currentEffect = prev;
|
if (activeEffect) {
|
||||||
effectStack.pop();
|
const subs = keySubscribers[key] ??= new Set();
|
||||||
|
trackSubscription(subs);
|
||||||
}
|
}
|
||||||
}) as EffectFn & { deps: Set<Set<EffectFn>>; _deleted?: boolean; _cleanups?: Set<() => void> };
|
const value = Reflect.get(target, key, receiver);
|
||||||
|
return isObj(value) ? $$(value, cache) : value;
|
||||||
runner.deps = new Set();
|
},
|
||||||
runner._deleted = false;
|
set(target, key, value, receiver) {
|
||||||
|
if (Object.is(target[key as keyof T], value)) return true;
|
||||||
runner();
|
const success = Reflect.set(target, key, value, receiver);
|
||||||
|
if (keySubscribers[key]) triggerUpdate(keySubscribers[key]);
|
||||||
return () => {
|
return success;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
(sig as any)._stop = stop;
|
|
||||||
return sig;
|
|
||||||
}
|
|
||||||
|
|
||||||
const proxyCache = new WeakMap < object, any> ();
|
cache.set(object, proxy);
|
||||||
|
|
||||||
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);
|
|
||||||
return 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 owner = currentOwner;
|
||||||
const prev = currentOwner;
|
const runner = (() => {
|
||||||
currentOwner = owner;
|
if (runner._deleted) return;
|
||||||
try {
|
runner._deps.forEach(dep => dep.delete(runner));
|
||||||
return fn();
|
runner._deps.clear();
|
||||||
} finally {
|
runner._cleanups?.forEach(clean => clean());
|
||||||
currentOwner = prev;
|
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 };
|
||||||
|
|
||||||
function addCleanup(fn: CleanupFn) {
|
assign(runner, {
|
||||||
if (currentOwner) currentOwner.cleanups.add(fn);
|
_deps: new Set<Set<EffectFn>>(),
|
||||||
}
|
_cleanups: new Set<() => void>(),
|
||||||
|
_deleted: false,
|
||||||
export interface Runtime {
|
stop: () => {
|
||||||
container: HTMLElement | DocumentFragment;
|
if (runner._deleted) return;
|
||||||
destroy: () => void;
|
runner._deleted = true;
|
||||||
}
|
effectQueue.delete(runner);
|
||||||
|
runner._deps.forEach(dep => dep.delete(runner));
|
||||||
export function Render(renderFn: (ctx: { onCleanup: (fn: CleanupFn) => void }) => any): Runtime {
|
runner._cleanups?.forEach(clean => clean());
|
||||||
const cleanups = new Set < CleanupFn > ();
|
if (owner) owner.cleanups.delete(runner.stop);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (owner) owner.cleanups.add(runner.stop);
|
||||||
|
runner();
|
||||||
|
return runner.stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 };
|
||||||
|
|
||||||
|
const processResult = (result: any) => {
|
||||||
|
if (!result) return;
|
||||||
if (result._isRuntime) {
|
if (result._isRuntime) {
|
||||||
cleanups.add(result.destroy);
|
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 = () => {
|
try {
|
||||||
for (const node of currentNodes) {
|
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
||||||
cleanupNode(node);
|
} finally {
|
||||||
node.remove();
|
currentOwner = prevOwner;
|
||||||
}
|
}
|
||||||
currentNodes = [];
|
|
||||||
let result: any;
|
|
||||||
runWithOwner({ cleanups }, () => {
|
|
||||||
result = renderFn({ onCleanup: addCleanup });
|
|
||||||
});
|
|
||||||
currentNodes = processResult(result);
|
|
||||||
container.append(...currentNodes);
|
|
||||||
};
|
|
||||||
|
|
||||||
render();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_isRuntime: true,
|
_isRuntime: true,
|
||||||
container,
|
container,
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
cleanups.forEach(fn => fn());
|
cleanups.forEach(fn => fn());
|
||||||
for (const node of currentNodes) cleanupNode(node);
|
cleanupNode(container);
|
||||||
container.remove();
|
container.remove();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanupNode(node: Node) {
|
/**
|
||||||
if ((node as any)._cleanups) {
|
* Crea un elemento DOM con atributos e hijos reactivos.
|
||||||
(node as any)._cleanups.forEach((fn: CleanupFn) => fn());
|
*/
|
||||||
(node as any)._cleanups.clear();
|
export function Tag(tag: string, props: any = {}, children: any = []): HTMLElement {
|
||||||
}
|
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||||
node.childNodes.forEach(child => cleanupNode(child));
|
children = props;
|
||||||
}
|
props = {};
|
||||||
|
|
||||||
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 ?? [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
|
const isSVG = /^(svg|path|circle|rect|line|polyline|polygon|g|defs|text|tspan|use)$/.test(tag);
|
||||||
const el = isSVG
|
const el = isSVG
|
||||||
? document.createElementNS("http://www.w3.org/2000/svg", tag)
|
? doc.createElementNS("http://www.w3.org/2000/svg", tag)
|
||||||
: document.createElement(tag);
|
: createEl(tag) as HTMLElement;
|
||||||
|
|
||||||
const cleanups = new Set < CleanupFn > ();
|
(el as any)._cleanups = new Set<() => void>();
|
||||||
(el as any)._cleanups = cleanups;
|
(el as any).onUnmount = (fn: () => void) => (el as any)._cleanups.add(fn);
|
||||||
|
|
||||||
const updateAttribute = (name: string, value: any) => {
|
const booleanAttrs = ["disabled", "checked", "required", "readonly", "selected", "multiple", "autofocus"];
|
||||||
if (name === 'href' || name === 'src') {
|
|
||||||
value = sanitizeURL(value);
|
const updateAttr = (name: string, value: any) => {
|
||||||
}
|
const safe = (name === 'src' || name === 'href') && String(value).includes('javascript:') ? '#' : value;
|
||||||
if (booleanAttributes.has(name)) {
|
if (booleanAttrs.includes(name)) {
|
||||||
const bool = !!value;
|
(el as any)[name] = !!safe;
|
||||||
if (bool) el.setAttribute(name, '');
|
safe ? el.setAttribute(name, "") : el.removeAttribute(name);
|
||||||
else el.removeAttribute(name);
|
|
||||||
(el as any)[name] = bool;
|
|
||||||
} else if (value === undefined || value === null) {
|
|
||||||
el.removeAttribute(name);
|
|
||||||
} else {
|
} else {
|
||||||
el.setAttribute(name, String(value));
|
safe == null ? el.removeAttribute(name) : el.setAttribute(name, String(safe));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [key, val] of Object.entries(props)) {
|
for (const [key, val] of Object.entries(props)) {
|
||||||
if (key === 'ref') {
|
if (key === "ref") {
|
||||||
if (typeof val === 'function') val(el);
|
if (isFunc(val)) val(el);
|
||||||
else if (val && typeof val === 'object') val.current = el;
|
else if (val && typeof val === "object") (val as { current: any }).current = el;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (key.startsWith('on')) {
|
|
||||||
const eventName = key.slice(2).toLowerCase();
|
const isReactive = isFunc(val) && (val as any)._isSignal === true;
|
||||||
const handler = val;
|
if (key.startsWith("on")) {
|
||||||
el.addEventListener(eventName, handler);
|
const eventName = key.slice(2).toLowerCase().split(".")[0];
|
||||||
cleanups.add(() => el.removeEventListener(eventName, handler));
|
el.addEventListener(eventName, val);
|
||||||
continue;
|
(el as any)._cleanups.add(() => el.removeEventListener(eventName, val));
|
||||||
}
|
} else if (isReactive) {
|
||||||
if ((tag === 'input' || tag === 'textarea' || tag === 'select') &&
|
(el as any)._cleanups.add(Watch(() => {
|
||||||
(key === 'value' || key === 'checked') &&
|
const currentVal = (val as Signal<any>)();
|
||||||
typeof val === 'function' && (val as any)._subs) {
|
if (key === "class") el.className = currentVal || "";
|
||||||
const eventType = key === 'checked' ? 'change' : 'input';
|
else updateAttr(key, currentVal);
|
||||||
const handler = (e: Event) => {
|
}));
|
||||||
const target = e.target as HTMLInputElement;
|
if (["INPUT", "TEXTAREA", "SELECT"].includes(el.tagName) && (key === "value" || key === "checked")) {
|
||||||
const newValue = key === 'checked' ? target.checked : target.value;
|
const ev = key === "checked" ? "change" : "input";
|
||||||
(val as Signal<any>)(newValue);
|
const handler = (e: Event) => (val as Signal<any>)((e.target as any)[key]);
|
||||||
};
|
el.addEventListener(ev, handler);
|
||||||
el.addEventListener(eventType, handler);
|
(el as any)._cleanups.add(() => el.removeEventListener(ev, 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);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateAttribute(key, val);
|
updateAttr(key, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const appendChildNode = (child: any) => {
|
const appendChildNode = (child: any) => {
|
||||||
if (child == null || child === false || child === true) return;
|
if (isArr(child)) return child.forEach(appendChildNode);
|
||||||
if (Array.isArray(child)) {
|
if (isFunc(child) && (child as any)._isSignal !== true) {
|
||||||
child.forEach(appendChildNode);
|
const marker = createText("");
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof child === 'function' && (child as any)._subs) {
|
|
||||||
const marker = document.createComment('sig');
|
|
||||||
el.appendChild(marker);
|
el.appendChild(marker);
|
||||||
let currentNodes: Node[] = [];
|
let currentNodes: Node[] = [];
|
||||||
const dispose = Watch(() => {
|
(el as any)._cleanups.add(Watch(() => {
|
||||||
const value = child();
|
const result = child();
|
||||||
const newNodes: Node[] = [];
|
const nextNodes = (isArr(result) ? result : [result]).map((node: any) =>
|
||||||
const process = (v: any) => {
|
node?._isRuntime ? node.container : (node instanceof Node ? node : createText(node))
|
||||||
if (v == null) return;
|
);
|
||||||
if (Array.isArray(v)) v.forEach(process);
|
currentNodes.forEach(n => { cleanupNode(n); n.remove(); });
|
||||||
else if (v._isRuntime) {
|
nextNodes.forEach(n => marker.parentNode?.insertBefore(n, marker));
|
||||||
newNodes.push(v.container);
|
currentNodes = nextNodes;
|
||||||
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);
|
|
||||||
} else {
|
} else {
|
||||||
el.appendChild(document.createTextNode(String(child)));
|
el.appendChild(child instanceof Node ? child : createText(child));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
appendChildNode(childArray);
|
appendChildNode(children);
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Router simple ---
|
||||||
export const Router = {
|
export const Router = {
|
||||||
params: $({} as Record<string, string>),
|
params: $({} as Record<string, string>),
|
||||||
currentPath: $('/'),
|
to: (path: string) => { window.location.hash = path.replace(/^#?\/?/, "#/"); },
|
||||||
routes: [] as Array<{ path: string; component: any }>,
|
back: () => window.history.back(),
|
||||||
|
path: () => window.location.hash.replace(/^#/, "") || "/",
|
||||||
init(routes: Array<{ path: string; component: any }>) {
|
outlet: (routes: Array<{ path: string; component: Component }>) => {
|
||||||
this.routes = routes;
|
const currentPath = $(Router.path());
|
||||||
const update = () => {
|
window.addEventListener("hashchange", () => currentPath(Router.path()));
|
||||||
const hash = window.location.hash.slice(1) || '/';
|
const outlet = Tag("div", { class: "router-outlet" });
|
||||||
this.currentPath(hash);
|
|
||||||
};
|
|
||||||
window.addEventListener('hashchange', update);
|
|
||||||
update();
|
|
||||||
return this.outlet();
|
|
||||||
},
|
|
||||||
|
|
||||||
outlet() {
|
|
||||||
const outlet = Tag('div');
|
|
||||||
let currentView: Runtime | null = null;
|
let currentView: Runtime | null = null;
|
||||||
Watch(() => {
|
|
||||||
const path = this.currentPath();
|
Watch(currentPath, async () => {
|
||||||
const route = this.routes.find(r => {
|
const path = currentPath();
|
||||||
const parts = r.path.split('/').filter(Boolean);
|
const route = routes.find(r => {
|
||||||
const pathParts = path.split('/').filter(Boolean);
|
const rParts = r.path.split("/").filter(Boolean);
|
||||||
if (parts.length !== pathParts.length) return false;
|
const pParts = path.split("/").filter(Boolean);
|
||||||
return parts.every((part, i) => part.startsWith(':') || part === pathParts[i]);
|
return rParts.length === pParts.length && rParts.every((part, i) => part.startsWith(":") || part === pParts[i]);
|
||||||
}) || this.routes.find(r => r.path === '*');
|
}) || routes.find(r => r.path === "*");
|
||||||
|
|
||||||
if (route) {
|
if (route) {
|
||||||
const params: Record<string, string> = {};
|
let comp = route.component;
|
||||||
const parts = route.path.split('/').filter(Boolean);
|
if (isFunc(comp) && comp.toString().includes('import')) {
|
||||||
const pathParts = path.split('/').filter(Boolean);
|
comp = (await (comp as any)()).default || (await (comp as any)());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
const view = Render(() => {
|
const params: Record<string, string> = {};
|
||||||
const C = typeof Comp === 'function' ? Comp : () => Comp;
|
route.path.split("/").filter(Boolean).forEach((part, i) => {
|
||||||
return C(params);
|
if (part.startsWith(":")) params[part.slice(1)] = path.split("/").filter(Boolean)[i];
|
||||||
});
|
});
|
||||||
|
Router.params(params);
|
||||||
if (currentView) currentView.destroy();
|
if (currentView) currentView.destroy();
|
||||||
currentView = view;
|
currentView = Render(() => isFunc(comp) ? comp(params) : comp);
|
||||||
outlet.innerHTML = '';
|
outlet.replaceChildren(currentView.container);
|
||||||
outlet.appendChild(view.container);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return outlet;
|
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> ();
|
/**
|
||||||
|
* Monta un componente en el DOM, limpiando montajes previos.
|
||||||
export function Mount(component: (() => any) | any, target: string | HTMLElement): Runtime {
|
*/
|
||||||
const targetEl = typeof target === 'string' ? document.querySelector(target) : target;
|
export function Mount(component: Component | (() => any), target: string | HTMLElement): Runtime | undefined {
|
||||||
if (!targetEl) throw new Error('Target not found');
|
const targetEl = typeof target === "string" ? doc.querySelector(target) : target;
|
||||||
const prev = mounted.get(targetEl);
|
if (!targetEl) return;
|
||||||
if (prev) prev.destroy();
|
if (MOUNTED_NODES.has(targetEl)) MOUNTED_NODES.get(targetEl)!.destroy();
|
||||||
const instance = Render(typeof component === 'function' ? component : () => component);
|
const instance = Render(isFunc(component) ? component : () => component);
|
||||||
targetEl.replaceChildren(instance.container);
|
targetEl.replaceChildren(instance.container);
|
||||||
mounted.set(targetEl, instance);
|
MOUNTED_NODES.set(targetEl, instance);
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
// --- Registro automático de tags JSX y exportación global ---
|
||||||
(window as any).$ = $;
|
const sigPro = { $, $$, Render, Watch, Tag, Router, Mount };
|
||||||
(window as any).Watch = Watch;
|
if (typeof window !== "undefined") {
|
||||||
(window as any).$$ = $$;
|
Object.assign(window, sigPro);
|
||||||
(window as any).Render = Render;
|
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(" ");
|
||||||
(window as any).Tag = Tag;
|
tags.forEach(tag => {
|
||||||
(window as any).Mount = Mount;
|
const helper = tag[0].toUpperCase() + tag.slice(1);
|
||||||
(window as any).Router = Router;
|
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