New modular Sigpro
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
All checks were successful
Deploy Docs to Synology / deploy (push) Successful in 3s
This commit is contained in:
129
dist/sigpro.esm.js
vendored
129
dist/sigpro.esm.js
vendored
@@ -15,6 +15,10 @@ var MOUNTED_NODES = new WeakMap;
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
||||
var SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","));
|
||||
var attrFilter = null;
|
||||
var filterXSS = (fn) => {
|
||||
attrFilter = fn;
|
||||
};
|
||||
var dispose = (eff) => {
|
||||
if (!eff || eff._disposed)
|
||||
return;
|
||||
@@ -243,21 +247,6 @@ var cleanupNode = (node) => {
|
||||
if (node.childNodes)
|
||||
node.childNodes.forEach((n) => cleanupNode(n));
|
||||
};
|
||||
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||
var DANGEROUS_URI_ATTRS = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
|
||||
var isDangerousAttr = (key) => DANGEROUS_URI_ATTRS.has(key) || key.startsWith("on");
|
||||
var validateAttr = (key, val) => {
|
||||
if (val == null || val === false)
|
||||
return null;
|
||||
if (isDangerousAttr(key)) {
|
||||
const sVal = String(val);
|
||||
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
||||
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`);
|
||||
return "#";
|
||||
}
|
||||
}
|
||||
return val;
|
||||
};
|
||||
var h = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||
children = props;
|
||||
@@ -296,38 +285,38 @@ var h = (tag, props = {}, children = []) => {
|
||||
isFunc(v) ? v(el) : v.current = el;
|
||||
continue;
|
||||
}
|
||||
let val = attrFilter ? attrFilter(k, v) : v;
|
||||
if (isSVG && k.startsWith("xlink:")) {
|
||||
const cleanVal = validateAttr(k.slice(6), v);
|
||||
cleanVal == null ? el.removeAttributeNS(XLINK_NS, k.slice(6)) : el.setAttributeNS(XLINK_NS, k.slice(6), cleanVal);
|
||||
val == null ? el.removeAttributeNS(XLINK_NS, k.slice(6)) : el.setAttributeNS(XLINK_NS, k.slice(6), val);
|
||||
continue;
|
||||
}
|
||||
if (k.startsWith("on")) {
|
||||
const ev = k.slice(2).toLowerCase();
|
||||
el.addEventListener(ev, v);
|
||||
const off = () => el.removeEventListener(ev, v);
|
||||
el.addEventListener(ev, val);
|
||||
const off = () => el.removeEventListener(ev, val);
|
||||
el._cleanups.add(off);
|
||||
onUnmount(off);
|
||||
} else if (isFunc(v)) {
|
||||
} else if (isFunc(val)) {
|
||||
const effect = createEffect(() => {
|
||||
const val = validateAttr(k, v());
|
||||
const raw = val();
|
||||
const safeVal = attrFilter ? attrFilter(k, raw) : raw;
|
||||
if (k === "class")
|
||||
el.className = val || "";
|
||||
else if (val == null)
|
||||
el.className = safeVal || "";
|
||||
else if (safeVal == null)
|
||||
el.removeAttribute(k);
|
||||
else if (k in el && !isSVG)
|
||||
el[k] = val;
|
||||
el[k] = safeVal;
|
||||
else
|
||||
el.setAttribute(k, val === true ? "" : val);
|
||||
el.setAttribute(k, safeVal === true ? "" : safeVal);
|
||||
});
|
||||
effect();
|
||||
el._cleanups.add(() => dispose(effect));
|
||||
onUnmount(() => dispose(effect));
|
||||
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||
const evType = k === "checked" ? "change" : "input";
|
||||
el.addEventListener(evType, (ev) => v(ev.target[k]));
|
||||
el.addEventListener(evType, (ev) => val(ev.target[k]));
|
||||
}
|
||||
} else {
|
||||
const val = validateAttr(k, v);
|
||||
if (val != null) {
|
||||
if (k in el && !isSVG)
|
||||
el[k] = val;
|
||||
@@ -433,35 +422,6 @@ var when = (cond, SIP, NOP = null) => {
|
||||
onUnmount(() => currentView?.destroy());
|
||||
return root;
|
||||
};
|
||||
var fx = ({ name, duration = 200, scale, slide, rotate, blur }, child) => {
|
||||
const el = typeof child === "function" ? child() : child;
|
||||
if (!(el instanceof Node))
|
||||
return el;
|
||||
if (name) {
|
||||
el.style.animation = `${name}-in ${duration}ms`;
|
||||
return el;
|
||||
}
|
||||
const hasTransform = scale || slide || rotate || blur;
|
||||
const initialTransform = [
|
||||
scale ? "scale(0.95)" : "",
|
||||
slide ? "translateY(-10px)" : "",
|
||||
rotate ? "rotate(-2deg)" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
el.style.transition = `all ${duration}ms ease`;
|
||||
el.style.opacity = "0";
|
||||
if (hasTransform)
|
||||
el.style.transform = initialTransform;
|
||||
if (blur)
|
||||
el.style.filter = "blur(4px)";
|
||||
requestAnimationFrame(() => {
|
||||
el.style.opacity = "1";
|
||||
if (hasTransform)
|
||||
el.style.transform = "none";
|
||||
if (blur)
|
||||
el.style.filter = "none";
|
||||
});
|
||||
return el;
|
||||
};
|
||||
var each = (src, itemFn, keyField) => {
|
||||
const anchor = doc.createTextNode("");
|
||||
const root = h("div", { style: "display:contents" }, [anchor]);
|
||||
@@ -527,47 +487,6 @@ router.params = $({});
|
||||
router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||
router.back = () => window.history.back();
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
var req = ({ url, method = "GET", headers = {} }) => {
|
||||
const loading = $(false);
|
||||
const error = $(null);
|
||||
const data = $(null);
|
||||
let controller = null;
|
||||
let timeoutId = null;
|
||||
const run = async (body = null) => {
|
||||
controller?.abort();
|
||||
clearTimeout(timeoutId);
|
||||
controller = new AbortController;
|
||||
timeoutId = setTimeout(() => controller.abort(), 1e4);
|
||||
loading(true);
|
||||
error(null);
|
||||
try {
|
||||
const isFormData = body instanceof FormData;
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: isFormData ? headers : { "Content-Type": "application/json", ...headers },
|
||||
body: isFormData ? body : body ? JSON.stringify(body) : undefined,
|
||||
signal: controller.signal
|
||||
});
|
||||
const text = await res.text();
|
||||
const json = text ? JSON.parse(text) : null;
|
||||
if (!res.ok)
|
||||
throw new Error(json?.message || res.statusText);
|
||||
data(json);
|
||||
return json;
|
||||
} catch (e) {
|
||||
if (e.name !== "AbortError")
|
||||
error(e.message);
|
||||
throw e;
|
||||
} finally {
|
||||
loading(false);
|
||||
clearTimeout(timeoutId);
|
||||
controller = null;
|
||||
timeoutId = null;
|
||||
}
|
||||
};
|
||||
const abort = () => controller?.abort();
|
||||
return { run, abort, loading, error, data };
|
||||
};
|
||||
var mount = (comp, target) => {
|
||||
const t = typeof target === "string" ? doc.querySelector(target) : target;
|
||||
if (!t)
|
||||
@@ -579,27 +498,13 @@ var mount = (comp, target) => {
|
||||
MOUNTED_NODES.set(t, inst);
|
||||
return inst;
|
||||
};
|
||||
var sigproFn = Object.freeze({ $, $$, watch, h, when, each, fx, router, req, mount, batch });
|
||||
var sigpro = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, sigproFn);
|
||||
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
|
||||
window[tag] = (props, children) => h(tag, props, children);
|
||||
});
|
||||
console.log("SigPro DX installed.");
|
||||
}
|
||||
};
|
||||
if (typeof import.meta === "undefined" && typeof window !== "undefined")
|
||||
sigpro();
|
||||
export {
|
||||
when,
|
||||
watch,
|
||||
sigpro,
|
||||
router,
|
||||
req,
|
||||
mount,
|
||||
h,
|
||||
fx,
|
||||
filterXSS,
|
||||
each,
|
||||
batch,
|
||||
$$,
|
||||
|
||||
2
dist/sigpro.esm.min.js
vendored
2
dist/sigpro.esm.min.js
vendored
File diff suppressed because one or more lines are too long
165
dist/sigpro.js
vendored
165
dist/sigpro.js
vendored
@@ -37,17 +37,15 @@
|
||||
});
|
||||
};
|
||||
|
||||
// index.js
|
||||
var exports_sigpro = {};
|
||||
__export(exports_sigpro, {
|
||||
// sigpro-full.js
|
||||
var exports_sigpro_full = {};
|
||||
__export(exports_sigpro_full, {
|
||||
when: () => when,
|
||||
watch: () => watch,
|
||||
sigpro: () => sigpro,
|
||||
router: () => router,
|
||||
req: () => req,
|
||||
mount: () => mount,
|
||||
h: () => h,
|
||||
fx: () => fx,
|
||||
filterXSS: () => filterXSS,
|
||||
each: () => each,
|
||||
batch: () => batch,
|
||||
$$: () => $$,
|
||||
@@ -71,6 +69,10 @@
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
||||
var SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","));
|
||||
var attrFilter = null;
|
||||
var filterXSS = (fn) => {
|
||||
attrFilter = fn;
|
||||
};
|
||||
var dispose = (eff) => {
|
||||
if (!eff || eff._disposed)
|
||||
return;
|
||||
@@ -299,21 +301,6 @@
|
||||
if (node.childNodes)
|
||||
node.childNodes.forEach((n) => cleanupNode(n));
|
||||
};
|
||||
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||
var DANGEROUS_URI_ATTRS = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
|
||||
var isDangerousAttr = (key) => DANGEROUS_URI_ATTRS.has(key) || key.startsWith("on");
|
||||
var validateAttr = (key, val) => {
|
||||
if (val == null || val === false)
|
||||
return null;
|
||||
if (isDangerousAttr(key)) {
|
||||
const sVal = String(val);
|
||||
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
||||
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`);
|
||||
return "#";
|
||||
}
|
||||
}
|
||||
return val;
|
||||
};
|
||||
var h = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||
children = props;
|
||||
@@ -352,38 +339,38 @@
|
||||
isFunc(v) ? v(el) : v.current = el;
|
||||
continue;
|
||||
}
|
||||
let val = attrFilter ? attrFilter(k, v) : v;
|
||||
if (isSVG && k.startsWith("xlink:")) {
|
||||
const cleanVal = validateAttr(k.slice(6), v);
|
||||
cleanVal == null ? el.removeAttributeNS(XLINK_NS, k.slice(6)) : el.setAttributeNS(XLINK_NS, k.slice(6), cleanVal);
|
||||
val == null ? el.removeAttributeNS(XLINK_NS, k.slice(6)) : el.setAttributeNS(XLINK_NS, k.slice(6), val);
|
||||
continue;
|
||||
}
|
||||
if (k.startsWith("on")) {
|
||||
const ev = k.slice(2).toLowerCase();
|
||||
el.addEventListener(ev, v);
|
||||
const off = () => el.removeEventListener(ev, v);
|
||||
el.addEventListener(ev, val);
|
||||
const off = () => el.removeEventListener(ev, val);
|
||||
el._cleanups.add(off);
|
||||
onUnmount(off);
|
||||
} else if (isFunc(v)) {
|
||||
} else if (isFunc(val)) {
|
||||
const effect = createEffect(() => {
|
||||
const val = validateAttr(k, v());
|
||||
const raw = val();
|
||||
const safeVal = attrFilter ? attrFilter(k, raw) : raw;
|
||||
if (k === "class")
|
||||
el.className = val || "";
|
||||
else if (val == null)
|
||||
el.className = safeVal || "";
|
||||
else if (safeVal == null)
|
||||
el.removeAttribute(k);
|
||||
else if (k in el && !isSVG)
|
||||
el[k] = val;
|
||||
el[k] = safeVal;
|
||||
else
|
||||
el.setAttribute(k, val === true ? "" : val);
|
||||
el.setAttribute(k, safeVal === true ? "" : safeVal);
|
||||
});
|
||||
effect();
|
||||
el._cleanups.add(() => dispose(effect));
|
||||
onUnmount(() => dispose(effect));
|
||||
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||
const evType = k === "checked" ? "change" : "input";
|
||||
el.addEventListener(evType, (ev) => v(ev.target[k]));
|
||||
el.addEventListener(evType, (ev) => val(ev.target[k]));
|
||||
}
|
||||
} else {
|
||||
const val = validateAttr(k, v);
|
||||
if (val != null) {
|
||||
if (k in el && !isSVG)
|
||||
el[k] = val;
|
||||
@@ -489,35 +476,6 @@
|
||||
onUnmount(() => currentView?.destroy());
|
||||
return root;
|
||||
};
|
||||
var fx = ({ name, duration = 200, scale, slide, rotate, blur }, child) => {
|
||||
const el = typeof child === "function" ? child() : child;
|
||||
if (!(el instanceof Node))
|
||||
return el;
|
||||
if (name) {
|
||||
el.style.animation = `${name}-in ${duration}ms`;
|
||||
return el;
|
||||
}
|
||||
const hasTransform = scale || slide || rotate || blur;
|
||||
const initialTransform = [
|
||||
scale ? "scale(0.95)" : "",
|
||||
slide ? "translateY(-10px)" : "",
|
||||
rotate ? "rotate(-2deg)" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
el.style.transition = `all ${duration}ms ease`;
|
||||
el.style.opacity = "0";
|
||||
if (hasTransform)
|
||||
el.style.transform = initialTransform;
|
||||
if (blur)
|
||||
el.style.filter = "blur(4px)";
|
||||
requestAnimationFrame(() => {
|
||||
el.style.opacity = "1";
|
||||
if (hasTransform)
|
||||
el.style.transform = "none";
|
||||
if (blur)
|
||||
el.style.filter = "none";
|
||||
});
|
||||
return el;
|
||||
};
|
||||
var each = (src, itemFn, keyField) => {
|
||||
const anchor = doc.createTextNode("");
|
||||
const root = h("div", { style: "display:contents" }, [anchor]);
|
||||
@@ -583,47 +541,6 @@
|
||||
router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||
router.back = () => window.history.back();
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
var req = ({ url, method = "GET", headers = {} }) => {
|
||||
const loading = $(false);
|
||||
const error = $(null);
|
||||
const data = $(null);
|
||||
let controller = null;
|
||||
let timeoutId = null;
|
||||
const run = async (body = null) => {
|
||||
controller?.abort();
|
||||
clearTimeout(timeoutId);
|
||||
controller = new AbortController;
|
||||
timeoutId = setTimeout(() => controller.abort(), 1e4);
|
||||
loading(true);
|
||||
error(null);
|
||||
try {
|
||||
const isFormData = body instanceof FormData;
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: isFormData ? headers : { "Content-Type": "application/json", ...headers },
|
||||
body: isFormData ? body : body ? JSON.stringify(body) : undefined,
|
||||
signal: controller.signal
|
||||
});
|
||||
const text = await res.text();
|
||||
const json = text ? JSON.parse(text) : null;
|
||||
if (!res.ok)
|
||||
throw new Error(json?.message || res.statusText);
|
||||
data(json);
|
||||
return json;
|
||||
} catch (e) {
|
||||
if (e.name !== "AbortError")
|
||||
error(e.message);
|
||||
throw e;
|
||||
} finally {
|
||||
loading(false);
|
||||
clearTimeout(timeoutId);
|
||||
controller = null;
|
||||
timeoutId = null;
|
||||
}
|
||||
};
|
||||
const abort = () => controller?.abort();
|
||||
return { run, abort, loading, error, data };
|
||||
};
|
||||
var mount = (comp, target) => {
|
||||
const t = typeof target === "string" ? doc.querySelector(target) : target;
|
||||
if (!t)
|
||||
@@ -635,16 +552,38 @@
|
||||
MOUNTED_NODES.set(t, inst);
|
||||
return inst;
|
||||
};
|
||||
var sigproFn = Object.freeze({ $, $$, watch, h, when, each, fx, router, req, mount, batch });
|
||||
var sigpro = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, sigproFn);
|
||||
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
|
||||
window[tag] = (props, children) => h(tag, props, children);
|
||||
});
|
||||
console.log("SigPro DX installed.");
|
||||
// sigpro/tags.js
|
||||
if (typeof window !== "undefined") {
|
||||
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
|
||||
window[tag] = (props, children) => h(tag, props, children);
|
||||
});
|
||||
console.log("SigPro tags ready");
|
||||
}
|
||||
|
||||
// sigpro/xss.js
|
||||
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||
var DANGEROUS_URI_ATTRS = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
|
||||
var isDangerousAttr = (key) => DANGEROUS_URI_ATTRS.has(key) || key.startsWith("on");
|
||||
var validateAttr = (key, val) => {
|
||||
if (val == null || val === false)
|
||||
return null;
|
||||
if (isDangerousAttr(key)) {
|
||||
const sVal = String(val);
|
||||
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
||||
console.warn(`[SigPro XSS] Locked ${key}`);
|
||||
return "#";
|
||||
}
|
||||
}
|
||||
return val;
|
||||
};
|
||||
if (typeof import.meta === "undefined" && typeof window !== "undefined")
|
||||
sigpro();
|
||||
filterXSS(validateAttr);
|
||||
|
||||
// sigpro-full.js
|
||||
if (typeof window !== "undefined") {
|
||||
const props = {};
|
||||
for (const fn of [["$", $], ["$$", $$], ["watch", watch], ["h", h], ["when", when], ["each", each], ["router", router], ["mount", mount], ["batch", batch]]) {
|
||||
props[fn[0]] = { value: fn[1], writable: false, configurable: false, enumerable: true };
|
||||
}
|
||||
Object.defineProperties(window, props);
|
||||
}
|
||||
})();
|
||||
|
||||
2
dist/sigpro.min.js
vendored
2
dist/sigpro.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -22,23 +22,6 @@
|
||||
<li><strong>SigPro:</strong> You ship <strong>Pure Vanilla JS</strong>. The runtime is so small that it often costs less than a single high‑res icon.</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-2xl font-bold mt-8 mb-4">Two Ways to Use SigPro</h3>
|
||||
<p class="mb-2"><strong>🎯 Modern ESM (recommended):</strong> Import only the functions you need – zero global pollution.</p>
|
||||
<pre class="bg-base-300/30 p-4 rounded-lg mb-4"><code>import { $, div, button, mount } from 'sigpro';
|
||||
const count = $(0);
|
||||
mount(() => div([ button({ onclick: () => count(count()+1) }, count) ]), '#app');</code></pre>
|
||||
<p class="mb-2"><strong>🌍 Classic Global (IIFE):</strong> Load the script and everything is automatically on <code>window</code> – no imports needed.</p>
|
||||
<pre class="bg-base-300/30 p-4 rounded-lg mb-4"><code><script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.19/dist/sigpro.js"></script>
|
||||
<script>
|
||||
const count = $(0);
|
||||
mount(() => div([ button({ onclick: () => count(count()+1) }, count) ]), '#app');
|
||||
</script></code></pre>
|
||||
<p class="mb-2"><strong>🔧 ESM + Global injection:</strong> If you want the global helpers but still use <code>type="module"</code>, call <code>sigpro()</code>.</p>
|
||||
<pre class="bg-base-300/30 p-4 rounded-lg"><code><script type="module">
|
||||
import { sigpro } from 'https://cdn.jsdelivr.net/npm/sigpro@latest/+esm';
|
||||
sigpro(); // now $, div, button, etc. are global
|
||||
</script></code></pre>
|
||||
|
||||
<h3 class="text-2xl font-bold mt-10 mb-4">Precision Engineering</h3>
|
||||
<h4 class="text-xl font-semibold mt-6 mb-2">1. Functional Efficiency</h4>
|
||||
<p><code>div()</code>, <code>button()</code>, <code>span()</code>… These aren't just wrappers; they are pre‑optimized constructors. When you call <code>div({ class: 'btn' }, "Click")</code>, SigPro creates the element and attaches its reactive listeners in a single, surgical operation.</p>
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
* [when](api/when.md)
|
||||
* [each](api/each.md)
|
||||
* [router](api/router.md)
|
||||
* [fx](api/fx.md)
|
||||
* [req](api/req.md)
|
||||
* [mount](api/mount.md)
|
||||
* [h](api/h.md)
|
||||
|
||||
* **Concepts**
|
||||
* [Tags](api/tags.md)
|
||||
* [Global Store](api/global.md)
|
||||
* [JSX Style](api/jsx.md)
|
||||
184
docs/api/fx.md
184
docs/api/fx.md
@@ -1,184 +0,0 @@
|
||||
# Animation Helper: `fx( )`
|
||||
|
||||
The `fx` function applies simple **enter animations** to DOM elements. You can either use a predefined CSS keyframes animation or declare inline transition effects (scale, slide, rotate, blur). It is designed to be used when dynamically creating elements – especially inside `when` or `each` branches.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
fx(
|
||||
options: {
|
||||
name?: string; // CSS keyframes animation name (will append '-in')
|
||||
duration?: number; // Animation duration in ms (default: 200)
|
||||
scale?: boolean; // Start with scale(0.95) → none
|
||||
slide?: boolean; // Start with translateY(-10px) → none
|
||||
rotate?: boolean; // Start with rotate(-2deg) → none
|
||||
blur?: boolean; // Start with blur(4px) → none
|
||||
},
|
||||
child: Node | (() => Node)
|
||||
): Node
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`options`** | `object` | Yes | Animation configuration. |
|
||||
| **`options.name`** | `string` | No | Name of a CSS `@keyframes` animation. The actual animation name becomes `${name}-in`. |
|
||||
| **`options.duration`** | `number` | No | Duration in milliseconds (default `200`). |
|
||||
| **`options.scale`** | `boolean` | No | Add a scale transform from `0.95` to `none`. |
|
||||
| **`options.slide`** | `boolean` | No | Add a vertical slide from `translateY(-10px)` to `none`. |
|
||||
| **`options.rotate`** | `boolean` | No | Add a small rotation from `rotate(-2deg)` to `none`. |
|
||||
| **`options.blur`** | `boolean` | No | Add a blur filter from `blur(4px)` to `none`. |
|
||||
| **`child`** | `Node` or `() => Node` | Yes | The element to animate. If a function is passed, it is called to obtain the node. |
|
||||
|
||||
**Returns:** The same DOM node (or the child if it is not a `Node`), after applying the animation setup.
|
||||
|
||||
> **Availability:** `fx` is exported from the SigPro module. In **ESM** you must import it (`import { fx } from 'sigpro'`) or inject all globals via `sigpro()`. In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Named CSS Keyframes Animation
|
||||
|
||||
Define a `@keyframes` rule in your CSS, for example:
|
||||
|
||||
```css
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
```
|
||||
|
||||
Then apply it with `fx`:
|
||||
|
||||
```javascript
|
||||
const MyComponent = () =>
|
||||
fx({ name: "fade", duration: 300 },
|
||||
div("I will fade in")
|
||||
);
|
||||
```
|
||||
|
||||
> The animation name used is `${name}-in`. In this example: `fade-in`.
|
||||
|
||||
### 2. Inline Transition (Scale + Opacity)
|
||||
|
||||
No CSS keyframes needed. The element starts with `opacity: 0` and `transform: scale(0.95)`, then transitions to `opacity: 1` and `transform: none`.
|
||||
|
||||
```javascript
|
||||
fx({ scale: true, duration: 200 },
|
||||
button({ onClick: () => alert("Hi") }, "Click me")
|
||||
);
|
||||
```
|
||||
|
||||
### 3. Combining Multiple Effects
|
||||
|
||||
You can combine `scale`, `slide`, `rotate`, and `blur` at the same time.
|
||||
|
||||
```javascript
|
||||
fx({ scale: true, slide: true, blur: true, duration: 250 },
|
||||
div({ class: "card" }, "Smooth enter")
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Using with `when` (Conditional Rendering)
|
||||
|
||||
Wrap the branch content with `fx` to animate entering elements.
|
||||
|
||||
```javascript
|
||||
when(show,
|
||||
() => fx({ slide: true },
|
||||
div("This slides in when visible")
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### 5. Using a Function as Child
|
||||
|
||||
If the element is created inside a function (e.g. to avoid recreation until needed), pass a function that returns the node.
|
||||
|
||||
```javascript
|
||||
fx({ scale: true },
|
||||
() => div("Lazy created and then animated")
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Happens Under the Hood
|
||||
|
||||
### With `name` (CSS animation)
|
||||
|
||||
- Sets `el.style.animation = `${name}-in ${duration}ms``.
|
||||
- The element animates according to your keyframes.
|
||||
- No further inline style changes are applied.
|
||||
|
||||
### Without `name` (transition effects)
|
||||
|
||||
- Sets `el.style.transition = `all ${duration}ms ease``.
|
||||
- Sets initial `opacity: 0`.
|
||||
- Applies initial transforms (`scale`, `slide`, `rotate`) if selected.
|
||||
- Applies initial `filter: blur(4px)` if `blur: true`.
|
||||
- In the next animation frame (via `requestAnimationFrame`), sets:
|
||||
- `opacity: 1`
|
||||
- `transform: none`
|
||||
- `filter: none`
|
||||
- The element transitions smoothly from the start state to the final state.
|
||||
|
||||
> **Important:** The element must be **in the DOM** when the animation starts. `fx` does **not** automatically mount the node – you must already have appended it or be about to mount it. In practice, when you call `fx` inside a component that is being mounted, the element will be added to the DOM shortly after, and the animation runs correctly.
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
const App = () =>
|
||||
div([
|
||||
fx({ name: "fade", duration: 400 },
|
||||
h1("Welcome to SigPro")
|
||||
),
|
||||
fx({ scale: true, slide: true, duration: 250 },
|
||||
button({ onClick: () => alert("Animated!") }, "Animated button")
|
||||
)
|
||||
]);
|
||||
|
||||
mount(App, "#app");
|
||||
```
|
||||
|
||||
With accompanying CSS:
|
||||
|
||||
```css
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- `fx` is **not** required for basic reactivity – it is purely a visual helper for enter animations.
|
||||
- For exit animations (when an element is removed), use CSS transitions on the element itself combined with `when` – or consider adding a wrapper that delays removal. SigPro does not include built‑in exit animations.
|
||||
- The function returns the same node you passed, so you can inline it inside `h` or tag helpers:
|
||||
|
||||
```javascript
|
||||
div([
|
||||
fx({ scale: true }, span("Hello"))
|
||||
])
|
||||
```
|
||||
|
||||
- If `child` is not a DOM node (e.g., a string or number), `fx` returns it unchanged – no animation is applied.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Option | Effect |
|
||||
| :--- | :--- |
|
||||
| `name` | Uses `@keyframes ${name}-in` CSS animation. |
|
||||
| `duration` | Controls animation/transition length (ms). |
|
||||
| `scale` | Start scale `0.95` → `none`. |
|
||||
| `slide` | Start `translateY(-10px)` → `none`. |
|
||||
| `rotate` | Start `rotate(-2deg)` → `none`. |
|
||||
| `blur` | Start `blur(4px)` → `none`. |
|
||||
|
||||
Combine options to create smooth, modern entrance effects without writing extra CSS.
|
||||
@@ -112,6 +112,7 @@ export const filteredTodos = $(() => {
|
||||
```javascript
|
||||
// components/TodoApp.js
|
||||
import { todos, filter, addTodo, toggleTodo, filteredTodos } from "../store/todos.js";
|
||||
import "sigpro/tags" // tags helpers available in global
|
||||
|
||||
const TodoApp = () =>
|
||||
div({ class: "todo-app" }, [
|
||||
|
||||
@@ -120,6 +120,7 @@ When `destroy()` is called (or when a new mount replaces an old one), everything
|
||||
|
||||
```javascript
|
||||
import { $, mount, div, h1, button } from 'sigpro';
|
||||
import "sigpro/tags" // tags helpers available in global
|
||||
|
||||
const App = () => {
|
||||
const count = $(0);
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
# ⚡ SigPro – Complete API Reference
|
||||
# SigPro – Complete API Reference
|
||||
|
||||
SigPro is a **Real‑DOM first** reactive micro‑framework. No virtual DOM, no diffing overhead – it updates the DOM directly with surgical precision. Built‑in automatic cleanup prevents memory leaks, and the API is designed to be both tiny and powerful.
|
||||
|
||||
```javascript
|
||||
import { $, $$, watch, h, when, each, fx, router, req, mount, batch } from 'sigpro'
|
||||
// Or, if you prefer the global style in an ESM environment:
|
||||
// import { sigpro } from 'sigpro'; sigpro(); // then $, h, div... become window globals
|
||||
// In a classic IIFE script (<script src="sigpro.js">), everything is available globally automatically.
|
||||
import { $, $$, watch, h, when, each, router, mount, batch } from 'sigpro'
|
||||
// Optional side‑effects (activate global helpers & security):
|
||||
import 'sigpro/tags' // → window.div, window.span, etc.
|
||||
import 'sigpro/xss' // → attribute sanitisation
|
||||
```
|
||||
|
||||
In a classic IIFE script (`<script src="sigpro.min.js">`), **everything** (core, router, tags, XSS, global functions) is available automatically.
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Core Reactivity
|
||||
## Core Reactivity
|
||||
|
||||
### `$(value, localStorageKey?)` – Signal & Computed
|
||||
|
||||
@@ -82,7 +84,7 @@ watch([count, double], ([newCount, newDouble]) => {
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Components & DOM (Hyperscript)
|
||||
## Components & DOM (Hyperscript)
|
||||
|
||||
### `h(tag, props, children)` – Create DOM Nodes
|
||||
|
||||
@@ -96,7 +98,7 @@ The universal builder. `props` can be omitted. Children can be strings, numbers,
|
||||
| Two‑way binding | `value: mySignal` (works on `input`, `textarea`, `select`) |
|
||||
| Refs | `ref: (el) => ...` or `ref: { current: null }` |
|
||||
| SVG support | tag names like `svg`, `circle`, `path` – sets correct namespace |
|
||||
| Dangerous URL sanitising | `href` / `src` with `javascript:` or `data:` are blocked → `'#'` |
|
||||
| Dangerous URL sanitising | `href` / `src` with `javascript:` or `data:` are blocked → `'#'` (when XSS shield is active) |
|
||||
|
||||
**Dynamic children** – pass a function as a child, it will be re‑executed and the DOM patched automatically:
|
||||
|
||||
@@ -108,28 +110,40 @@ h('div', {}, [
|
||||
|
||||
### Tag shortcuts
|
||||
|
||||
When using the **ESM module with named imports**, you can import the tag helpers individually:
|
||||
Tag helpers are **not exported individually** from the core. To use them globally without the `h(...)` wrapper, activate the side‑effect module:
|
||||
|
||||
```javascript
|
||||
import { div, h1, button } from 'sigpro'
|
||||
import 'sigpro/tags' // now window.div, window.span, etc. are available
|
||||
|
||||
// You can now write:
|
||||
div({ class: 'container' }, [
|
||||
h1({}, 'Title'),
|
||||
button({ onClick: () => alert('hi') }, 'Click me')
|
||||
])
|
||||
```
|
||||
|
||||
If you prefer the **global style** (all tags and core functions on `window`), either:
|
||||
- Use the classic IIFE script: `<script src="sigpro.js"></script>`
|
||||
- Or in ESM, call `sigpro()` after importing: `import { sigpro } from 'sigpro'; sigpro();`
|
||||
|
||||
After that, you can write `div()`, `span()`, etc. directly, without any import.
|
||||
In the **IIFE bundle** (`sigpro.min.js`), the helpers are already injected globally – no import needed.
|
||||
|
||||
Available tags: `a`, `abbr`, `article`, `aside`, `audio`, `b`, `blockquote`, `br`, `button`, `canvas`, `caption`, `cite`, `code`, `col`, `colgroup`, `datalist`, `dd`, `del`, `details`, `dfn`, `dialog`, `div`, `dl`, `dt`, `em`, `embed`, `fieldset`, `figcaption`, `figure`, `footer`, `form`, `h1`…`h6`, `header`, `hr`, `i`, `iframe`, `img`, `input`, `ins`, `kbd`, `label`, `legend`, `li`, `main`, `mark`, `meter`, `nav`, `object`, `ol`, `optgroup`, `option`, `output`, `p`, `picture`, `pre`, `progress`, `section`, `select`, `slot`, `small`, `source`, `span`, `strong`, `sub`, `summary`, `sup`, `svg`, `table`, `tbody`, `td`, `template`, `textarea`, `tfoot`, `th`, `thead`, `time`, `tr`, `u`, `ul`, `video`.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Flow Control Components
|
||||
## Built‑in XSS Shield (Optional)
|
||||
|
||||
SigPro includes an optional security layer that sanitises dangerous attributes (`href`, `src`, `formaction`, etc.) and blocks `javascript:` / `data:` / `vbscript:` protocols.
|
||||
To enable it in ESM environments:
|
||||
|
||||
```javascript
|
||||
import 'sigpro/xss'
|
||||
```
|
||||
|
||||
In the IIFE bundle, the shield is **active by default**.
|
||||
|
||||
When the shield is enabled, trying to set `href="javascript:alert(1)"` will log a warning and replace the value with `'#'`.
|
||||
|
||||
---
|
||||
|
||||
## Flow Control Components
|
||||
|
||||
### `when(condition, thenComponent, elseComponent?)`
|
||||
|
||||
@@ -162,7 +176,7 @@ When the array changes, elements are added, removed, or reordered with minimal D
|
||||
|
||||
---
|
||||
|
||||
## 💥 Batch
|
||||
## Batch
|
||||
|
||||
### `batch(fn)`
|
||||
|
||||
@@ -178,33 +192,13 @@ batch(() => {
|
||||
|
||||
---
|
||||
|
||||
## ✨ Animations – `fx(options, child)`
|
||||
|
||||
Applies smooth enter animations (CSS transitions / keyframes). Returns the modified element.
|
||||
|
||||
```javascript
|
||||
fx({ name: 'fade', duration: 300 },
|
||||
div({}, 'Hello')
|
||||
)
|
||||
```
|
||||
|
||||
**Options**
|
||||
- `name` – uses predefined keyframes `${name}-in` (you must define them in your CSS)
|
||||
- `duration` – in ms (default 200)
|
||||
- `scale` – adds `scale(0.95)` → `none`
|
||||
- `slide` – adds `translateY(-10px)` → `none`
|
||||
- `rotate` – adds `rotate(-2deg)` → `none`
|
||||
- `blur` – adds `blur(4px)` → `none`
|
||||
|
||||
If `name` is given, it sets `animation: ${name}-in ${duration}ms`. Otherwise it applies a smooth transition from the initial transform/filter to the final state.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Router – `router(routes)`
|
||||
## Router – `router(routes)`
|
||||
|
||||
Hash‑based SPA router. Returns a DOM node that renders the current route.
|
||||
|
||||
```javascript
|
||||
import { router } from 'sigpro'
|
||||
|
||||
const routes = [
|
||||
{ path: '/', component: () => div({}, 'Home') },
|
||||
{ path: '/user/:id', component: (params) => div({}, `User ${params.id}`) },
|
||||
@@ -229,44 +223,7 @@ const App = () => div({}, [
|
||||
|
||||
---
|
||||
|
||||
## 🌐 HTTP Requests – `req(config)`
|
||||
|
||||
Creates a reactive request controller with built‑in loading/error/data signals and abort support.
|
||||
|
||||
```javascript
|
||||
const fetchUser = req({ url: '/api/user/1', method: 'GET' })
|
||||
|
||||
// start the request
|
||||
fetchUser.run().catch(console.error)
|
||||
|
||||
// reactively display state
|
||||
watch(() => {
|
||||
if (fetchUser.loading()) console.log('loading...')
|
||||
if (fetchUser.error()) console.error(fetchUser.error())
|
||||
if (fetchUser.data()) console.log(fetchUser.data())
|
||||
})
|
||||
|
||||
// abort if needed
|
||||
fetchUser.abort()
|
||||
```
|
||||
|
||||
**Options**
|
||||
- `url` (required)
|
||||
- `method` (default `'GET'`)
|
||||
- `headers` (object, default `{}`)
|
||||
|
||||
**Return value**
|
||||
- `run(body?)` – initiates the request, returns a promise.
|
||||
- `abort()` – aborts the current request (AbortController).
|
||||
- `loading` – signal (boolean)
|
||||
- `error` – signal (`null` or error message)
|
||||
- `data` – signal (`null` or parsed JSON)
|
||||
|
||||
> **Note**: Automatically sets `Content-Type: application/json` unless `body` is a `FormData`. Timeout after 10 seconds aborts the request.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Mounting – `mount(component, target)`
|
||||
## Mounting – `mount(component, target)`
|
||||
|
||||
Clears the target element and mounts the application. Returns the runtime instance (which has a `.destroy()` method).
|
||||
|
||||
@@ -280,7 +237,7 @@ If you mount again on the same target, the previous instance is automatically de
|
||||
|
||||
---
|
||||
|
||||
## 🧹 Global Cleanup & Memory
|
||||
## Global Cleanup & Memory
|
||||
|
||||
SigPro tracks every effect, DOM event listener, and nested component. When a component is unmounted:
|
||||
- All its effects are disposed.
|
||||
@@ -292,10 +249,12 @@ You never need to manually clean up – just write reactive code.
|
||||
|
||||
---
|
||||
|
||||
## 📦 Full Example – Counter with Persistence
|
||||
## Full Example – Counter with Persistence
|
||||
|
||||
```javascript
|
||||
import { $, watch, h, mount } from 'sigpro'
|
||||
import { $, watch, mount } from 'sigpro'
|
||||
import 'sigpro/tags' // ← activate global helpers
|
||||
import 'sigpro/xss' // ← activate security (optional)
|
||||
|
||||
const count = $(0, 'counter') // persists in localStorage
|
||||
|
||||
@@ -320,8 +279,8 @@ You can rename everything in one line:
|
||||
import { $ as signal, watch as effect, h as element, mount as render } from 'sigpro'
|
||||
```
|
||||
|
||||
Or assign globally (after calling `sigpro()` or using the classic script):
|
||||
In the IIFE bundle, the core functions are already available as both `window.$` and `window.SigPro.$`, so you can alias them globally if needed:
|
||||
|
||||
```javascript
|
||||
window.myReactive = $
|
||||
window.myReactive = $ // or window.SigPro.$
|
||||
```
|
||||
184
docs/api/req.md
184
docs/api/req.md
@@ -1,184 +0,0 @@
|
||||
# HTTP Requests: `req( )`
|
||||
|
||||
The `req` function creates a **reactive HTTP request controller**. It returns signals for `loading`, `error`, and `data`, plus a `run` method to execute the request and an `abort` method to cancel it. All signals update automatically as the request progresses.
|
||||
|
||||
## Function Signature
|
||||
|
||||
```typescript
|
||||
req(config: {
|
||||
url: string;
|
||||
method?: string; // default: 'GET'
|
||||
headers?: Record<string, string>;
|
||||
}): {
|
||||
run: (body?: any) => Promise<any>;
|
||||
abort: () => void;
|
||||
loading: Signal<boolean>;
|
||||
error: Signal<string | null>;
|
||||
data: Signal<any | null>;
|
||||
}
|
||||
```
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **`url`** | `string` | Yes | The endpoint to call. |
|
||||
| **`method`** | `string` | No | HTTP method (`'GET'`, `'POST'`, etc.). Default `'GET'`. |
|
||||
| **`headers`** | `object` | No | Custom headers (will be merged with defaults). |
|
||||
|
||||
**Returns:** A controller object with reactive signals and methods.
|
||||
|
||||
> **Availability:** `req` is exported from the SigPro module. In **ESM** you must import it (`import { req } from 'sigpro'`) or inject all globals via `sigpro()`. In the **IIFE** classic script, it is automatically available on `window`. The examples below assume the function is already in scope.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### 1. Basic GET Request
|
||||
|
||||
```javascript
|
||||
const users = req({ url: '/api/users' });
|
||||
|
||||
// Start the request
|
||||
users.run().catch(console.error);
|
||||
|
||||
// React to the response
|
||||
watch(() => {
|
||||
if (users.loading()) console.log('Loading...');
|
||||
if (users.error()) console.error(users.error());
|
||||
if (users.data()) console.log('Data:', users.data());
|
||||
});
|
||||
```
|
||||
|
||||
### 2. POST Request with Body
|
||||
|
||||
```javascript
|
||||
const createUser = req({ url: '/api/users', method: 'POST' });
|
||||
|
||||
const handleSubmit = async (formData) => {
|
||||
try {
|
||||
await createUser.run(formData);
|
||||
alert('User created!');
|
||||
} catch (err) {
|
||||
// Error already in createUser.error()
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Aborting a Request
|
||||
|
||||
```javascript
|
||||
const search = req({ url: '/api/search' });
|
||||
|
||||
// Abort if the user types again quickly
|
||||
let timeout;
|
||||
input({ onInput: (e) => {
|
||||
clearTimeout(timeout);
|
||||
search.abort(); // cancel previous in-flight request
|
||||
timeout = setTimeout(() => search.run({ q: e.target.value }), 300);
|
||||
}});
|
||||
```
|
||||
|
||||
### 4. Reactive UI with Signals
|
||||
|
||||
```javascript
|
||||
const profile = req({ url: '/api/me' });
|
||||
|
||||
const App = () =>
|
||||
div([
|
||||
when(() => profile.loading(),
|
||||
() => div("Loading...")
|
||||
),
|
||||
when(() => profile.error(),
|
||||
() => div("Error: " + profile.error())
|
||||
),
|
||||
when(() => profile.data(),
|
||||
() => div([
|
||||
h2(profile.data().name),
|
||||
p(profile.data().email)
|
||||
])
|
||||
)
|
||||
]);
|
||||
|
||||
profile.run();
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Request Lifecycle
|
||||
|
||||
When you call `run(body?)`:
|
||||
|
||||
1. Any previous request is **aborted** automatically.
|
||||
2. `loading` becomes `true`.
|
||||
3. `error` is cleared.
|
||||
4. A 10‑second timeout is set (auto‑abort).
|
||||
5. The request is sent using `fetch`.
|
||||
6. On success: `data` is set with the parsed JSON, `loading` becomes `false`.
|
||||
7. On error: `error` is set with the message, `data` is cleared, `loading` becomes `false`.
|
||||
|
||||
> **Important:** The response body is parsed as JSON. If the response is not OK or the JSON parsing fails, `error` is set and the promise rejects.
|
||||
|
||||
---
|
||||
|
||||
## Automatic Headers
|
||||
|
||||
- If `body` is a plain object or array, the request automatically includes `Content-Type: application/json` (unless you override it in `headers`).
|
||||
- If `body` is a `FormData` instance, the `Content-Type` is not set (browser will set it automatically with the correct boundary).
|
||||
- Other headers can be added via the `headers` option.
|
||||
|
||||
```javascript
|
||||
const upload = req({
|
||||
url: '/upload',
|
||||
method: 'POST',
|
||||
headers: { 'X-Custom': 'value' }
|
||||
});
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', fileInput.files[0]);
|
||||
upload.run(formData);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Network errors, timeouts, and HTTP error statuses (4xx, 5xx) all set `error` and cause the promise to reject.
|
||||
- The `error` signal contains a human‑readable message.
|
||||
- Abort errors (calling `abort()` or timeout) are **silent** – they do not set `error` or reject the promise?
|
||||
Actually, according to the source: if `e.name === 'AbortError'`, it does **not** call `error(e.message)`, but the promise still rejects with the AbortError. You can handle it with `.catch()` if needed.
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```javascript
|
||||
const api = req({ url: 'https://jsonplaceholder.typicode.com/posts/1' });
|
||||
|
||||
const App = () =>
|
||||
div({ class: "demo" }, [
|
||||
when(() => api.loading(), () => p("⏳ Loading...")),
|
||||
when(() => api.error(), () => p("❌ " + api.error())),
|
||||
when(() => api.data(), () => div([
|
||||
h2(api.data().title),
|
||||
p(api.data().body)
|
||||
])),
|
||||
button({
|
||||
onClick: () => api.run(),
|
||||
disabled: () => api.loading()
|
||||
}, "Fetch")
|
||||
]);
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Member | Type | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `run(body?)` | `(any) => Promise` | Starts the request. Returns a promise. |
|
||||
| `abort()` | `() => void` | Cancels the current request. |
|
||||
| `loading` | `Signal<boolean>` | `true` while request is in flight. |
|
||||
| `error` | `Signal<string\|null>` | Contains an error message, or `null`. |
|
||||
| `data` | `Signal<any\|null>` | Contains the parsed response (JSON), or `null`. |
|
||||
@@ -153,6 +153,7 @@ If you want the router outlet to have no layout impact, you can set `display: co
|
||||
|
||||
```javascript
|
||||
import { $, router, mount } from "sigpro";
|
||||
import "sigpro/tags" // tags helpers available in global
|
||||
|
||||
const Home = () => div("Welcome home");
|
||||
const About = () => div("About us");
|
||||
|
||||
113
docs/api/tags.md
113
docs/api/tags.md
@@ -4,58 +4,72 @@ In **SigPro**, you don't need to manually type `h('div', ...)` for every element
|
||||
|
||||
## 1. How it Works
|
||||
|
||||
SigPro iterates through a list of standard HTML tags and creates a wrapper function for each one.
|
||||
SigPro creates a wrapper function for each standard HTML tag.
|
||||
- **Under the hood:** `h('button', { onclick: ... }, 'Click')`
|
||||
- **SigPro Style:** `button({ onclick: ... }, 'Click')`
|
||||
|
||||
> **Note:** All tag helpers are **lowercase** (e.g., `div`, `span`, `button`). This keeps the syntax close to raw HTML.
|
||||
|
||||
These helpers can be used in two ways, depending on your environment:
|
||||
|
||||
### Mode A: Classic (IIFE) – Auto‑global
|
||||
When you load the **IIFE bundle** (`sigpro.js`) with a traditional `<script>` tag (no `type="module"`), all tag helpers are automatically injected into the `window` object.
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.19/dist/sigpro.js"></script>
|
||||
<script>
|
||||
// div, span, button, ... are already global
|
||||
const App = () => div({ class: "card" }, "Hello");
|
||||
</script>
|
||||
```
|
||||
|
||||
### Mode B: ESM (Modern) – Explicit or Imported
|
||||
When you import the **ES module** (via `import` or CDN with `type="module"`), nothing is added to `window` by default. You have two options:
|
||||
|
||||
1. **Manual global injection** – import `sigpro` and call it:
|
||||
```javascript
|
||||
import { sigpro } from 'sigpro';
|
||||
sigpro(); // now div, span, button, etc. become global
|
||||
```
|
||||
2. **Named imports** (recommended) – import the helpers you need directly:
|
||||
```javascript
|
||||
import { div, span, button } from 'sigpro';
|
||||
// use them directly
|
||||
```
|
||||
> **Note:** All tag helpers are **lowercase** (e.g., `div`, `span`, `button`) and can be used directly once globally enabled.
|
||||
|
||||
---
|
||||
|
||||
## 2. The Complete List of Tag Helpers
|
||||
## 2. Activating the Tag Helpers
|
||||
|
||||
All helpers are **lowercase** and follow HTML5 tag names. You can use them globally (after injection) or import them individually.
|
||||
Depending on how you load SigPro, the activation varies:
|
||||
|
||||
### A. Classic IIFE – Automatic Global Helpers
|
||||
When you use the **IIFE bundle** (`sigpro.js` or `sigpro.min.js`) with a traditional `<script>` tag (no `type="module"`), **all tag helpers, signals, and XSS protection are automatically installed on `window`**. No extra steps needed.
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@latest/dist/sigpro.min.js"></script>
|
||||
<script>
|
||||
// div, span, button, $, h, mount, router... are already global
|
||||
const App = () => div({ class: "card" }, "Hello");
|
||||
mount(App, '#app');
|
||||
</script>
|
||||
```
|
||||
|
||||
### B. ESM (Modern JavaScript) – Explicit Activation
|
||||
When you import the **ES module** (`import { ... } from 'sigpro'`), the core **does not** add helpers to `window` by default. To enable global tags, import the dedicated side‑effect module:
|
||||
|
||||
```js
|
||||
import 'sigpro/tags'; // ← activates window.div, window.span, etc.
|
||||
|
||||
// Now you can use helpers globally
|
||||
const App = () => div({ class: "app" }, "Ready!");
|
||||
```
|
||||
|
||||
If you also want built‑in **XSS protection**, enable it once:
|
||||
|
||||
```js
|
||||
import 'sigpro/xss'; // ← add security layer
|
||||
import 'sigpro/tags'; // ← global helpers
|
||||
```
|
||||
|
||||
Both are side‑effect modules, so the order doesn’t matter.
|
||||
|
||||
> **Important:** The tag helpers are **not** exported as individual named exports from the core (`sigpro`). They become available as global functions (`window.div`, etc.) after the side‑effect runs.
|
||||
> If you prefer to avoid globals, you can always use `h('div', ...)` directly—it’s perfectly fine.
|
||||
|
||||
---
|
||||
|
||||
## 3. The Complete List of Tag Helpers
|
||||
|
||||
All helpers are **lowercase** and follow HTML5 tag names.
|
||||
|
||||
| Category | Available functions |
|
||||
| :--- | :--- |
|
||||
| **Structure** | `div`, `span`, `p`, `section`, `nav`, `main`, `header`, `footer`, `article`, `aside` |
|
||||
| **Typography** | `h1`, `h2`, `h3`, `h4`, `h5`, `h6`, `ul`, `ol`, `li`, `dl`, `dt`, `dd`, `strong`, `em`, `code`, `pre`, `small`, `b`, `u`, `mark` |
|
||||
| **Typography** | `h1`…`h6`, `ul`, `ol`, `li`, `dl`, `dt`, `dd`, `strong`, `em`, `code`, `pre`, `small`, `b`, `u`, `mark` |
|
||||
| **Interactive** | `button`, `a`, `label`, `br`, `hr`, `details`, `summary`, `dialog` |
|
||||
| **Forms** | `form`, `input`, `select`, `option`, `textarea`, `fieldset`, `legend` |
|
||||
| **Tables** | `table`, `thead`, `tbody`, `tr`, `th`, `td`, `tfoot`, `caption` |
|
||||
| **Media** | `img`, `canvas`, `video`, `audio`, `svg`, `iframe`, `picture`, `source` |
|
||||
|
||||
Full list includes all standard tags: `a`, `abbr`, `article`, `aside`, `audio`, `b`, `blockquote`, `br`, `button`, `canvas`, `caption`, `cite`, `code`, `col`, `colgroup`, `datalist`, `dd`, `del`, `details`, `dfn`, `dialog`, `div`, `dl`, `dt`, `em`, `embed`, `fieldset`, `figcaption`, `figure`, `footer`, `form`, `h1`…`h6`, `header`, `hr`, `i`, `iframe`, `img`, `input`, `ins`, `kbd`, `label`, `legend`, `li`, `main`, `mark`, `meter`, `nav`, `object`, `ol`, `optgroup`, `option`, `output`, `p`, `picture`, `pre`, `progress`, `section`, `select`, `slot`, `small`, `source`, `span`, `strong`, `sub`, `summary`, `sup`, `svg`, `table`, `tbody`, `td`, `template`, `textarea`, `tfoot`, `th`, `thead`, `time`, `tr`, `u`, `ul`, `video`.
|
||||
Full list: `a`, `abbr`, `article`, `aside`, `audio`, `b`, `blockquote`, `br`, `button`, `canvas`, `caption`, `cite`, `code`, `col`, `colgroup`, `datalist`, `dd`, `del`, `details`, `dfn`, `dialog`, `div`, `dl`, `dt`, `em`, `embed`, `fieldset`, `figcaption`, `figure`, `footer`, `form`, `h1`…`h6`, `header`, `hr`, `i`, `iframe`, `img`, `input`, `ins`, `kbd`, `label`, `legend`, `li`, `main`, `mark`, `meter`, `nav`, `object`, `ol`, `optgroup`, `option`, `output`, `p`, `picture`, `pre`, `progress`, `section`, `select`, `slot`, `small`, `source`, `span`, `strong`, `sub`, `summary`, `sup`, `svg`, `table`, `tbody`, `td`, `template`, `textarea`, `tfoot`, `th`, `thead`, `time`, `tr`, `u`, `ul`, `video`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Usage Patterns
|
||||
## 4. Usage Patterns
|
||||
|
||||
### A. Attributes + Children
|
||||
|
||||
@@ -79,7 +93,7 @@ section([
|
||||
|
||||
---
|
||||
|
||||
## 4. Reactive Power
|
||||
## 5. Reactive Power
|
||||
|
||||
These helpers are natively wired into SigPro's reactivity system.
|
||||
|
||||
@@ -126,7 +140,7 @@ div([
|
||||
|
||||
---
|
||||
|
||||
## 5. Custom Components with `h()` or Tag Helpers
|
||||
## 6. Custom Components with `h()` or Tag Helpers
|
||||
|
||||
While the tag helpers cover all standard HTML tags, you can create reusable components using them directly.
|
||||
|
||||
@@ -170,7 +184,7 @@ const Timer = () => {
|
||||
|
||||
---
|
||||
|
||||
## 6. Comparison with `h()`
|
||||
## 7. Comparison with `h()`
|
||||
|
||||
| Use case | Recommendation |
|
||||
| :--- | :--- |
|
||||
@@ -182,11 +196,17 @@ const Timer = () => {
|
||||
|
||||
---
|
||||
|
||||
## 7. Complete Example
|
||||
## 8. Complete Example
|
||||
|
||||
### ESM (modern projects)
|
||||
|
||||
```javascript
|
||||
// In a modern ESM environment (recommended)
|
||||
import { div, h1, input, p, button, mount, $ } from 'sigpro';
|
||||
// Enable global helpers + security
|
||||
import 'sigpro/tags';
|
||||
import 'sigpro/xss';
|
||||
|
||||
// Import core functions
|
||||
import { $, mount } from 'sigpro';
|
||||
|
||||
const nameSignal = $('');
|
||||
|
||||
@@ -204,10 +224,10 @@ const App = () =>
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
Or using the classic script (auto‑global):
|
||||
### Classic IIFE (auto‑global)
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.19/dist/sigpro.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.23/dist/sigpro.min.js"></script>
|
||||
<script>
|
||||
const nameSignal = $('');
|
||||
const App = () => div({ class: "app" }, [
|
||||
@@ -222,23 +242,24 @@ Or using the classic script (auto‑global):
|
||||
|
||||
---
|
||||
|
||||
## 8. Important Notes
|
||||
## 9. Important Notes
|
||||
|
||||
- **Naming:** All tag helpers are **lowercase**.
|
||||
- **Global availability:**
|
||||
- **IIFE script** → automatically on `window`.
|
||||
- **ESM module** → not global by default; use `import { div } from 'sigpro'` or call `sigpro()` to inject all globals.
|
||||
- **IIFE script** – automatically on `window`.
|
||||
- **ESM module** – not global by default; use `import 'sigpro/tags'` to activate them.
|
||||
- **Custom components:** Use **PascalCase** for your own component functions (e.g., `UserCard`) to visually distinguish them from built‑in tags.
|
||||
|
||||
---
|
||||
|
||||
## 9. Summary
|
||||
## 10. Summary
|
||||
|
||||
| Feature | Description |
|
||||
| :--- | :--- |
|
||||
| **Tag helpers** | Lowercase functions for every HTML element (e.g., `div()`, `button()`). |
|
||||
| **Availability** | Auto‑global in IIFE; in ESM use named imports or `sigpro()`. |
|
||||
| **Activation** | IIFE: automatic. ESM: `import 'sigpro/tags'`. |
|
||||
| **Reactive attributes** | Pass a function to any attribute to keep it synced. |
|
||||
| **Two‑way binding** | Assign a signal directly to `value` or `checked` on form elements. |
|
||||
| **Dynamic children** | Pass a function as a child for live updating content. |
|
||||
| **Auto‑cleanup** | All effects, events, and children are disposed when the element is removed. |
|
||||
| **Security** | Optional XSS shield: `import 'sigpro/xss'`. |
|
||||
|
||||
@@ -48,15 +48,16 @@ bun add sigpro
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
// Import the module – no automatic global injection
|
||||
import { sigpro, $, h, mount } from 'https://cdn.jsdelivr.net/npm/sigpro@1.2.19/+esm';
|
||||
// Import the core – no global helpers or XSS yet
|
||||
import { $, h, mount } from 'https://cdn.jsdelivr.net/npm/sigpro@1.2.23/dist/sigpro.esm.min.js';
|
||||
|
||||
// Option A: manually inject all globals (like the classic script)
|
||||
sigpro(); // now $, h, div, watch, etc. are on window
|
||||
|
||||
// Option B: use named imports (no global pollution)
|
||||
// Option A: use named imports (no globals, recommended)
|
||||
const count = $(0);
|
||||
mount(() => h1(() => `Count: ${count()}`), '#app');
|
||||
mount(() => h('h1', {}, () => `Count: ${count()}`), '#app');
|
||||
|
||||
// Option B: enable global tag helpers (still optional)
|
||||
import 'https://cdn.jsdelivr.net/npm/sigpro@1.2.23/sigpro/tags.js';
|
||||
// now div, span, button, etc. become global
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -66,10 +67,10 @@ bun add sigpro
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-box p-6">
|
||||
|
||||
```html
|
||||
<!-- Classic script: auto‑installs everything on window -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.19/dist/sigpro.js"></script>
|
||||
<!-- Classic script: full kit (core, router, tags, XSS) auto‑installed -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.23/dist/sigpro.min.js"></script>
|
||||
<script>
|
||||
// $, h, div, button, watch, ... are already global
|
||||
// $, h, div, button, router, etc. are already global
|
||||
const count = $(0);
|
||||
const App = () => div({ class: "card" }, [
|
||||
h1(() => `Count: ${count()}`),
|
||||
@@ -93,10 +94,12 @@ SigPro uses **lowercase** Tag Helpers (e.g., `div`, `button`) to keep the syntax
|
||||
<div class="tab-content bg-base-100 border-base-300 rounded-lg p-6 mt-2">
|
||||
|
||||
```javascript
|
||||
// App.js – Using named imports (recommended)
|
||||
import { $, h1, button, div, mount } from 'sigpro';
|
||||
// App.js – Use named imports for the core, activate helpers if needed
|
||||
import { $, mount } from 'sigpro';
|
||||
import 'sigpro/tags'; // ← make div, h1, button, etc. global
|
||||
import 'sigpro/xss'; // ← optional: activate XSS shield
|
||||
|
||||
export const App = () => {
|
||||
const App = () => {
|
||||
const count = $(0);
|
||||
return div({ class: "card p-4" }, [
|
||||
h1(() => `Count is: ${count()}`),
|
||||
@@ -107,10 +110,6 @@ export const App = () => {
|
||||
]);
|
||||
};
|
||||
|
||||
// main.js
|
||||
import { mount } from 'sigpro';
|
||||
import { App } from './App.js';
|
||||
|
||||
mount(App, '#app');
|
||||
```
|
||||
|
||||
@@ -125,9 +124,9 @@ mount(App, '#app');
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.19/dist/sigpro.js"></script>
|
||||
<!-- IIFE full bundle – everything ready to use -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/sigpro@1.2.23/dist/sigpro.min.js"></script>
|
||||
<script>
|
||||
// Everything is already global – no import needed
|
||||
const name = $('Developer');
|
||||
const App = () => section({ class: "container" }, [
|
||||
h2(() => `Welcome, ${name()}`),
|
||||
@@ -153,32 +152,36 @@ mount(App, '#app');
|
||||
|
||||
SigPro gives you full control over global pollution.
|
||||
|
||||
### Mode A: Classic (IIFE) – Auto‑injection
|
||||
When you load the **IIFE bundle** (`sigpro.js`) with a traditional `<script>` tag (no `type="module"`), the library automatically injects:
|
||||
- All core functions (`$`, `$$`, `watch`, `h`, `when`, `each`, `fx`, `router`, `req`, `mount`, `batch`) into `window`.
|
||||
- Lowercase tag helpers (`div`, `span`, `button`, etc.) also become global functions.
|
||||
### Mode A: Classic (IIFE) – Full Auto‑injection
|
||||
When you load the **IIFE full bundle** (`sigpro.min.js`) with a traditional `<script>` tag (no `type="module"`), the library automatically provides **everything**:
|
||||
- Core functions (`$`, `$$`, `watch`, `h`, `when`, `each`, `router`, `mount`, `batch`) directly on `window` (also available as `SigPro.*`).
|
||||
- Lowercase tag helpers (`div`, `span`, `button`, …) become global functions.
|
||||
- Built‑in XSS protection activated.
|
||||
|
||||
✅ Zero configuration – just drop the script and start coding.
|
||||
|
||||
### Mode B: ESM (Modern) – Explicit Injection
|
||||
When you import the **ESM module** (from CDN or via `import`), **nothing** is added to `window` by default. You have two clean options:
|
||||
### Mode B: ESM (Modern) – Explicit Activation
|
||||
When you import the **ESM core** (`import { ... } from 'sigpro'`), **only the reactive core and router are available**. Tags and security are opt‑in:
|
||||
|
||||
1. **Named imports** (recommended for most apps):
|
||||
1. **Named imports** (for the core):
|
||||
```javascript
|
||||
import { $, h, mount } from 'sigpro';
|
||||
import { $, h, mount, router } from 'sigpro';
|
||||
```
|
||||
No global pollution, perfect for bundlers and large projects.
|
||||
No global pollution – perfect for bundlers and large projects.
|
||||
|
||||
2. **Manual global injection** (similar to classic mode but controlled):
|
||||
2. **Activate global helpers** (when you want `div`, `span`, etc. without importing each one):
|
||||
```javascript
|
||||
import { sigpro } from 'sigpro';
|
||||
sigpro(); // now $, h, div, button, ... are on window
|
||||
import 'sigpro/tags'; // side‑effect: injects all tag helpers into window
|
||||
```
|
||||
|
||||
3. **Activate XSS shield** (optional):
|
||||
```javascript
|
||||
import 'sigpro/xss'; // side‑effect: enables attribute sanitization
|
||||
```
|
||||
Useful for quick prototyping or when you prefer the global style.
|
||||
|
||||
### Why two modes?
|
||||
- **Legacy / no‑build**: Use the IIFE script and get everything automatically.
|
||||
- **Modern ESM**: Keep your global namespace clean, leverage tree‑shaking, or inject only when you need it.
|
||||
- **Legacy / no‑build**: Use the IIFE full script and get everything automatically.
|
||||
- **Modern ESM**: Keep your global namespace clean, only activate helpers/security when needed, and benefit from tree‑shaking.
|
||||
|
||||
---
|
||||
|
||||
|
||||
589
docs/sigpro-full.js
Normal file
589
docs/sigpro-full.js
Normal file
@@ -0,0 +1,589 @@
|
||||
(() => {
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
function __accessProp(key) {
|
||||
return this[key];
|
||||
}
|
||||
var __toCommonJS = (from) => {
|
||||
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
||||
if (entry)
|
||||
return entry;
|
||||
entry = __defProp({}, "__esModule", { value: true });
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (var key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(entry, key))
|
||||
__defProp(entry, key, {
|
||||
get: __accessProp.bind(from, key),
|
||||
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
||||
});
|
||||
}
|
||||
__moduleCache.set(from, entry);
|
||||
return entry;
|
||||
};
|
||||
var __moduleCache;
|
||||
var __returnValue = (v) => v;
|
||||
function __exportSetter(name, newValue) {
|
||||
this[name] = __returnValue.bind(null, newValue);
|
||||
}
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, {
|
||||
get: all[name],
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
set: __exportSetter.bind(all, name)
|
||||
});
|
||||
};
|
||||
|
||||
// sigpro-full.js
|
||||
var exports_sigpro_full = {};
|
||||
__export(exports_sigpro_full, {
|
||||
when: () => when,
|
||||
watch: () => watch,
|
||||
router: () => router,
|
||||
mount: () => mount,
|
||||
h: () => h,
|
||||
filterXSS: () => filterXSS,
|
||||
each: () => each,
|
||||
batch: () => batch,
|
||||
$$: () => $$,
|
||||
$: () => $
|
||||
});
|
||||
|
||||
// sigpro.js
|
||||
var isFunc = (f) => typeof f === "function";
|
||||
var isObj = (o) => o && typeof o === "object";
|
||||
var isArr = Array.isArray;
|
||||
var doc = typeof document !== "undefined" ? document : null;
|
||||
var ensureNode = (n) => n?._isRuntime ? n.container : n instanceof Node ? n : doc.createTextNode(n == null ? "" : String(n));
|
||||
var activeEffect = null;
|
||||
var activeOwner = null;
|
||||
var isFlushing = false;
|
||||
var batchDepth = 0;
|
||||
var effectQueue = new Set;
|
||||
var proxyCache = new WeakMap;
|
||||
var ITER = Symbol("iter");
|
||||
var MOUNTED_NODES = new WeakMap;
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
||||
var SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","));
|
||||
var attrFilter = null;
|
||||
var filterXSS = (fn) => {
|
||||
attrFilter = fn;
|
||||
};
|
||||
var dispose = (eff) => {
|
||||
if (!eff || eff._disposed)
|
||||
return;
|
||||
eff._disposed = true;
|
||||
const stack = [eff];
|
||||
while (stack.length) {
|
||||
const e = stack.pop();
|
||||
if (e._cleanups) {
|
||||
e._cleanups.forEach((fn) => fn());
|
||||
e._cleanups.clear();
|
||||
}
|
||||
if (e._children) {
|
||||
e._children.forEach((child) => stack.push(child));
|
||||
e._children.clear();
|
||||
}
|
||||
if (e._deps) {
|
||||
e._deps.forEach((depSet) => depSet.delete(e));
|
||||
e._deps.clear();
|
||||
}
|
||||
}
|
||||
};
|
||||
var onUnmount = (fn) => {
|
||||
if (activeOwner)
|
||||
(activeOwner._cleanups ||= new Set).add(fn);
|
||||
};
|
||||
var untrack = (fn) => {
|
||||
const p = activeEffect;
|
||||
activeEffect = null;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
activeEffect = p;
|
||||
}
|
||||
};
|
||||
var createEffect = (fn, isComputed = false) => {
|
||||
const effect = () => {
|
||||
if (effect._disposed)
|
||||
return;
|
||||
if (effect._deps)
|
||||
effect._deps.forEach((s) => s.delete(effect));
|
||||
if (effect._cleanups) {
|
||||
effect._cleanups.forEach((c) => c());
|
||||
effect._cleanups.clear();
|
||||
}
|
||||
const prevEffect = activeEffect;
|
||||
const prevOwner = activeOwner;
|
||||
activeEffect = activeOwner = effect;
|
||||
try {
|
||||
return effect._result = fn();
|
||||
} catch (e) {
|
||||
console.error("[SigPro]", e);
|
||||
} finally {
|
||||
activeEffect = prevEffect;
|
||||
activeOwner = prevOwner;
|
||||
}
|
||||
};
|
||||
effect._deps = effect._cleanups = effect._children = null;
|
||||
effect._disposed = false;
|
||||
effect._isComputed = isComputed;
|
||||
effect._depth = activeEffect ? activeEffect._depth + 1 : 0;
|
||||
effect._mounts = [];
|
||||
effect._parent = activeOwner;
|
||||
if (activeOwner)
|
||||
(activeOwner._children ||= new Set).add(effect);
|
||||
return effect;
|
||||
};
|
||||
var flush = () => {
|
||||
if (isFlushing)
|
||||
return;
|
||||
isFlushing = true;
|
||||
const sorted = Array.from(effectQueue).sort((a, b) => a._depth - b._depth);
|
||||
effectQueue.clear();
|
||||
for (const e of sorted)
|
||||
if (!e._disposed)
|
||||
e();
|
||||
isFlushing = false;
|
||||
};
|
||||
var batch = (fn) => {
|
||||
batchDepth++;
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
batchDepth--;
|
||||
if (batchDepth === 0 && effectQueue.size > 0 && !isFlushing) {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
};
|
||||
var trackUpdate = (subs, trigger = false) => {
|
||||
if (!trigger && activeEffect && !activeEffect._disposed) {
|
||||
subs.add(activeEffect);
|
||||
(activeEffect._deps ||= new Set).add(subs);
|
||||
} else if (trigger && subs.size > 0) {
|
||||
let hasQueue = false;
|
||||
for (const e of subs) {
|
||||
if (e === activeEffect || e._disposed)
|
||||
continue;
|
||||
if (e._isComputed) {
|
||||
e._dirty = true;
|
||||
if (e._subs)
|
||||
trackUpdate(e._subs, true);
|
||||
} else {
|
||||
effectQueue.add(e);
|
||||
hasQueue = true;
|
||||
}
|
||||
}
|
||||
if (hasQueue && !isFlushing && batchDepth === 0)
|
||||
queueMicrotask(flush);
|
||||
}
|
||||
};
|
||||
var $ = (val, key = null) => {
|
||||
const subs = new Set;
|
||||
if (isFunc(val)) {
|
||||
let cache;
|
||||
const computed = () => {
|
||||
if (computed._dirty) {
|
||||
const prev = activeEffect;
|
||||
activeEffect = computed;
|
||||
try {
|
||||
const next = val();
|
||||
if (!Object.is(cache, next)) {
|
||||
cache = next;
|
||||
trackUpdate(subs, true);
|
||||
}
|
||||
} finally {
|
||||
activeEffect = prev;
|
||||
}
|
||||
computed._dirty = false;
|
||||
}
|
||||
trackUpdate(subs);
|
||||
return cache;
|
||||
};
|
||||
computed._isComputed = true;
|
||||
computed._subs = subs;
|
||||
computed._dirty = true;
|
||||
computed._deps = null;
|
||||
computed._disposed = false;
|
||||
return computed;
|
||||
}
|
||||
if (key)
|
||||
try {
|
||||
val = JSON.parse(localStorage.getItem(key)) ?? val;
|
||||
} catch (e) {}
|
||||
return (...args) => {
|
||||
if (args.length) {
|
||||
const next = isFunc(args[0]) ? args[0](val) : args[0];
|
||||
if (!Object.is(val, next)) {
|
||||
val = next;
|
||||
if (key)
|
||||
localStorage.setItem(key, JSON.stringify(val));
|
||||
trackUpdate(subs, true);
|
||||
}
|
||||
}
|
||||
trackUpdate(subs);
|
||||
return val;
|
||||
};
|
||||
};
|
||||
var $$ = (target) => {
|
||||
if (!isObj(target))
|
||||
return target;
|
||||
const cached = proxyCache.get(target);
|
||||
if (cached)
|
||||
return cached;
|
||||
const subs = new Map;
|
||||
const getSubs = (key) => {
|
||||
let set = subs.get(key);
|
||||
if (!set)
|
||||
subs.set(key, set = new Set);
|
||||
return set;
|
||||
};
|
||||
const proxy = new Proxy(target, {
|
||||
get(target2, key, receiver) {
|
||||
if (typeof key !== "symbol")
|
||||
trackUpdate(getSubs(key));
|
||||
return $$(Reflect.get(target2, key, receiver));
|
||||
},
|
||||
set(target2, key, value, receiver) {
|
||||
const hadKey = Reflect.has(target2, key);
|
||||
const oldValue = Reflect.get(target2, key, receiver);
|
||||
const result = Reflect.set(target2, key, value, receiver);
|
||||
if (result && !Object.is(oldValue, value)) {
|
||||
trackUpdate(getSubs(key), true);
|
||||
if (!hadKey)
|
||||
trackUpdate(getSubs(ITER), true);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
deleteProperty(target2, key) {
|
||||
const result = Reflect.deleteProperty(target2, key);
|
||||
if (result) {
|
||||
trackUpdate(getSubs(key), true);
|
||||
trackUpdate(getSubs(ITER), true);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
ownKeys(target2) {
|
||||
trackUpdate(getSubs(ITER));
|
||||
return Reflect.ownKeys(target2);
|
||||
}
|
||||
});
|
||||
proxyCache.set(target, proxy);
|
||||
return proxy;
|
||||
};
|
||||
var watch = (sources, cb) => {
|
||||
if (cb === undefined) {
|
||||
const effect2 = createEffect(sources);
|
||||
effect2();
|
||||
return () => dispose(effect2);
|
||||
}
|
||||
const effect = createEffect(() => {
|
||||
const vals = Array.isArray(sources) ? sources.map((s) => s()) : sources();
|
||||
untrack(() => cb(vals));
|
||||
});
|
||||
effect();
|
||||
return () => dispose(effect);
|
||||
};
|
||||
var cleanupNode = (node) => {
|
||||
if (!node)
|
||||
return;
|
||||
if (node._cleanups) {
|
||||
node._cleanups.forEach((fn) => fn());
|
||||
node._cleanups.clear();
|
||||
}
|
||||
if (node._ownerEffect)
|
||||
dispose(node._ownerEffect);
|
||||
if (node.childNodes)
|
||||
node.childNodes.forEach((n) => cleanupNode(n));
|
||||
};
|
||||
var h = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||
children = props;
|
||||
props = {};
|
||||
}
|
||||
if (isFunc(tag)) {
|
||||
const effect = createEffect(() => {
|
||||
const result2 = tag(props, {
|
||||
children,
|
||||
emit: (ev, ...args) => props[`on${ev[0].toUpperCase()}${ev.slice(1)}`]?.(...args)
|
||||
});
|
||||
effect._result = result2;
|
||||
return result2;
|
||||
});
|
||||
effect();
|
||||
const result = effect._result;
|
||||
if (result == null)
|
||||
return null;
|
||||
const node = result instanceof Node || isArr(result) && result.every((n) => n instanceof Node) ? result : doc.createTextNode(String(result));
|
||||
const attach = (n) => {
|
||||
if (isObj(n) && !n._isRuntime) {
|
||||
n._mounts = effect._mounts || [];
|
||||
n._cleanups = effect._cleanups || new Set;
|
||||
n._ownerEffect = effect;
|
||||
}
|
||||
};
|
||||
isArr(node) ? node.forEach(attach) : attach(node);
|
||||
return node;
|
||||
}
|
||||
const isSVG = SVG_TAGS.has(tag);
|
||||
const el = isSVG ? doc.createElementNS(SVG_NS, tag) : doc.createElement(tag);
|
||||
el._cleanups = new Set;
|
||||
for (const k of Object.keys(props)) {
|
||||
let v = props[k];
|
||||
if (k === "ref") {
|
||||
isFunc(v) ? v(el) : v.current = el;
|
||||
continue;
|
||||
}
|
||||
let val = attrFilter ? attrFilter(k, v) : v;
|
||||
if (isSVG && k.startsWith("xlink:")) {
|
||||
val == null ? el.removeAttributeNS(XLINK_NS, k.slice(6)) : el.setAttributeNS(XLINK_NS, k.slice(6), val);
|
||||
continue;
|
||||
}
|
||||
if (k.startsWith("on")) {
|
||||
const ev = k.slice(2).toLowerCase();
|
||||
el.addEventListener(ev, val);
|
||||
const off = () => el.removeEventListener(ev, val);
|
||||
el._cleanups.add(off);
|
||||
onUnmount(off);
|
||||
} else if (isFunc(val)) {
|
||||
const effect = createEffect(() => {
|
||||
const raw = val();
|
||||
const safeVal = attrFilter ? attrFilter(k, raw) : raw;
|
||||
if (k === "class")
|
||||
el.className = safeVal || "";
|
||||
else if (safeVal == null)
|
||||
el.removeAttribute(k);
|
||||
else if (k in el && !isSVG)
|
||||
el[k] = safeVal;
|
||||
else
|
||||
el.setAttribute(k, safeVal === true ? "" : safeVal);
|
||||
});
|
||||
effect();
|
||||
el._cleanups.add(() => dispose(effect));
|
||||
onUnmount(() => dispose(effect));
|
||||
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||
const evType = k === "checked" ? "change" : "input";
|
||||
el.addEventListener(evType, (ev) => val(ev.target[k]));
|
||||
}
|
||||
} else {
|
||||
if (val != null) {
|
||||
if (k in el && !isSVG)
|
||||
el[k] = val;
|
||||
else
|
||||
el.setAttribute(k, val === true ? "" : val);
|
||||
}
|
||||
}
|
||||
}
|
||||
const append = (c) => {
|
||||
if (isArr(c))
|
||||
return c.forEach(append);
|
||||
if (isFunc(c)) {
|
||||
const anchor = doc.createTextNode("");
|
||||
el.appendChild(anchor);
|
||||
let currentNodes = [];
|
||||
const effect = createEffect(() => {
|
||||
const res = c();
|
||||
const next = (isArr(res) ? res : [res]).map(ensureNode);
|
||||
currentNodes.forEach((n) => {
|
||||
if (n._isRuntime)
|
||||
n.destroy();
|
||||
else
|
||||
cleanupNode(n);
|
||||
if (n.parentNode)
|
||||
n.remove();
|
||||
});
|
||||
let ref = anchor;
|
||||
for (let i = next.length - 1;i >= 0; i--) {
|
||||
const node = next[i];
|
||||
if (node.parentNode !== ref.parentNode)
|
||||
ref.parentNode?.insertBefore(node, ref);
|
||||
if (node._mounts)
|
||||
node._mounts.forEach((fn) => fn());
|
||||
ref = node;
|
||||
}
|
||||
currentNodes = next;
|
||||
});
|
||||
effect();
|
||||
el._cleanups.add(() => dispose(effect));
|
||||
onUnmount(() => dispose(effect));
|
||||
} else {
|
||||
const node = ensureNode(c);
|
||||
el.appendChild(node);
|
||||
if (node._mounts)
|
||||
node._mounts.forEach((fn) => fn());
|
||||
}
|
||||
};
|
||||
append(children);
|
||||
return el;
|
||||
};
|
||||
var render = (renderFn) => {
|
||||
const cleanups = new Set;
|
||||
const previousOwner = activeOwner;
|
||||
const previousEffect = activeEffect;
|
||||
const container = doc.createElement("div");
|
||||
container.style.display = "contents";
|
||||
container.setAttribute("role", "presentation");
|
||||
activeOwner = { _cleanups: cleanups };
|
||||
activeEffect = null;
|
||||
const processResult = (result) => {
|
||||
if (!result)
|
||||
return;
|
||||
if (result._isRuntime) {
|
||||
cleanups.add(result.destroy);
|
||||
container.appendChild(result.container);
|
||||
} else if (isArr(result)) {
|
||||
result.forEach(processResult);
|
||||
} else {
|
||||
container.appendChild(result instanceof Node ? result : doc.createTextNode(String(result == null ? "" : result)));
|
||||
}
|
||||
};
|
||||
try {
|
||||
processResult(renderFn({ onCleanup: (fn) => cleanups.add(fn) }));
|
||||
} finally {
|
||||
activeOwner = previousOwner;
|
||||
activeEffect = previousEffect;
|
||||
}
|
||||
return {
|
||||
_isRuntime: true,
|
||||
container,
|
||||
destroy: () => {
|
||||
cleanups.forEach((fn) => fn());
|
||||
cleanupNode(container);
|
||||
container.remove();
|
||||
}
|
||||
};
|
||||
};
|
||||
var when = (cond, SIP, NOP = null) => {
|
||||
const anchor = doc.createTextNode("");
|
||||
const root = h("div", { style: "display:contents" }, [anchor]);
|
||||
let currentView = null;
|
||||
watch(() => !!(isFunc(cond) ? cond() : cond), (show) => {
|
||||
if (currentView) {
|
||||
currentView.destroy();
|
||||
currentView = null;
|
||||
}
|
||||
const content = show ? SIP : NOP;
|
||||
if (content) {
|
||||
currentView = render(() => isFunc(content) ? content() : content);
|
||||
root.insertBefore(currentView.container, anchor);
|
||||
}
|
||||
});
|
||||
onUnmount(() => currentView?.destroy());
|
||||
return root;
|
||||
};
|
||||
var each = (src, itemFn, keyField) => {
|
||||
const anchor = doc.createTextNode("");
|
||||
const root = h("div", { style: "display:contents" }, [anchor]);
|
||||
let cache = new Map;
|
||||
watch(() => (isFunc(src) ? src() : src) || [], (items) => {
|
||||
const nextCache = new Map;
|
||||
const nextOrder = [];
|
||||
const newItems = items || [];
|
||||
for (let i = 0;i < newItems.length; i++) {
|
||||
const item = newItems[i];
|
||||
const key = keyField ? item?.[keyField] ?? i : item?.id ?? i;
|
||||
let view = cache.get(key);
|
||||
if (!view)
|
||||
view = render(() => itemFn(item, i));
|
||||
else
|
||||
cache.delete(key);
|
||||
nextCache.set(key, view);
|
||||
nextOrder.push(view);
|
||||
}
|
||||
cache.forEach((view) => view.destroy());
|
||||
let lastRef = anchor;
|
||||
for (let i = nextOrder.length - 1;i >= 0; i--) {
|
||||
const view = nextOrder[i];
|
||||
const node = view.container;
|
||||
if (node.nextSibling !== lastRef)
|
||||
root.insertBefore(node, lastRef);
|
||||
lastRef = node;
|
||||
}
|
||||
cache = nextCache;
|
||||
});
|
||||
return root;
|
||||
};
|
||||
var router = (routes) => {
|
||||
const getHash = () => window.location.hash.slice(1) || "/";
|
||||
const path = $(getHash());
|
||||
const handler = () => path(getHash());
|
||||
window.addEventListener("hashchange", handler);
|
||||
onUnmount(() => window.removeEventListener("hashchange", handler));
|
||||
const hook = h("div", { class: "router-hook" });
|
||||
let currentView = null;
|
||||
watch([path], () => {
|
||||
const cur = path();
|
||||
const route = routes.find((r) => {
|
||||
const p1 = r.path.split("/").filter(Boolean);
|
||||
const p2 = cur.split("/").filter(Boolean);
|
||||
return p1.length === p2.length && p1.every((p, i) => p[0] === ":" || p === p2[i]);
|
||||
}) || routes.find((r) => r.path === "*");
|
||||
if (route) {
|
||||
currentView?.destroy();
|
||||
const params = {};
|
||||
route.path.split("/").filter(Boolean).forEach((p, i) => {
|
||||
if (p[0] === ":")
|
||||
params[p.slice(1)] = cur.split("/").filter(Boolean)[i];
|
||||
});
|
||||
router.params(params);
|
||||
currentView = render(() => isFunc(route.component) ? route.component(params) : route.component);
|
||||
hook.replaceChildren(currentView.container);
|
||||
}
|
||||
});
|
||||
return hook;
|
||||
};
|
||||
router.params = $({});
|
||||
router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||
router.back = () => window.history.back();
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
var mount = (comp, target) => {
|
||||
const t = typeof target === "string" ? doc.querySelector(target) : target;
|
||||
if (!t)
|
||||
return;
|
||||
if (MOUNTED_NODES.has(t))
|
||||
MOUNTED_NODES.get(t).destroy();
|
||||
const inst = render(isFunc(comp) ? comp : () => comp);
|
||||
t.replaceChildren(inst.container);
|
||||
MOUNTED_NODES.set(t, inst);
|
||||
return inst;
|
||||
};
|
||||
// sigpro/tags.js
|
||||
if (typeof window !== "undefined") {
|
||||
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
|
||||
window[tag] = (props, children) => h(tag, props, children);
|
||||
});
|
||||
console.log("SigPro tags ready");
|
||||
}
|
||||
|
||||
// sigpro/xss.js
|
||||
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||
var DANGEROUS_URI_ATTRS = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
|
||||
var isDangerousAttr = (key) => DANGEROUS_URI_ATTRS.has(key) || key.startsWith("on");
|
||||
var validateAttr = (key, val) => {
|
||||
if (val == null || val === false)
|
||||
return null;
|
||||
if (isDangerousAttr(key)) {
|
||||
const sVal = String(val);
|
||||
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
||||
console.warn(`[SigPro XSS] Locked ${key}`);
|
||||
return "#";
|
||||
}
|
||||
}
|
||||
return val;
|
||||
};
|
||||
filterXSS(validateAttr);
|
||||
|
||||
// sigpro-full.js
|
||||
if (typeof window !== "undefined") {
|
||||
const props = {};
|
||||
for (const fn of [["$", $], ["$$", $$], ["watch", watch], ["h", h], ["when", when], ["each", each], ["router", router], ["mount", mount], ["batch", batch]]) {
|
||||
props[fn[0]] = { value: fn[1], writable: false, configurable: false, enumerable: true };
|
||||
}
|
||||
Object.defineProperties(window, props);
|
||||
}
|
||||
})();
|
||||
132
docs/sigpro.js
132
docs/sigpro.js
@@ -42,12 +42,13 @@
|
||||
__export(exports_sigpro, {
|
||||
when: () => when,
|
||||
watch: () => watch,
|
||||
sigpro: () => sigpro,
|
||||
setAttrFilter: () => setAttrFilter,
|
||||
router: () => router,
|
||||
req: () => req,
|
||||
render: () => render,
|
||||
onUnmount: () => onUnmount,
|
||||
mount: () => mount,
|
||||
isFunc: () => isFunc,
|
||||
h: () => h,
|
||||
fx: () => fx,
|
||||
each: () => each,
|
||||
batch: () => batch,
|
||||
$$: () => $$,
|
||||
@@ -71,6 +72,10 @@
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
var XLINK_NS = "http://www.w3.org/1999/xlink";
|
||||
var SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","));
|
||||
var attrFilter = null;
|
||||
var setAttrFilter = (fn) => {
|
||||
attrFilter = fn;
|
||||
};
|
||||
var dispose = (eff) => {
|
||||
if (!eff || eff._disposed)
|
||||
return;
|
||||
@@ -299,21 +304,6 @@
|
||||
if (node.childNodes)
|
||||
node.childNodes.forEach((n) => cleanupNode(n));
|
||||
};
|
||||
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||
var DANGEROUS_URI_ATTRS = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
|
||||
var isDangerousAttr = (key) => DANGEROUS_URI_ATTRS.has(key) || key.startsWith("on");
|
||||
var validateAttr = (key, val) => {
|
||||
if (val == null || val === false)
|
||||
return null;
|
||||
if (isDangerousAttr(key)) {
|
||||
const sVal = String(val);
|
||||
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
||||
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`);
|
||||
return "#";
|
||||
}
|
||||
}
|
||||
return val;
|
||||
};
|
||||
var h = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||
children = props;
|
||||
@@ -352,38 +342,38 @@
|
||||
isFunc(v) ? v(el) : v.current = el;
|
||||
continue;
|
||||
}
|
||||
let val = attrFilter ? attrFilter(k, v) : v;
|
||||
if (isSVG && k.startsWith("xlink:")) {
|
||||
const cleanVal = validateAttr(k.slice(6), v);
|
||||
cleanVal == null ? el.removeAttributeNS(XLINK_NS, k.slice(6)) : el.setAttributeNS(XLINK_NS, k.slice(6), cleanVal);
|
||||
val == null ? el.removeAttributeNS(XLINK_NS, k.slice(6)) : el.setAttributeNS(XLINK_NS, k.slice(6), val);
|
||||
continue;
|
||||
}
|
||||
if (k.startsWith("on")) {
|
||||
const ev = k.slice(2).toLowerCase();
|
||||
el.addEventListener(ev, v);
|
||||
const off = () => el.removeEventListener(ev, v);
|
||||
el.addEventListener(ev, val);
|
||||
const off = () => el.removeEventListener(ev, val);
|
||||
el._cleanups.add(off);
|
||||
onUnmount(off);
|
||||
} else if (isFunc(v)) {
|
||||
} else if (isFunc(val)) {
|
||||
const effect = createEffect(() => {
|
||||
const val = validateAttr(k, v());
|
||||
const raw = val();
|
||||
const safeVal = attrFilter ? attrFilter(k, raw) : raw;
|
||||
if (k === "class")
|
||||
el.className = val || "";
|
||||
else if (val == null)
|
||||
el.className = safeVal || "";
|
||||
else if (safeVal == null)
|
||||
el.removeAttribute(k);
|
||||
else if (k in el && !isSVG)
|
||||
el[k] = val;
|
||||
el[k] = safeVal;
|
||||
else
|
||||
el.setAttribute(k, val === true ? "" : val);
|
||||
el.setAttribute(k, safeVal === true ? "" : safeVal);
|
||||
});
|
||||
effect();
|
||||
el._cleanups.add(() => dispose(effect));
|
||||
onUnmount(() => dispose(effect));
|
||||
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||
const evType = k === "checked" ? "change" : "input";
|
||||
el.addEventListener(evType, (ev) => v(ev.target[k]));
|
||||
el.addEventListener(evType, (ev) => val(ev.target[k]));
|
||||
}
|
||||
} else {
|
||||
const val = validateAttr(k, v);
|
||||
if (val != null) {
|
||||
if (k in el && !isSVG)
|
||||
el[k] = val;
|
||||
@@ -489,35 +479,6 @@
|
||||
onUnmount(() => currentView?.destroy());
|
||||
return root;
|
||||
};
|
||||
var fx = ({ name, duration = 200, scale, slide, rotate, blur }, child) => {
|
||||
const el = typeof child === "function" ? child() : child;
|
||||
if (!(el instanceof Node))
|
||||
return el;
|
||||
if (name) {
|
||||
el.style.animation = `${name}-in ${duration}ms`;
|
||||
return el;
|
||||
}
|
||||
const hasTransform = scale || slide || rotate || blur;
|
||||
const initialTransform = [
|
||||
scale ? "scale(0.95)" : "",
|
||||
slide ? "translateY(-10px)" : "",
|
||||
rotate ? "rotate(-2deg)" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
el.style.transition = `all ${duration}ms ease`;
|
||||
el.style.opacity = "0";
|
||||
if (hasTransform)
|
||||
el.style.transform = initialTransform;
|
||||
if (blur)
|
||||
el.style.filter = "blur(4px)";
|
||||
requestAnimationFrame(() => {
|
||||
el.style.opacity = "1";
|
||||
if (hasTransform)
|
||||
el.style.transform = "none";
|
||||
if (blur)
|
||||
el.style.filter = "none";
|
||||
});
|
||||
return el;
|
||||
};
|
||||
var each = (src, itemFn, keyField) => {
|
||||
const anchor = doc.createTextNode("");
|
||||
const root = h("div", { style: "display:contents" }, [anchor]);
|
||||
@@ -583,47 +544,6 @@
|
||||
router.to = (p) => window.location.hash = p.replace(/^#?\/?/, "#/");
|
||||
router.back = () => window.history.back();
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/";
|
||||
var req = ({ url, method = "GET", headers = {} }) => {
|
||||
const loading = $(false);
|
||||
const error = $(null);
|
||||
const data = $(null);
|
||||
let controller = null;
|
||||
let timeoutId = null;
|
||||
const run = async (body = null) => {
|
||||
controller?.abort();
|
||||
clearTimeout(timeoutId);
|
||||
controller = new AbortController;
|
||||
timeoutId = setTimeout(() => controller.abort(), 1e4);
|
||||
loading(true);
|
||||
error(null);
|
||||
try {
|
||||
const isFormData = body instanceof FormData;
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: isFormData ? headers : { "Content-Type": "application/json", ...headers },
|
||||
body: isFormData ? body : body ? JSON.stringify(body) : undefined,
|
||||
signal: controller.signal
|
||||
});
|
||||
const text = await res.text();
|
||||
const json = text ? JSON.parse(text) : null;
|
||||
if (!res.ok)
|
||||
throw new Error(json?.message || res.statusText);
|
||||
data(json);
|
||||
return json;
|
||||
} catch (e) {
|
||||
if (e.name !== "AbortError")
|
||||
error(e.message);
|
||||
throw e;
|
||||
} finally {
|
||||
loading(false);
|
||||
clearTimeout(timeoutId);
|
||||
controller = null;
|
||||
timeoutId = null;
|
||||
}
|
||||
};
|
||||
const abort = () => controller?.abort();
|
||||
return { run, abort, loading, error, data };
|
||||
};
|
||||
var mount = (comp, target) => {
|
||||
const t = typeof target === "string" ? doc.querySelector(target) : target;
|
||||
if (!t)
|
||||
@@ -635,16 +555,4 @@
|
||||
MOUNTED_NODES.set(t, inst);
|
||||
return inst;
|
||||
};
|
||||
var sigproFn = Object.freeze({ $, $$, watch, h, when, each, fx, router, req, mount, batch });
|
||||
var sigpro = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, sigproFn);
|
||||
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video".split(" ").forEach((tag) => {
|
||||
window[tag] = (props, children) => h(tag, props, children);
|
||||
});
|
||||
console.log("SigPro DX installed.");
|
||||
}
|
||||
};
|
||||
if (typeof import.meta === "undefined" && typeof window !== "undefined")
|
||||
sigpro();
|
||||
})();
|
||||
|
||||
49
package.json
49
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sigpro",
|
||||
"version": "1.2.22",
|
||||
"version": "1.2.24",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"main": "./dist/sigpro.esm.min.js",
|
||||
@@ -11,12 +11,9 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/sigpro.esm.min.js",
|
||||
"script": "./dist/sigpro.js",
|
||||
"script": "./dist/sigpro.min.js",
|
||||
"types": "./sigpro.d.ts"
|
||||
},
|
||||
"./router": "./sigpro/router.js",
|
||||
"./fx": "./sigpro/fx.js",
|
||||
"./req": "./sigpro/req.js",
|
||||
"./xss": "./sigpro/xss.js",
|
||||
"./tags": "./sigpro/tags.js",
|
||||
"./vite": "./vite/index.js",
|
||||
@@ -26,8 +23,8 @@
|
||||
"index.js",
|
||||
"sigpro.js",
|
||||
"sigpro/",
|
||||
"dist",
|
||||
"vite",
|
||||
"dist/",
|
||||
"vite/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
@@ -39,29 +36,31 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/natxocc/sigpro/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist",
|
||||
"prebuild": "npm run clean",
|
||||
"build:iife": "bun build ./index.js --bundle --outfile=./dist/sigpro.js --format=iife --global-name=SigPro",
|
||||
"build:docs": "bun build ./index.js --bundle --outfile=./docs/sigpro.js --format=iife --global-name=SigPro",
|
||||
"build:iife:min": "bun build ./index.js --bundle --outfile=./dist/sigpro.min.js --format=iife --global-name=SigPro --minify",
|
||||
"build:esm": "bun build ./index.js --bundle --outfile=./dist/sigpro.esm.js --format=esm",
|
||||
"build:esm:min": "bun build ./index.js --bundle --outfile=./dist/sigpro.esm.min.js --format=esm --minify",
|
||||
"build": "bun run build:iife && bun run build:iife:min && bun run build:esm && bun run build:esm:min && bun run build:docs",
|
||||
"docs": "bun x serve docs",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist",
|
||||
"prebuild": "npm run clean",
|
||||
"build:esm": "bun build ./index.js --bundle --outfile=./dist/sigpro.esm.js --format=esm",
|
||||
"build:esm:min": "bun build ./index.js --bundle --outfile=./dist/sigpro.esm.min.js --format=esm --minify",
|
||||
"build:iife": "bun build ./sigpro-full.js --bundle --outfile=./dist/sigpro.js --format=iife --global-name=SigPro",
|
||||
"build:iife:min": "bun build ./sigpro-full.js --bundle --outfile=./dist/sigpro.min.js --format=iife --global-name=SigPro --minify",
|
||||
"build:docs": "bun build ./sigpro-full.js --bundle --outfile=./docs/sigpro-full.js --format=iife --global-name=SigPro",
|
||||
"build": "bun run build:esm && bun run build:esm:min && bun run build:iife && bun run build:iife:min && bun run build:docs",
|
||||
"docs": "bun x serve docs",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"signals",
|
||||
"reactivity",
|
||||
"reactive",
|
||||
"pure",
|
||||
"vanilla",
|
||||
"js",
|
||||
"library",
|
||||
"framework",
|
||||
"zero",
|
||||
"ui",
|
||||
"dom",
|
||||
"state",
|
||||
"frontend",
|
||||
"spa",
|
||||
"lightweight"
|
||||
"lightweight",
|
||||
"no-vdom",
|
||||
"sigpro"
|
||||
]
|
||||
}
|
||||
}
|
||||
12
sigpro-full.js
Normal file
12
sigpro-full.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// sigpro-full.js
|
||||
export * from './sigpro.js';
|
||||
import './sigpro/tags.js';
|
||||
import './sigpro/xss.js';
|
||||
import { $, $$, watch, h, when, each, router, mount, batch } from './sigpro.js';
|
||||
if (typeof window !== 'undefined') {
|
||||
const props = {};
|
||||
for (const fn of [['$', $], ['$$', $$], ['watch', watch], ['h', h], ['when', when], ['each', each], ['router', router], ['mount', mount], ['batch', batch]]) {
|
||||
props[fn[0]] = { value: fn[1], writable: false, configurable: false, enumerable: true };
|
||||
}
|
||||
Object.defineProperties(window, props);
|
||||
}
|
||||
137
sigpro.js
137
sigpro.js
@@ -17,6 +17,9 @@ const SVG_NS = "http://www.w3.org/2000/svg"
|
||||
const XLINK_NS = "http://www.w3.org/1999/xlink"
|
||||
const SVG_TAGS = new Set("svg,path,circle,rect,line,polyline,polygon,g,defs,text,textPath,tspan,use,symbol,image,marker,ellipse".split(","))
|
||||
|
||||
let attrFilter = null
|
||||
const filterXSS = fn => { attrFilter = fn }
|
||||
|
||||
const dispose = eff => {
|
||||
if (!eff || eff._disposed) return
|
||||
eff._disposed = true
|
||||
@@ -232,22 +235,6 @@ const cleanupNode = (node) => {
|
||||
if (node.childNodes) node.childNodes.forEach(n => cleanupNode(n));
|
||||
};
|
||||
|
||||
var DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||
var DANGEROUS_URI_ATTRS = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
|
||||
var isDangerousAttr = (key) => DANGEROUS_URI_ATTRS.has(key) || key.startsWith("on");
|
||||
|
||||
const validateAttr = (key, val) => {
|
||||
if (val == null || val === false) return null
|
||||
if (isDangerousAttr(key)) {
|
||||
const sVal = String(val)
|
||||
if (DANGEROUS_PROTOCOL.test(sVal)) {
|
||||
console.warn(`[SigPro] Bloqueado protocolo peligroso en ${key}`)
|
||||
return '#'
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
const h = (tag, props = {}, children = []) => {
|
||||
if (props instanceof Node || isArr(props) || !isObj(props)) {
|
||||
children = props
|
||||
@@ -294,36 +281,37 @@ const h = (tag, props = {}, children = []) => {
|
||||
isFunc(v) ? v(el) : (v.current = el)
|
||||
continue
|
||||
}
|
||||
let val = attrFilter ? attrFilter(k, v) : v
|
||||
|
||||
if (isSVG && k.startsWith("xlink:")) {
|
||||
const cleanVal = validateAttr(k.slice(6), v)
|
||||
cleanVal == null
|
||||
val == null
|
||||
? el.removeAttributeNS(XLINK_NS, k.slice(6))
|
||||
: el.setAttributeNS(XLINK_NS, k.slice(6), cleanVal)
|
||||
: el.setAttributeNS(XLINK_NS, k.slice(6), val)
|
||||
continue
|
||||
}
|
||||
if (k.startsWith("on")) {
|
||||
const ev = k.slice(2).toLowerCase()
|
||||
el.addEventListener(ev, v)
|
||||
const off = () => el.removeEventListener(ev, v)
|
||||
el.addEventListener(ev, val)
|
||||
const off = () => el.removeEventListener(ev, val)
|
||||
el._cleanups.add(off)
|
||||
onUnmount(off)
|
||||
} else if (isFunc(v)) {
|
||||
} else if (isFunc(val)) {
|
||||
const effect = createEffect(() => {
|
||||
const val = validateAttr(k, v())
|
||||
if (k === "class") el.className = val || ""
|
||||
else if (val == null) el.removeAttribute(k)
|
||||
else if (k in el && !isSVG) el[k] = val
|
||||
else el.setAttribute(k, val === true ? "" : val)
|
||||
const raw = val()
|
||||
const safeVal = attrFilter ? attrFilter(k, raw) : raw
|
||||
if (k === "class") el.className = safeVal || ""
|
||||
else if (safeVal == null) el.removeAttribute(k)
|
||||
else if (k in el && !isSVG) el[k] = safeVal
|
||||
else el.setAttribute(k, safeVal === true ? "" : safeVal)
|
||||
})
|
||||
effect()
|
||||
el._cleanups.add(() => dispose(effect))
|
||||
onUnmount(() => dispose(effect))
|
||||
if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (k === "value" || k === "checked")) {
|
||||
const evType = k === "checked" ? "change" : "input"
|
||||
el.addEventListener(evType, ev => v(ev.target[k]))
|
||||
el.addEventListener(evType, ev => val(ev.target[k]))
|
||||
}
|
||||
} else {
|
||||
const val = validateAttr(k, v)
|
||||
if (val != null) {
|
||||
if (k in el && !isSVG) el[k] = val
|
||||
else el.setAttribute(k, val === true ? "" : val)
|
||||
@@ -432,36 +420,6 @@ const when = (cond, SIP, NOP = null) => {
|
||||
return root
|
||||
}
|
||||
|
||||
const fx = ({ name, duration = 200, scale, slide, rotate, blur }, child) => {
|
||||
const el = typeof child === "function" ? child() : child;
|
||||
if (!(el instanceof Node)) return el;
|
||||
|
||||
if (name) {
|
||||
el.style.animation = `${name}-in ${duration}ms`;
|
||||
return el;
|
||||
}
|
||||
|
||||
const hasTransform = scale || slide || rotate || blur;
|
||||
const initialTransform = [
|
||||
scale ? "scale(0.95)" : "",
|
||||
slide ? "translateY(-10px)" : "",
|
||||
rotate ? "rotate(-2deg)" : ""
|
||||
].filter(Boolean).join(" ");
|
||||
|
||||
el.style.transition = `all ${duration}ms ease`;
|
||||
el.style.opacity = "0";
|
||||
if (hasTransform) el.style.transform = initialTransform;
|
||||
if (blur) el.style.filter = "blur(4px)";
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
el.style.opacity = "1";
|
||||
if (hasTransform) el.style.transform = "none";
|
||||
if (blur) el.style.filter = "none";
|
||||
});
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
const each = (src, itemFn, keyField) => {
|
||||
const anchor = doc.createTextNode("")
|
||||
const root = h("div", { style: "display:contents" }, [anchor])
|
||||
@@ -525,52 +483,6 @@ router.to = p => window.location.hash = p.replace(/^#?\/?/, "#/")
|
||||
router.back = () => window.history.back()
|
||||
router.path = () => window.location.hash.replace(/^#/, "") || "/"
|
||||
|
||||
const req = ({ url, method = 'GET', headers = {} }) => {
|
||||
const loading = $(false);
|
||||
const error = $(null);
|
||||
const data = $(null);
|
||||
let controller = null;
|
||||
let timeoutId = null;
|
||||
|
||||
const run = async (body = null) => {
|
||||
controller?.abort();
|
||||
clearTimeout(timeoutId);
|
||||
controller = new AbortController();
|
||||
timeoutId = setTimeout(() => controller.abort(), 10000);
|
||||
loading(true);
|
||||
error(null);
|
||||
|
||||
try {
|
||||
const isFormData = body instanceof FormData;
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: isFormData ? headers : { 'Content-Type': 'application/json', ...headers },
|
||||
body: isFormData ? body : (body ? JSON.stringify(body) : undefined),
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
const json = text ? JSON.parse(text) : null;
|
||||
|
||||
if (!res.ok) throw new Error(json?.message || res.statusText);
|
||||
data(json);
|
||||
return json;
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') error(e.message);
|
||||
throw e;
|
||||
} finally {
|
||||
loading(false);
|
||||
clearTimeout(timeoutId);
|
||||
controller = null;
|
||||
timeoutId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const abort = () => controller?.abort();
|
||||
|
||||
return { run, abort, loading, error, data };
|
||||
};
|
||||
|
||||
const mount = (comp, target) => {
|
||||
const t = typeof target === "string" ? doc.querySelector(target) : target
|
||||
if (!t) return
|
||||
@@ -581,17 +493,4 @@ const mount = (comp, target) => {
|
||||
return inst
|
||||
}
|
||||
|
||||
const sigproFn = Object.freeze({ $, $$, watch, h, when, each, fx, router, req, mount, batch })
|
||||
|
||||
const sigpro = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
Object.assign(window, sigproFn)
|
||||
"a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video"
|
||||
.split(" ").forEach(tag => { window[tag] = (props, children) => h(tag, props, children) })
|
||||
console.log("SigPro DX installed.")
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof import.meta === 'undefined' && typeof window !== 'undefined') sigpro()
|
||||
|
||||
export { sigpro, $, $$, watch, h, when, each, fx, router, req, mount, batch }
|
||||
export { $, $$, watch, h, when, each, router, mount, batch, filterXSS }
|
||||
@@ -1,4 +1,4 @@
|
||||
import { h } from './sigpro.core.js';
|
||||
import { h } from '../sigpro.js';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
'a abbr article aside audio b blockquote br button canvas caption cite code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 header hr i iframe img input ins kbd label legend li main mark meter nav object ol optgroup option output p picture pre progress section select slot small source span strong sub summary sup svg table tbody td template textarea tfoot th thead time tr u ul video'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { setAttrFilter } from './sigpro.core.js';
|
||||
import { filterXSS } from '../sigpro.js';
|
||||
|
||||
const DANGEROUS_PROTOCOL = /^\s*(javascript|data|vbscript):/i;
|
||||
const DANGEROUS_URI_ATTRS = new Set(["src", "href", "formaction", "action", "background", "code", "archive"]);
|
||||
@@ -16,4 +16,4 @@ const validateAttr = (key, val) => {
|
||||
return val;
|
||||
};
|
||||
|
||||
setAttrFilter(validateAttr);
|
||||
filterXSS(validateAttr);
|
||||
Reference in New Issue
Block a user