Update sigpro.js nre $.page
This commit is contained in:
340
src/sigpro.js
340
src/sigpro.js
@@ -1,14 +1,7 @@
|
|||||||
// Global state for tracking the current reactive effect
|
// Global state for tracking the current reactive effect
|
||||||
let activeEffect = null;
|
let activeEffect = null;
|
||||||
|
|
||||||
// Queue for batched effect updates
|
|
||||||
const effectQueue = new Set();
|
const effectQueue = new Set();
|
||||||
let isFlushScheduled = false;
|
let isFlushScheduled = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Flushes all pending effects in the queue
|
|
||||||
* Executes all queued jobs and clears the queue
|
|
||||||
*/
|
|
||||||
let flushCount = 0;
|
let flushCount = 0;
|
||||||
|
|
||||||
const flushEffectQueue = () => {
|
const flushEffectQueue = () => {
|
||||||
@@ -39,43 +32,40 @@ const flushEffectQueue = () => {
|
|||||||
* @param {any} initialValue - Initial value or getter function
|
* @param {any} initialValue - Initial value or getter function
|
||||||
* @returns {Function} Signal getter/setter function
|
* @returns {Function} Signal getter/setter function
|
||||||
*/
|
*/
|
||||||
export const $ = (initialValue) => {
|
const $ = (initialValue) => {
|
||||||
const subscribers = new Set();
|
const subscribers = new Set();
|
||||||
|
let signal;
|
||||||
|
|
||||||
if (typeof initialValue === "function") {
|
if (typeof initialValue === "function") {
|
||||||
// Computed signal case
|
|
||||||
let isDirty = true;
|
let isDirty = true;
|
||||||
let cachedValue;
|
let cachedValue;
|
||||||
|
|
||||||
const computedEffect = {
|
const computedEffect = {
|
||||||
dependencies: new Set(),
|
dependencies: new Set(),
|
||||||
cleanupHandlers: new Set(),
|
|
||||||
markDirty: () => {
|
markDirty: () => {
|
||||||
if (!isDirty) {
|
if (!isDirty) {
|
||||||
isDirty = true;
|
isDirty = true;
|
||||||
subscribers.forEach((subscriber) => {
|
subscribers.forEach((sub) => {
|
||||||
if (subscriber.markDirty) subscriber.markDirty();
|
if (sub.markDirty) sub.markDirty();
|
||||||
effectQueue.add(subscriber);
|
effectQueue.add(sub);
|
||||||
});
|
});
|
||||||
|
if (!isFlushScheduled && effectQueue.size) {
|
||||||
|
isFlushScheduled = true;
|
||||||
|
queueMicrotask(flushEffectQueue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
run: () => {
|
run: () => {
|
||||||
// Clear old dependencies
|
computedEffect.dependencies.forEach((dep) => dep.delete(computedEffect));
|
||||||
computedEffect.dependencies.forEach((dependencySet) => dependencySet.delete(computedEffect));
|
|
||||||
computedEffect.dependencies.clear();
|
computedEffect.dependencies.clear();
|
||||||
|
const prev = activeEffect;
|
||||||
const previousEffect = activeEffect;
|
|
||||||
activeEffect = computedEffect;
|
activeEffect = computedEffect;
|
||||||
try {
|
try { cachedValue = initialValue(); }
|
||||||
cachedValue = initialValue();
|
finally { activeEffect = prev; isDirty = false; }
|
||||||
} finally {
|
|
||||||
activeEffect = previousEffect;
|
|
||||||
isDirty = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return () => {
|
signal = () => {
|
||||||
if (activeEffect) {
|
if (activeEffect) {
|
||||||
subscribers.add(activeEffect);
|
subscribers.add(activeEffect);
|
||||||
activeEffect.dependencies.add(subscribers);
|
activeEffect.dependencies.add(subscribers);
|
||||||
@@ -83,17 +73,15 @@ export const $ = (initialValue) => {
|
|||||||
if (isDirty) computedEffect.run();
|
if (isDirty) computedEffect.run();
|
||||||
return cachedValue;
|
return cachedValue;
|
||||||
};
|
};
|
||||||
}
|
} else {
|
||||||
|
signal = (...args) => {
|
||||||
// Regular signal case
|
|
||||||
return (...args) => {
|
|
||||||
if (args.length) {
|
if (args.length) {
|
||||||
const nextValue = typeof args[0] === "function" ? args[0](initialValue) : args[0];
|
const next = typeof args[0] === "function" ? args[0](initialValue) : args[0];
|
||||||
if (!Object.is(initialValue, nextValue)) {
|
if (!Object.is(initialValue, next)) {
|
||||||
initialValue = nextValue;
|
initialValue = next;
|
||||||
subscribers.forEach((subscriber) => {
|
subscribers.forEach((sub) => {
|
||||||
if (subscriber.markDirty) subscriber.markDirty();
|
if (sub.markDirty) sub.markDirty();
|
||||||
effectQueue.add(subscriber);
|
effectQueue.add(sub);
|
||||||
});
|
});
|
||||||
if (!isFlushScheduled && effectQueue.size) {
|
if (!isFlushScheduled && effectQueue.size) {
|
||||||
isFlushScheduled = true;
|
isFlushScheduled = true;
|
||||||
@@ -107,8 +95,12 @@ export const $ = (initialValue) => {
|
|||||||
}
|
}
|
||||||
return initialValue;
|
return initialValue;
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
return signal;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let currentPageCleanups = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a reactive effect that runs when dependencies change
|
* Creates a reactive effect that runs when dependencies change
|
||||||
* @param {Function} effectFn - The effect function to run
|
* @param {Function} effectFn - The effect function to run
|
||||||
@@ -119,31 +111,27 @@ const $e = (effectFn) => {
|
|||||||
dependencies: new Set(),
|
dependencies: new Set(),
|
||||||
cleanupHandlers: new Set(),
|
cleanupHandlers: new Set(),
|
||||||
run() {
|
run() {
|
||||||
// Run cleanup handlers
|
this.cleanupHandlers.forEach((h) => h());
|
||||||
this.cleanupHandlers.forEach((handler) => handler());
|
|
||||||
this.cleanupHandlers.clear();
|
this.cleanupHandlers.clear();
|
||||||
|
this.dependencies.forEach((dep) => dep.delete(this));
|
||||||
// Clear old dependencies
|
|
||||||
this.dependencies.forEach((dependencySet) => dependencySet.delete(this));
|
|
||||||
this.dependencies.clear();
|
this.dependencies.clear();
|
||||||
|
|
||||||
const previousEffect = activeEffect;
|
const prev = activeEffect;
|
||||||
activeEffect = this;
|
activeEffect = this;
|
||||||
try {
|
try {
|
||||||
const result = effectFn();
|
const res = effectFn();
|
||||||
if (typeof result === "function") this.cleanupFunction = result;
|
if (typeof res === "function") this.cleanupHandlers.add(res);
|
||||||
} finally {
|
} finally { activeEffect = prev; }
|
||||||
activeEffect = previousEffect;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
stop() {
|
stop() {
|
||||||
this.cleanupHandlers.forEach((handler) => handler());
|
this.cleanupHandlers.forEach((h) => h());
|
||||||
this.dependencies.forEach((dependencySet) => dependencySet.delete(this));
|
this.dependencies.forEach((dep) => dep.delete(this));
|
||||||
this.cleanupFunction?.();
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (currentPageCleanups) currentPageCleanups.push(() => effect.stop());
|
||||||
if (activeEffect) activeEffect.cleanupHandlers.add(() => effect.stop());
|
if (activeEffect) activeEffect.cleanupHandlers.add(() => effect.stop());
|
||||||
|
|
||||||
effect.run();
|
effect.run();
|
||||||
return () => effect.stop();
|
return () => effect.stop();
|
||||||
};
|
};
|
||||||
@@ -195,22 +183,11 @@ const $s = (key, initialValue, storage = localStorage) => {
|
|||||||
* @returns {DocumentFragment} Reactive document fragment
|
* @returns {DocumentFragment} Reactive document fragment
|
||||||
* @see {@link https://developer.mozilla.org/es/docs/Glossary/Cross-site_scripting}
|
* @see {@link https://developer.mozilla.org/es/docs/Glossary/Cross-site_scripting}
|
||||||
*/
|
*/
|
||||||
export const html = (strings, ...values) => {
|
const html = (strings, ...values) => {
|
||||||
const templateCache = html._templateCache ?? (html._templateCache = new WeakMap());
|
const templateCache = html._templateCache ?? (html._templateCache = new WeakMap());
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a node by path from root
|
|
||||||
* @param {Node} root - Root node
|
|
||||||
* @param {number[]} path - Path indices
|
|
||||||
* @returns {Node} Target node
|
|
||||||
*/
|
|
||||||
const getNodeByPath = (root, path) => path.reduce((node, index) => node?.childNodes?.[index], root);
|
const getNodeByPath = (root, path) => path.reduce((node, index) => node?.childNodes?.[index], root);
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies reactive text content to a node
|
|
||||||
* @param {Node} node - Target node
|
|
||||||
* @param {any[]} values - Values to insert
|
|
||||||
*/
|
|
||||||
const applyTextContent = (node, values) => {
|
const applyTextContent = (node, values) => {
|
||||||
const parts = node.textContent.split("{{part}}");
|
const parts = node.textContent.split("{{part}}");
|
||||||
const parent = node.parentNode;
|
const parent = node.parentNode;
|
||||||
@@ -225,12 +202,19 @@ export const html = (strings, ...values) => {
|
|||||||
parent.insertBefore(startMarker, node);
|
parent.insertBefore(startMarker, node);
|
||||||
parent.insertBefore(endMarker, node);
|
parent.insertBefore(endMarker, node);
|
||||||
|
|
||||||
|
if (typeof currentValue === "function") {
|
||||||
let lastResult;
|
let lastResult;
|
||||||
$e(() => {
|
$e(() => {
|
||||||
let result = typeof currentValue === "function" ? currentValue() : currentValue;
|
const result = currentValue();
|
||||||
if (result === lastResult) return;
|
if (result === lastResult) return;
|
||||||
lastResult = result;
|
lastResult = result;
|
||||||
|
updateContent(result);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateContent(currentValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateContent(result) {
|
||||||
if (typeof result !== "object" && !Array.isArray(result)) {
|
if (typeof result !== "object" && !Array.isArray(result)) {
|
||||||
const textNode = startMarker.nextSibling;
|
const textNode = startMarker.nextSibling;
|
||||||
const safeText = String(result ?? "");
|
const safeText = String(result ?? "");
|
||||||
@@ -241,10 +225,7 @@ export const html = (strings, ...values) => {
|
|||||||
while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling);
|
while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling);
|
||||||
parent.insertBefore(document.createTextNode(safeText), endMarker);
|
parent.insertBefore(document.createTextNode(safeText), endMarker);
|
||||||
}
|
}
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
// Handle arrays or objects
|
|
||||||
while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling);
|
while (startMarker.nextSibling !== endMarker) parent.removeChild(startMarker.nextSibling);
|
||||||
|
|
||||||
const items = Array.isArray(result) ? result : [result];
|
const items = Array.isArray(result) ? result : [result];
|
||||||
@@ -255,26 +236,21 @@ export const html = (strings, ...values) => {
|
|||||||
fragment.appendChild(nodeItem);
|
fragment.appendChild(nodeItem);
|
||||||
});
|
});
|
||||||
parent.insertBefore(fragment, endMarker);
|
parent.insertBefore(fragment, endMarker);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
node.remove();
|
node.remove();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get or create template from cache
|
|
||||||
let cachedTemplate = templateCache.get(strings);
|
let cachedTemplate = templateCache.get(strings);
|
||||||
if (!cachedTemplate) {
|
if (!cachedTemplate) {
|
||||||
const template = document.createElement("template");
|
const template = document.createElement("template");
|
||||||
template.innerHTML = strings.join("{{part}}");
|
template.innerHTML = strings.join("{{part}}");
|
||||||
|
|
||||||
const dynamicNodes = [];
|
const dynamicNodes = [];
|
||||||
const treeWalker = document.createTreeWalker(template.content, 133); // NodeFilter.SHOW_ALL
|
const treeWalker = document.createTreeWalker(template.content, 133);
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets path indices for a node
|
|
||||||
* @param {Node} node - Target node
|
|
||||||
* @returns {number[]} Path indices
|
|
||||||
*/
|
|
||||||
const getNodePath = (node) => {
|
const getNodePath = (node) => {
|
||||||
const path = [];
|
const path = [];
|
||||||
while (node && node !== template.content) {
|
while (node && node !== template.content) {
|
||||||
@@ -296,7 +272,6 @@ export const html = (strings, ...values) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (currentNode.nodeType === 1) {
|
if (currentNode.nodeType === 1) {
|
||||||
// Element node
|
|
||||||
for (let i = 0; i < currentNode.attributes.length; i++) {
|
for (let i = 0; i < currentNode.attributes.length; i++) {
|
||||||
const attribute = currentNode.attributes[i];
|
const attribute = currentNode.attributes[i];
|
||||||
if (attribute.value.includes("{{part}}")) {
|
if (attribute.value.includes("{{part}}")) {
|
||||||
@@ -305,7 +280,6 @@ export const html = (strings, ...values) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (currentNode.nodeType === 3 && currentNode.textContent.includes("{{part}}")) {
|
} else if (currentNode.nodeType === 3 && currentNode.textContent.includes("{{part}}")) {
|
||||||
// Text node
|
|
||||||
isDynamic = true;
|
isDynamic = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,7 +292,6 @@ export const html = (strings, ...values) => {
|
|||||||
const fragment = cachedTemplate.template.content.cloneNode(true);
|
const fragment = cachedTemplate.template.content.cloneNode(true);
|
||||||
let valueIndex = 0;
|
let valueIndex = 0;
|
||||||
|
|
||||||
// Get target nodes before applyTextContent modifies the DOM
|
|
||||||
const targets = cachedTemplate.dynamicNodes.map((nodeInfo) => ({
|
const targets = cachedTemplate.dynamicNodes.map((nodeInfo) => ({
|
||||||
node: getNodeByPath(fragment, nodeInfo.path),
|
node: getNodeByPath(fragment, nodeInfo.path),
|
||||||
info: nodeInfo,
|
info: nodeInfo,
|
||||||
@@ -328,7 +301,6 @@ export const html = (strings, ...values) => {
|
|||||||
if (!node) return;
|
if (!node) return;
|
||||||
|
|
||||||
if (info.type === 1) {
|
if (info.type === 1) {
|
||||||
// Element node
|
|
||||||
info.parts.forEach((part) => {
|
info.parts.forEach((part) => {
|
||||||
const currentValue = values[valueIndex++];
|
const currentValue = values[valueIndex++];
|
||||||
const attributeName = part.name;
|
const attributeName = part.name;
|
||||||
@@ -342,7 +314,6 @@ export const html = (strings, ...values) => {
|
|||||||
if (modifiers.includes("stop")) e.stopPropagation();
|
if (modifiers.includes("stop")) e.stopPropagation();
|
||||||
if (modifiers.includes("self") && e.target !== node) return;
|
if (modifiers.includes("self") && e.target !== node) return;
|
||||||
|
|
||||||
// Debounce
|
|
||||||
if (modifiers.some((m) => m.startsWith("debounce"))) {
|
if (modifiers.some((m) => m.startsWith("debounce"))) {
|
||||||
const ms = modifiers.find((m) => m.startsWith("debounce"))?.split(":")[1] || 300;
|
const ms = modifiers.find((m) => m.startsWith("debounce"))?.split(":")[1] || 300;
|
||||||
clearTimeout(node._debounceTimer);
|
clearTimeout(node._debounceTimer);
|
||||||
@@ -350,7 +321,6 @@ export const html = (strings, ...values) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once (auto-remover)
|
|
||||||
if (modifiers.includes("once")) {
|
if (modifiers.includes("once")) {
|
||||||
node.removeEventListener(eventName, handlerWrapper);
|
node.removeEventListener(eventName, handlerWrapper);
|
||||||
}
|
}
|
||||||
@@ -362,44 +332,52 @@ export const html = (strings, ...values) => {
|
|||||||
passive: modifiers.includes("passive"),
|
passive: modifiers.includes("passive"),
|
||||||
capture: modifiers.includes("capture"),
|
capture: modifiers.includes("capture"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
if ($e.onCleanup) {
|
|
||||||
$e.onCleanup(() => node.removeEventListener(eventName, handlerWrapper));
|
|
||||||
}
|
|
||||||
} else if (firstChar === ":") {
|
} else if (firstChar === ":") {
|
||||||
// Two-way binding
|
|
||||||
const propertyName = attributeName.slice(1);
|
const propertyName = attributeName.slice(1);
|
||||||
const eventType = node.type === "checkbox" || node.type === "radio" ? "change" : "input";
|
const eventType = node.type === "checkbox" || node.type === "radio" ? "change" : "input";
|
||||||
|
|
||||||
|
if (typeof currentValue === "function") {
|
||||||
$e(() => {
|
$e(() => {
|
||||||
const value = typeof currentValue === "function" ? currentValue() : currentValue;
|
const value = currentValue();
|
||||||
if (node[propertyName] !== value) node[propertyName] = value;
|
if (node[propertyName] !== value) node[propertyName] = value;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
node[propertyName] = currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
node.addEventListener(eventType, () => {
|
node.addEventListener(eventType, () => {
|
||||||
const value = eventType === "change" ? node.checked : node.value;
|
const value = eventType === "change" ? node.checked : node.value;
|
||||||
if (typeof currentValue === "function") currentValue(value);
|
if (typeof currentValue === "function") currentValue(value);
|
||||||
});
|
});
|
||||||
} else if (firstChar === "?") {
|
} else if (firstChar === "?") {
|
||||||
// Boolean attribute
|
|
||||||
const attrName = attributeName.slice(1);
|
const attrName = attributeName.slice(1);
|
||||||
|
|
||||||
|
if (typeof currentValue === "function") {
|
||||||
$e(() => {
|
$e(() => {
|
||||||
const result = typeof currentValue === "function" ? currentValue() : currentValue;
|
const result = currentValue();
|
||||||
node.toggleAttribute(attrName, !!result);
|
node.toggleAttribute(attrName, !!result);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
node.toggleAttribute(attrName, !!currentValue);
|
||||||
|
}
|
||||||
} else if (firstChar === ".") {
|
} else if (firstChar === ".") {
|
||||||
// Property binding
|
|
||||||
const propertyName = attributeName.slice(1);
|
const propertyName = attributeName.slice(1);
|
||||||
|
|
||||||
|
if (typeof currentValue === "function") {
|
||||||
$e(() => {
|
$e(() => {
|
||||||
let result = typeof currentValue === "function" ? currentValue() : currentValue;
|
const result = currentValue();
|
||||||
node[propertyName] = result;
|
node[propertyName] = result;
|
||||||
if (result != null && typeof result !== "object" && typeof result !== "boolean") {
|
if (result != null && typeof result !== "object" && typeof result !== "boolean") {
|
||||||
node.setAttribute(propertyName, result);
|
node.setAttribute(propertyName, result);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Regular attribute
|
node[propertyName] = currentValue;
|
||||||
|
if (currentValue != null && typeof currentValue !== "object" && typeof currentValue !== "boolean") {
|
||||||
|
node.setAttribute(propertyName, currentValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if (typeof currentValue === "function") {
|
if (typeof currentValue === "function") {
|
||||||
$e(() => node.setAttribute(attributeName, currentValue()));
|
$e(() => node.setAttribute(attributeName, currentValue()));
|
||||||
} else {
|
} else {
|
||||||
@@ -408,7 +386,6 @@ export const html = (strings, ...values) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (info.type === 3) {
|
} else if (info.type === 3) {
|
||||||
// Text node
|
|
||||||
const placeholderCount = node.textContent.split("{{part}}").length - 1;
|
const placeholderCount = node.textContent.split("{{part}}").length - 1;
|
||||||
applyTextContent(node, values.slice(valueIndex, valueIndex + placeholderCount));
|
applyTextContent(node, values.slice(valueIndex, valueIndex + placeholderCount));
|
||||||
valueIndex += placeholderCount;
|
valueIndex += placeholderCount;
|
||||||
@@ -418,6 +395,48 @@ export const html = (strings, ...values) => {
|
|||||||
return fragment;
|
return fragment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a page with automatic cleanup
|
||||||
|
* @param {Function} setupFunction - Page setup function that receives props
|
||||||
|
* @returns {Function} A function that creates page instances with props
|
||||||
|
*/
|
||||||
|
const $p = (setupFunction) => {
|
||||||
|
const tagName = "page-" + Math.random().toString(36).substring(2, 9);
|
||||||
|
|
||||||
|
customElements.define(
|
||||||
|
tagName,
|
||||||
|
class extends HTMLElement {
|
||||||
|
connectedCallback() {
|
||||||
|
this.style.display = "contents";
|
||||||
|
this._cleanups = [];
|
||||||
|
currentPageCleanups = this._cleanups;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = setupFunction({
|
||||||
|
params: JSON.parse(this.getAttribute("params") || "{}"),
|
||||||
|
onUnmount: (fn) => this._cleanups.push(fn),
|
||||||
|
});
|
||||||
|
this.appendChild(result instanceof Node ? result : document.createTextNode(String(result)));
|
||||||
|
} finally {
|
||||||
|
currentPageCleanups = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this._cleanups.forEach((fn) => fn());
|
||||||
|
this._cleanups = [];
|
||||||
|
this.innerHTML = "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (props = {}) => {
|
||||||
|
const el = document.createElement(tagName);
|
||||||
|
el.setAttribute("params", JSON.stringify(props));
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a custom web component with reactive properties
|
* Creates a custom web component with reactive properties
|
||||||
* @param {string} tagName - Custom element tag name
|
* @param {string} tagName - Custom element tag name
|
||||||
@@ -519,153 +538,52 @@ const $f = async (url, data, loading) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizePath = (path) => {
|
|
||||||
// Eliminar cualquier intento de HTML/JavaScript
|
|
||||||
return String(path).replace(/[<>"']/g, "");
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a router for hash-based navigation
|
* Creates a router for hash-based navigation
|
||||||
* @param {Array<{path: string|RegExp, component: Function}>} routes - Route configurations
|
* @param {Array<{path: string|RegExp, component: Function}>} routes - Route configurations
|
||||||
* @returns {HTMLDivElement} Router container element
|
* @returns {HTMLDivElement} Router container element
|
||||||
*/
|
*/
|
||||||
const $r = (routes) => {
|
const $r = (routes) => {
|
||||||
/**
|
|
||||||
* Gets current path from hash
|
|
||||||
* @returns {string} Current path
|
|
||||||
*/
|
|
||||||
const getCurrentPath = () => window.location.hash.replace(/^#/, "") || "/";
|
const getCurrentPath = () => window.location.hash.replace(/^#/, "") || "/";
|
||||||
|
|
||||||
const currentPath = $(getCurrentPath());
|
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
container.style.display = "contents";
|
container.style.display = "contents";
|
||||||
|
|
||||||
window.addEventListener("hashchange", () => {
|
const render = () => {
|
||||||
const nextPath = sanitizePath(getCurrentPath());
|
const path = getCurrentPath();
|
||||||
if (currentPath() !== nextPath) currentPath(nextPath);
|
let matchedRoute = routes.find(r => r.path instanceof RegExp ? path.match(r.path) : r.path === path);
|
||||||
});
|
|
||||||
|
|
||||||
$e(() => {
|
|
||||||
const path = currentPath();
|
|
||||||
let matchedRoute = null;
|
|
||||||
let routeParams = {};
|
let routeParams = {};
|
||||||
|
|
||||||
for (const route of routes) {
|
if (matchedRoute?.path instanceof RegExp) {
|
||||||
if (route.path instanceof RegExp) {
|
const m = path.match(matchedRoute.path);
|
||||||
const match = path.match(route.path);
|
routeParams = m.groups || { id: m[1] };
|
||||||
if (match) {
|
|
||||||
matchedRoute = route;
|
|
||||||
routeParams = match.groups || { id: match[1] };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (route.path === path) {
|
|
||||||
matchedRoute = route;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousEffect = activeEffect;
|
|
||||||
activeEffect = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const view = matchedRoute ? matchedRoute.component(routeParams) : Object.assign(document.createElement("h1"), { textContent: "404" });
|
const view = matchedRoute ? matchedRoute.component(routeParams) : Object.assign(document.createElement("h1"), { textContent: "404" });
|
||||||
|
container.replaceChildren(view instanceof Node ? view : document.createTextNode(String(view ?? "")));
|
||||||
|
};
|
||||||
|
|
||||||
container.replaceChildren(view instanceof Node ? view : document.createTextNode(view ?? ""));
|
window.addEventListener("hashchange", render);
|
||||||
} finally {
|
render();
|
||||||
activeEffect = previousEffect;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigates to a specific route
|
|
||||||
* @param {string} path - Target path
|
|
||||||
*/
|
|
||||||
$r.go = (path) => {
|
$r.go = (path) => {
|
||||||
const targetPath = path.startsWith("/") ? path : `/${path}`;
|
const targetPath = path.startsWith("/") ? path : `/${path}`;
|
||||||
if (window.location.hash !== `#${targetPath}`) window.location.hash = targetPath;
|
if (window.location.hash !== `#${targetPath}`) {
|
||||||
};
|
window.location.hash = targetPath;
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple WebSocket wrapper with signals
|
|
||||||
* @param {string} url - WebSocket URL
|
|
||||||
* @param {Object} options - Auto-reconnect options
|
|
||||||
* @returns {Object} WebSocket with reactive signals
|
|
||||||
*/
|
|
||||||
const $ws = (url, options = {}) => {
|
|
||||||
const { reconnect = true, maxReconnect = 5, reconnectInterval = 1000 } = options;
|
|
||||||
|
|
||||||
const status = $("disconnected");
|
|
||||||
const messages = $([]);
|
|
||||||
const error = $(null);
|
|
||||||
|
|
||||||
let ws = null;
|
|
||||||
let reconnectAttempts = 0;
|
|
||||||
let reconnectTimer = null;
|
|
||||||
|
|
||||||
const connect = () => {
|
|
||||||
status("connecting");
|
|
||||||
ws = new WebSocket(url);
|
|
||||||
|
|
||||||
ws.onopen = () => {
|
|
||||||
status("connected");
|
|
||||||
reconnectAttempts = 0;
|
|
||||||
error(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
|
||||||
const data = e.data;
|
|
||||||
messages([...messages(), data]);
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onerror = (err) => {
|
|
||||||
error(err);
|
|
||||||
status("error");
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
status("disconnected");
|
|
||||||
|
|
||||||
if (reconnect && reconnectAttempts < maxReconnect) {
|
|
||||||
reconnectTimer = setTimeout(
|
|
||||||
() => {
|
|
||||||
reconnectAttempts++;
|
|
||||||
connect();
|
|
||||||
},
|
|
||||||
reconnectInterval * Math.pow(2, reconnectAttempts),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const send = (data) => {
|
|
||||||
if (ws?.readyState === WebSocket.OPEN) {
|
|
||||||
ws.send(typeof data === "string" ? data : JSON.stringify(data));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
||||||
if (ws) {
|
|
||||||
ws.close();
|
|
||||||
ws = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
connect();
|
|
||||||
|
|
||||||
if ($e?.onCleanup) $e.onCleanup(close);
|
|
||||||
|
|
||||||
return { status, messages, error, send, close };
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Can customize the name of your functions */
|
/* Can customize the name of your functions */
|
||||||
|
|
||||||
$.effect = $e;
|
$.effect = $e;
|
||||||
|
$.page = $p;
|
||||||
$.component = $c;
|
$.component = $c;
|
||||||
$.fetch = $f;
|
$.fetch = $f;
|
||||||
$.router = $r;
|
$.router = $r;
|
||||||
$.ws = $ws;
|
|
||||||
$.storage = $s;
|
$.storage = $s;
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.$ = $;
|
||||||
|
}
|
||||||
|
export { $, html };
|
||||||
|
|||||||
Reference in New Issue
Block a user