improvements in sigpro
This commit is contained in:
283
sigpro/sigpro.js
283
sigpro/sigpro.js
@@ -1,43 +1,76 @@
|
|||||||
/**
|
/**
|
||||||
* SigPro - Atomic Unified Reactive Engine
|
* SigPro v2.0
|
||||||
* A lightweight, fine-grained reactivity system with built-in routing and plugin support.
|
|
||||||
* @author NatxoCC
|
|
||||||
*
|
|
||||||
* Type definitions available in sigpro.d.ts
|
|
||||||
*/
|
*/
|
||||||
(() => {
|
(() => {
|
||||||
/** @type {Function|null} Internal tracker for the currently executing reactive effect. */
|
|
||||||
let activeEffect = null;
|
let activeEffect = null;
|
||||||
|
const effectQueue = new Set();
|
||||||
|
let isFlushScheduled = false;
|
||||||
|
let flushCount = 0;
|
||||||
|
|
||||||
|
// --- Motor de Batching con Protección (v1 + v2) ---
|
||||||
|
const flushQueue = () => {
|
||||||
|
isFlushScheduled = false;
|
||||||
|
flushCount++;
|
||||||
|
|
||||||
|
if (flushCount > 100) {
|
||||||
|
effectQueue.clear();
|
||||||
|
throw new Error("SigPro: Bucle infinito detectado");
|
||||||
|
}
|
||||||
|
|
||||||
|
const effects = Array.from(effectQueue);
|
||||||
|
effectQueue.clear();
|
||||||
|
effects.forEach(fn => fn());
|
||||||
|
|
||||||
|
// Resetear contador al final del microtask
|
||||||
|
queueMicrotask(() => flushCount = 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleFlush = (s) => {
|
||||||
|
effectQueue.add(s);
|
||||||
|
if (!isFlushScheduled) {
|
||||||
|
isFlushScheduled = true;
|
||||||
|
queueMicrotask(flushQueue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a reactive Signal, Computed value, or Store (Object of signals)
|
|
||||||
* @param {any|Function|Object} initial - Initial value, computed function, or state object
|
|
||||||
* @param {string} [key] - Optional localStorage key for persistence
|
|
||||||
* @returns {Function|Object} Reactive accessor/mutator or Store object
|
|
||||||
*/
|
|
||||||
const $ = (initial, key) => {
|
const $ = (initial, key) => {
|
||||||
const subs = new Set();
|
const subs = new Set();
|
||||||
|
|
||||||
if (typeof initial === 'object' && initial !== null && !Array.isArray(initial) && typeof initial !== 'function' && !(initial instanceof Node)) {
|
// 1. Objetos Reactivos (v2)
|
||||||
|
if (initial?.constructor === Object && !key) {
|
||||||
const store = {};
|
const store = {};
|
||||||
for (let k in initial) {
|
for (let k in initial) store[k] = $(initial[k]);
|
||||||
store[k] = $(initial[k], key ? `${key}_${k}` : null);
|
|
||||||
}
|
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Efectos y Computados con Limpieza (v1 + v2)
|
||||||
if (typeof initial === 'function') {
|
if (typeof initial === 'function') {
|
||||||
let cached;
|
let cached, running = false;
|
||||||
|
const cleanups = new Set();
|
||||||
|
|
||||||
const runner = () => {
|
const runner = () => {
|
||||||
|
if (runner.el && !runner.el.isConnected) return; // GC: v2
|
||||||
|
if (running) return;
|
||||||
|
|
||||||
|
// Ejecutar limpiezas previas (v1)
|
||||||
|
cleanups.forEach(fn => fn());
|
||||||
|
cleanups.clear();
|
||||||
|
|
||||||
const prev = activeEffect;
|
const prev = activeEffect;
|
||||||
activeEffect = runner;
|
activeEffect = runner;
|
||||||
|
activeEffect.onCleanup = (fn) => cleanups.add(fn); // Registro de limpieza
|
||||||
|
|
||||||
|
running = true;
|
||||||
try {
|
try {
|
||||||
const next = initial();
|
const next = initial();
|
||||||
if (!Object.is(cached, next)) {
|
if (!Object.is(cached, next)) {
|
||||||
cached = next;
|
cached = next;
|
||||||
subs.forEach(s => s());
|
subs.forEach(scheduleFlush);
|
||||||
}
|
}
|
||||||
} finally { activeEffect = prev; }
|
} finally {
|
||||||
|
activeEffect = prev;
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
runner();
|
runner();
|
||||||
return () => {
|
return () => {
|
||||||
@@ -46,180 +79,162 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3. Persistencia (v2)
|
||||||
if (key) {
|
if (key) {
|
||||||
const saved = localStorage.getItem(key);
|
const saved = localStorage.getItem(key);
|
||||||
if (saved !== null) {
|
if (saved !== null) try { initial = JSON.parse(saved); } catch (e) { }
|
||||||
try { initial = JSON.parse(saved); } catch (e) { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Señal Atómica
|
||||||
return (...args) => {
|
return (...args) => {
|
||||||
if (args.length) {
|
if (args.length) {
|
||||||
const next = typeof args[0] === 'function' ? args[0](initial) : args[0];
|
const next = typeof args[0] === 'function' ? args[0](initial) : args[0];
|
||||||
if (!Object.is(initial, next)) {
|
if (!Object.is(initial, next)) {
|
||||||
initial = next;
|
initial = next;
|
||||||
if (key) localStorage.setItem(key, JSON.stringify(initial));
|
if (key) localStorage.setItem(key, JSON.stringify(initial));
|
||||||
subs.forEach(s => s());
|
subs.forEach(scheduleFlush);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (activeEffect) subs.add(activeEffect);
|
if (activeEffect) {
|
||||||
|
subs.add(activeEffect);
|
||||||
|
if (activeEffect.onCleanup) activeEffect.onCleanup(() => subs.delete(activeEffect));
|
||||||
|
}
|
||||||
return initial;
|
return initial;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// --- Motor de Renderizado con Modificadores de v1 ---
|
||||||
* Creates reactive HTML elements
|
|
||||||
* @param {string} tag - HTML tag name
|
|
||||||
* @param {Object} [props] - Attributes and event handlers
|
|
||||||
* @param {any} [content] - Child content
|
|
||||||
* @returns {HTMLElement}
|
|
||||||
*/
|
|
||||||
$.html = (tag, props = {}, content = []) => {
|
$.html = (tag, props = {}, content = []) => {
|
||||||
const el = document.createElement(tag);
|
const el = document.createElement(tag);
|
||||||
if (typeof props !== 'object' || props instanceof Node || Array.isArray(props) || typeof props === 'function') {
|
if (props instanceof Node || Array.isArray(props) || typeof props !== 'object') {
|
||||||
content = props;
|
content = props; props = {};
|
||||||
props = {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let [key, val] of Object.entries(props)) {
|
for (let [key, val] of Object.entries(props)) {
|
||||||
if (key.startsWith('on')) {
|
if (key.startsWith('on')) {
|
||||||
el.addEventListener(key.toLowerCase().slice(2), val);
|
const [rawName, ...mods] = key.toLowerCase().slice(2).split('.');
|
||||||
} else if (key.startsWith('$')) {
|
const handler = (e) => {
|
||||||
|
if (mods.includes('prevent')) e.preventDefault();
|
||||||
|
if (mods.includes('stop')) e.stopPropagation();
|
||||||
|
|
||||||
|
if (mods.some(m => m.startsWith('debounce'))) {
|
||||||
|
const ms = mods.find(m => m.startsWith('debounce')).split(':')[1] || 300;
|
||||||
|
clearTimeout(val._timer);
|
||||||
|
val._timer = setTimeout(() => val(e), ms);
|
||||||
|
} else {
|
||||||
|
val(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
el.addEventListener(rawName, handler, { once: mods.includes('once') });
|
||||||
|
}
|
||||||
|
else if (key.startsWith('$')) {
|
||||||
const attr = key.slice(1);
|
const attr = key.slice(1);
|
||||||
if ((attr === 'value' || attr === 'checked') && typeof val === 'function') {
|
const attrEff = () => {
|
||||||
const ev = attr === 'checked' ? 'change' : 'input';
|
|
||||||
el.addEventListener(ev, e => val(attr === 'checked' ? e.target.checked : e.target.value));
|
|
||||||
}
|
|
||||||
$(() => {
|
|
||||||
const v = typeof val === 'function' ? val() : val;
|
const v = typeof val === 'function' ? val() : val;
|
||||||
if (attr === 'value' || attr === 'checked') el[attr] = v;
|
if (attr === 'value' || attr === 'checked') el[attr] = v;
|
||||||
else if (typeof v === 'boolean') el.toggleAttribute(attr, v);
|
else if (typeof v === 'boolean') el.toggleAttribute(attr, v);
|
||||||
else el.setAttribute(attr, v ?? '');
|
else if (v == null) el.removeAttribute(attr);
|
||||||
});
|
else el.setAttribute(attr, v);
|
||||||
|
};
|
||||||
|
attrEff.el = el; $(attrEff);
|
||||||
|
|
||||||
|
if ((attr === 'value' || attr === 'checked') && typeof val === 'function') {
|
||||||
|
el.addEventListener(attr === 'checked' ? 'change' : 'input', e =>
|
||||||
|
val(attr === 'checked' ? e.target.checked : e.target.value)
|
||||||
|
);
|
||||||
|
}
|
||||||
} else el.setAttribute(key, val);
|
} else el.setAttribute(key, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
const append = (c) => {
|
const append = (c) => {
|
||||||
if (Array.isArray(c)) return c.forEach(append);
|
if (Array.isArray(c)) return c.forEach(append);
|
||||||
if (typeof c === 'function') {
|
if (typeof c === 'function') {
|
||||||
const node = document.createTextNode('');
|
let nodes = [document.createTextNode('')];
|
||||||
$(() => {
|
const contentEff = () => {
|
||||||
const res = c();
|
const res = c();
|
||||||
if (res instanceof Node) {
|
const nextNodes = (Array.isArray(res) ? res : [res]).map(i =>
|
||||||
if (node.parentNode) node.replaceWith(res);
|
i instanceof Node ? i : document.createTextNode(i ?? '')
|
||||||
} else {
|
);
|
||||||
node.textContent = res ?? '';
|
if (nextNodes.length === 0) nextNodes.push(document.createTextNode(''));
|
||||||
|
|
||||||
|
if (nodes[0].parentNode) {
|
||||||
|
const parent = nodes[0].parentNode;
|
||||||
|
nextNodes.forEach(n => parent.insertBefore(n, nodes[0]));
|
||||||
|
nodes.forEach(n => n.remove());
|
||||||
|
nodes = nextNodes;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
return el.appendChild(node);
|
contentEff.el = nodes[0];
|
||||||
|
nodes.forEach(n => el.appendChild(n));
|
||||||
|
$(contentEff);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? ''));
|
el.appendChild(c instanceof Node ? c : document.createTextNode(c ?? ''));
|
||||||
};
|
};
|
||||||
|
|
||||||
append(content);
|
append(content);
|
||||||
return el;
|
return el;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tags = [
|
const tags = ['div', 'span', 'p', 'h1', 'h2', 'ul', 'li', 'button', 'input', 'label', 'form', 'section', 'a', 'img'];
|
||||||
'div', 'span', 'p', 'section', 'nav', 'main', 'header', 'footer', 'article', 'aside',
|
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'dl', 'dt', 'dd',
|
|
||||||
'button', 'a', 'label', 'strong', 'em', 'code', 'pre', 'br', 'hr', 'small', 'i', 'b', 'u', 'mark',
|
|
||||||
'input', 'form', 'select', 'option', 'textarea', 'fieldset', 'legend', 'details', 'summary',
|
|
||||||
'table', 'thead', 'tbody', 'tr', 'th', 'td', 'tfoot', 'caption',
|
|
||||||
'img', 'canvas', 'video', 'audio', 'svg', 'path', 'iframe'
|
|
||||||
];
|
|
||||||
|
|
||||||
tags.forEach(t => window[t] = (p, c) => $.html(t, p, c));
|
tags.forEach(t => window[t] = (p, c) => $.html(t, p, c));
|
||||||
|
|
||||||
/**
|
// --- Router mejorado ---
|
||||||
* Mounts a component to the DOM
|
|
||||||
* @param {HTMLElement|Function} node - Component or element to mount
|
|
||||||
* @param {HTMLElement|string} [target=document.body] - Target element or selector
|
|
||||||
*/
|
|
||||||
$.mount = (node, target = document.body) => {
|
|
||||||
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
|
||||||
if (el) {
|
|
||||||
el.innerHTML = '';
|
|
||||||
el.appendChild(typeof node === 'function' ? node() : node);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a hash-based router
|
|
||||||
* @param {Array<{path: string, component: Function|Promise|HTMLElement}>} routes
|
|
||||||
* @returns {HTMLElement}
|
|
||||||
*/
|
|
||||||
$.router = (routes) => {
|
$.router = (routes) => {
|
||||||
|
// Señal persistente del path actual
|
||||||
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
|
const sPath = $(window.location.hash.replace(/^#/, "") || "/");
|
||||||
|
|
||||||
|
// Listener nativo
|
||||||
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
|
window.addEventListener("hashchange", () => sPath(window.location.hash.replace(/^#/, "") || "/"));
|
||||||
|
|
||||||
return $.html('div', [
|
const container = div({ class: "router-outlet" });
|
||||||
() => {
|
|
||||||
const current = sPath();
|
|
||||||
const cP = current.split('/').filter(Boolean);
|
|
||||||
|
|
||||||
const route = routes.find(r => {
|
const routeEff = () => {
|
||||||
const rP = r.path.split('/').filter(Boolean);
|
const cur = sPath();
|
||||||
if (rP.length !== cP.length) return false;
|
const cP = cur.split('/').filter(Boolean);
|
||||||
return rP.every((part, i) => part.startsWith(':') || part === cP[i]);
|
|
||||||
}) || routes.find(r => r.path === "*");
|
|
||||||
|
|
||||||
if (!route) return $.html('h1', "404 - Not Found");
|
// Buscamos la ruta (incluyendo parámetros :id y wildcard *)
|
||||||
|
const route = routes.find(r => {
|
||||||
|
const rP = r.path.split('/').filter(Boolean);
|
||||||
|
return rP.length === cP.length && rP.every((p, i) => p.startsWith(':') || p === cP[i]);
|
||||||
|
}) || routes.find(r => r.path === "*");
|
||||||
|
|
||||||
const rP = route.path.split('/').filter(Boolean);
|
if (!route) return container.replaceChildren(h1("404 - Not Found"));
|
||||||
const params = {};
|
|
||||||
rP.forEach((part, i) => {
|
|
||||||
if (part.startsWith(':')) params[part.slice(1)] = cP[i];
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = typeof route.component === 'function' ? route.component(params) : route.component;
|
// Extraer parámetros dinámicos
|
||||||
|
const params = {};
|
||||||
|
route.path.split('/').filter(Boolean).forEach((p, i) => {
|
||||||
|
if (p.startsWith(':')) params[p.slice(1)] = cP[i];
|
||||||
|
});
|
||||||
|
|
||||||
if (result instanceof Promise) {
|
const res = typeof route.component === 'function' ? route.component(params) : route.component;
|
||||||
const $lazyNode = $($.html('span', "Loading..."));
|
|
||||||
result.then(m => {
|
|
||||||
const content = m.default || m;
|
|
||||||
const finalView = typeof content === 'function' ? content(params) : content;
|
|
||||||
$lazyNode(finalView);
|
|
||||||
});
|
|
||||||
return () => $lazyNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result instanceof Node ? result : $.html('span', String(result));
|
// Renderizado Seguro con replaceChildren (v1 spirit)
|
||||||
|
if (res instanceof Promise) {
|
||||||
|
const loader = span("Cargando...");
|
||||||
|
container.replaceChildren(loader);
|
||||||
|
res.then(c => container.replaceChildren(c instanceof Node ? c : document.createTextNode(c)));
|
||||||
|
} else {
|
||||||
|
container.replaceChildren(res instanceof Node ? res : document.createTextNode(res));
|
||||||
}
|
}
|
||||||
]);
|
};
|
||||||
|
|
||||||
|
routeEff.el = container; // Seguridad de SigPro v2
|
||||||
|
$(routeEff);
|
||||||
|
|
||||||
|
return container;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Vinculamos el método .go
|
||||||
* Programmatic navigation
|
|
||||||
* @param {string} path - Destination path
|
|
||||||
*/
|
|
||||||
$.router.go = (path) => {
|
$.router.go = (path) => {
|
||||||
window.location.hash = path.startsWith('/') ? path : `/${path}`;
|
const target = path.startsWith('/') ? path : `/${path}`;
|
||||||
|
window.location.hash = target;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
$.mount = (node, target = 'body') => {
|
||||||
* Plugin system - extends SigPro or loads external scripts
|
const el = typeof target === 'string' ? document.querySelector(target) : target;
|
||||||
* @param {Function|string|string[]} source - Plugin or script URL(s)
|
if (el) { el.innerHTML = ''; el.appendChild(typeof node === 'function' ? node() : node); }
|
||||||
* @returns {Promise<SigPro>|SigPro}
|
|
||||||
*/
|
|
||||||
$.plugin = (source) => {
|
|
||||||
if (typeof source === 'function') {
|
|
||||||
source($);
|
|
||||||
return $;
|
|
||||||
}
|
|
||||||
const urls = Array.isArray(source) ? source : [source];
|
|
||||||
return Promise.all(urls.map(url => new Promise((resolve, reject) => {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.src = url;
|
|
||||||
script.async = true;
|
|
||||||
script.onload = () => {
|
|
||||||
console.log(`%c[SigPro] Plugin Loaded: ${url}`, "color: #51cf66; font-weight: bold;");
|
|
||||||
resolve();
|
|
||||||
};
|
|
||||||
script.onerror = () => reject(new Error(`[SigPro] Failed to load: ${url}`));
|
|
||||||
document.head.appendChild(script);
|
|
||||||
}))).then(() => $);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.$ = $;
|
window.$ = $;
|
||||||
})();
|
})();
|
||||||
export const { $ } = window;
|
|
||||||
Reference in New Issue
Block a user