1
0
mirror of https://github.com/jaandrle/deka-dom-el synced 2025-07-03 05:02:15 +02:00

12 Commits

Author SHA1 Message Date
c3a17e6dde 🔤 UI enhancements 2025-03-04 11:06:44 +01:00
bdb20ec298 🔤 Docs UI/UX 2025-03-03 19:06:23 +01:00
7ec50e1660 🐛 coumputed signal 2025-03-03 15:25:04 +01:00
6c4ddd655f 🔤 2025-03-03 15:21:43 +01:00
198f4a3777 🔤 2025-03-03 15:20:31 +01:00
3435ea6cfe 🐛 Better types for on* 2025-03-03 15:10:20 +01:00
ed7e6c7963 Refatc signals to .get/.set syntax #26 2025-03-03 14:19:41 +01:00
3168f452ae wip 2025-02-28 19:53:07 +01:00
b53f3926b3 wip 2025-02-28 17:12:40 +01:00
8f2fd5a68c 🔤 2025-02-28 14:32:51 +01:00
f53b97a89c wip 2025-02-28 13:40:56 +01:00
f8a94ab9f8 🎉 2025-02-28 13:05:46 +01:00
45 changed files with 4944 additions and 2488 deletions

View File

@ -18,19 +18,19 @@ function HelloWorldComponent({ initial }){
/** @param {HTMLOptionElement} el */ /** @param {HTMLOptionElement} el */
const isSelected= el=> (el.selected= el.value===initial); const isSelected= el=> (el.selected= el.value===initial);
// @ts-expect-error 2339: The <select> has only two options with {@link Emoji} // @ts-expect-error 2339: The <select> has only two options with {@link Emoji}
const onChange= on("change", event=> emoji(event.target.value)); const onChange= on("change", event=> emoji.set(event.target.value));
return el().append( return el().append(
el("p", { el("p", {
textContent: S(() => `Hello World ${emoji().repeat(clicks())}`), textContent: S(() => `Hello World ${emoji.get().repeat(clicks.get())}`),
className: "example", className: "example",
ariaLive: "polite", //OR ariaset: { live: "polite" }, ariaLive: "polite", //OR ariaset: { live: "polite" },
dataset: { example: "Example" }, //OR dataExample: "Example", dataset: { example: "Example" }, //OR dataExample: "Example",
}), }),
el("button", el("button",
{ textContent: "Fire", type: "button" }, { textContent: "Fire", type: "button" },
on("click", ()=> clicks(clicks() + 1)), on("click", ()=> clicks.set(clicks.get() + 1)),
on("keyup", ()=> clicks(clicks() - 2)), on("keyup", ()=> clicks.set(clicks.get() - 2)),
), ),
el("select", null, onChange).append( el("select", null, onChange).append(
el(OptionComponent, "🎉", isSelected),//OR { textContent: "🎉" } el(OptionComponent, "🎉", isSelected),//OR { textContent: "🎉" }
@ -86,10 +86,7 @@ To balance these requirements, numerous compromises have been made. To summarize
- [dist/](dist/) (`https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/`…) - [dist/](dist/) (`https://cdn.jsdelivr.net/gh/jaandrle/deka-dom-el/dist/`…)
## Signals ## Signals
- [Signals — whats going on behind the scenes | by Ryan Hoffnan | ITNEXT](https://itnext.io/ - [Signals — whats going on behind the scenes \| by Ryan Hoffnan \| ITNEXT](https://itnext.io/signals-whats-going-on-behind-the-scenes-ec858589ea63)
signals-whats-going-on-behind-the-scenes-ec858589ea63) - [The Evolution of Signals in JavaScript - DEV Community](https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob)
- [The Evolution of Signals in JavaScript - DEV Community](https://dev.to/this-is-learning/the-evolution-of-signals-in- - there is also [tc39/proposal-signals: A proposal to add signals to JavaScript.](https://github.com/tc39/proposal-signals)
javascript-8ob)
- there is also [tc39/proposal-signals: A proposal to add signals to JavaScript.](https://github.com/tc39/proposal-
signals)
- [Observer pattern - Wikipedia](https://en.wikipedia.org/wiki/Observer_pattern) - [Observer pattern - Wikipedia](https://en.wikipedia.org/wiki/Observer_pattern)

1347
dist/dde-with-signals.js vendored

File diff suppressed because it is too large Load Diff

842
dist/dde.js vendored
View File

@ -1,456 +1,664 @@
//deka-dom-el library is available via global namespace `dde` //deka-dom-el library is available via global namespace `dde`
(()=> { (()=> {
// src/signals-common.js
var C = {
isSignal(t) {
return !1;
},
processReactiveAttribute(t, e, r, n) {
return r;
}
};
function Z(t, e = !0) {
return e ? Object.assign(C, t) : (Object.setPrototypeOf(t, C), t);
}
function S(t) {
return C.isPrototypeOf(t) && t !== C ? t : C;
}
// src/helpers.js // src/helpers.js
function m(t) { function isUndef(value) {
return typeof t > "u"; return typeof value === "undefined";
} }
function L(t, e) { function isInstance(obj, cls) {
if (!t || !(t instanceof AbortSignal)) return obj instanceof cls;
return !0; }
if (!t.aborted) function isProtoFrom(obj, cls) {
return t.addEventListener("abort", e), function() { return Object.prototype.isPrototypeOf.call(cls, obj);
t.removeEventListener("abort", e); }
function oAssign(...o) {
return Object.assign(...o);
}
function onAbort(signal, listener) {
if (!signal || !isInstance(signal, AbortSignal))
return true;
if (signal.aborted)
return;
signal.addEventListener("abort", listener);
return function cleanUp() {
signal.removeEventListener("abort", listener);
}; };
} }
function q(t, e) { function observedAttributes(instance, observedAttribute) {
let { observedAttributes: r = [] } = t.constructor; const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
return r.reduce(function(n, o) { return observedAttributes3.reduce(function(out, name) {
return n[G(o)] = e(t, o), n; out[kebabToCamel(name)] = observedAttribute(instance, name);
return out;
}, {}); }, {});
} }
function G(t) { function kebabToCamel(name) {
return t.replace(/-./g, (e) => e[1].toUpperCase()); return name.replace(/-./g, (x) => x[1].toUpperCase());
}
// src/signals-lib/common.js
var signals_global = {
/**
* Checks if a value is a signal
* @param {any} attributes - Value to check
* @returns {boolean} Whether the value is a signal
*/
isSignal(attributes) {
return false;
},
/**
* Processes an attribute that might be reactive
* @param {Element} obj - Element that owns the attribute
* @param {string} key - Attribute name
* @param {any} attr - Attribute value
* @param {Function} set - Function to set the attribute
* @returns {any} Processed attribute value
*/
processReactiveAttribute(obj, key, attr, set) {
return attr;
}
};
function registerReactivity(def, global = true) {
if (global) return oAssign(signals_global, def);
Object.setPrototypeOf(def, signals_global);
return def;
}
function signals(_this) {
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
} }
// src/dom-common.js // src/dom-common.js
var a = { var enviroment = {
setDeleteAttr: V, setDeleteAttr,
ssr: "", ssr: "",
D: globalThis.document, D: globalThis.document,
F: globalThis.DocumentFragment, F: globalThis.DocumentFragment,
H: globalThis.HTMLElement, H: globalThis.HTMLElement,
S: globalThis.SVGElement, S: globalThis.SVGElement,
M: globalThis.MutationObserver, M: globalThis.MutationObserver,
q: (t) => t || Promise.resolve() q: (p) => p || Promise.resolve()
}; };
function V(t, e, r) { function setDeleteAttr(obj, prop, val) {
if (Reflect.set(t, e, r), !!m(r)) { Reflect.set(obj, prop, val);
if (Reflect.deleteProperty(t, e), t instanceof a.H && t.getAttribute(e) === "undefined") if (!isUndef(val)) return;
return t.removeAttribute(e); Reflect.deleteProperty(obj, prop);
if (Reflect.get(t, e) === "undefined") if (isInstance(obj, enviroment.H) && obj.getAttribute(prop) === "undefined")
return Reflect.set(t, e, ""); return obj.removeAttribute(prop);
} if (Reflect.get(obj, prop) === "undefined")
return Reflect.set(obj, prop, "");
} }
var x = "__dde_lifecyclesToEvents", v = "dde:connected", w = "dde:disconnected", y = "dde:attributeChanged"; var keyLTE = "__dde_lifecyclesToEvents";
var evc = "dde:connected";
var evd = "dde:disconnected";
var eva = "dde:attributeChanged";
// src/dom.js // src/dom.js
function dt(t) { function queue(promise) {
return a.q(t); return enviroment.q(promise);
} }
var g = [{ var scopes = [{
get scope() { get scope() {
return a.D.body; return enviroment.D.body;
}, },
host: (t) => t ? t(a.D.body) : a.D.body, host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: !0 prevent: true
}], O = { }];
var scope = {
/**
* Gets the current scope
* @returns {Object} Current scope context
*/
get current() { get current() {
return g[g.length - 1]; return scopes[scopes.length - 1];
}, },
/**
* Gets the host element of the current scope
* @returns {Function} Host accessor function
*/
get host() { get host() {
return this.current.host; return this.current.host;
}, },
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context
*/
preventDefault() { preventDefault() {
let { current: t } = this; const { current } = this;
return t.prevent = !0, t; current.prevent = true;
return current;
}, },
/**
* Gets a copy of the current scope stack
* @returns {Array} Copy of scope stack
*/
get state() { get state() {
return [...g]; return [...scopes];
}, },
push(t = {}) { /**
return g.push(Object.assign({}, this.current, { prevent: !1 }, t)); * Pushes a new scope to the stack
* @param {Object} [s={}] - Scope object to push
* @returns {number} New length of the scope stack
*/
push(s = {}) {
return scopes.push(oAssign({}, this.current, { prevent: false }, s));
}, },
/**
* Pushes the root scope to the stack
* @returns {number} New length of the scope stack
*/
pushRoot() { pushRoot() {
return g.push(g[0]); return scopes.push(scopes[0]);
}, },
/**
* Pops the current scope from the stack
* @returns {Object|undefined} Popped scope or undefined if only one scope remains
*/
pop() { pop() {
if (g.length !== 1) if (scopes.length === 1) return;
return g.pop(); return scopes.pop();
} }
}; };
function k(...t) { function append(...els) {
return this.appendOriginal(...t), this; this.appendOriginal(...els);
return this;
} }
function J(t) { function chainableAppend(el) {
return t.append === k || (t.appendOriginal = t.append, t.append = k), t; if (el.append === append) return el;
el.appendOriginal = el.append;
el.append = append;
return el;
} }
var T; var namespace;
function P(t, e, ...r) { function createElement(tag, attributes, ...addons) {
let n = S(this), o = 0, c, d; const s = signals(this);
switch ((Object(e) !== e || n.isSignal(e)) && (e = { textContent: e }), !0) { let scoped = 0;
case typeof t == "function": { let el, el_host;
o = 1; if (Object(attributes) !== attributes || s.isSignal(attributes))
let f = (...l) => l.length ? (o === 1 ? r.unshift(...l) : l.forEach((E) => E(d)), void 0) : d; attributes = { textContent: attributes };
O.push({ scope: t, host: f }), c = t(e || void 0); switch (true) {
let p = c instanceof a.F; case typeof tag === "function": {
if (c.nodeName === "#comment") break; scoped = 1;
let b = P.mark({ const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host });
el = tag(attributes || void 0);
const is_fragment = isInstance(el, enviroment.F);
if (el.nodeName === "#comment") break;
const el_mark = createElement.mark({
type: "component", type: "component",
name: t.name, name: tag.name,
host: p ? "this" : "parentElement" host: is_fragment ? "this" : "parentElement"
}); });
c.prepend(b), p && (d = b); el.prepend(el_mark);
if (is_fragment) el_host = el_mark;
break; break;
} }
case t === "#text": case tag === "#text":
c = R.call(this, a.D.createTextNode(""), e); el = assign.call(this, enviroment.D.createTextNode(""), attributes);
break; break;
case (t === "<>" || !t): case (tag === "<>" || !tag):
c = R.call(this, a.D.createDocumentFragment(), e); el = assign.call(this, enviroment.D.createDocumentFragment(), attributes);
break; break;
case !!T: case Boolean(namespace):
c = R.call(this, a.D.createElementNS(T, t), e); el = assign.call(this, enviroment.D.createElementNS(namespace, tag), attributes);
break; break;
case !c: case !el:
c = R.call(this, a.D.createElement(t), e); el = assign.call(this, enviroment.D.createElement(tag), attributes);
} }
return J(c), d || (d = c), r.forEach((f) => f(d)), o && O.pop(), o = 2, c; chainableAppend(el);
if (!el_host) el_host = el;
addons.forEach((c) => c(el_host));
if (scoped) scope.pop();
scoped = 2;
return el;
} }
P.mark = function(t, e = !1) { createElement.mark = function(attrs, is_open = false) {
t = Object.entries(t).map(([o, c]) => o + `="${c}"`).join(" "); attrs = Object.entries(attrs).map(([n, v]) => n + `="${v}"`).join(" ");
let r = e ? "" : "/", n = a.D.createComment(`<dde:mark ${t}${a.ssr}${r}>`); const end = is_open ? "" : "/";
return e && (n.end = a.D.createComment("</dde:mark>")), n; const out = enviroment.D.createComment(`<dde:mark ${attrs}${enviroment.ssr}${end}>`);
if (is_open) out.end = enviroment.D.createComment("</dde:mark>");
return out;
}; };
function pt(t) { function createElementNS(ns) {
let e = this; const _this = this;
return function(...n) { return function createElementNSCurried(...rest) {
T = t; namespace = ns;
let o = P.call(e, ...n); const el = createElement.call(_this, ...rest);
return T = void 0, o; namespace = void 0;
return el;
}; };
} }
function lt(t, e = t) { function simulateSlots(element, root = element) {
let r = "\xB9\u2070", n = "\u2713", o = Object.fromEntries( const mark_e = "\xB9\u2070", mark_s = "\u2713";
Array.from(e.querySelectorAll("slot")).filter((c) => !c.name.endsWith(r)).map((c) => [c.name += r, c]) const slots = Object.fromEntries(
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
); );
if (t.append = new Proxy(t.append, { element.append = new Proxy(element.append, {
apply(c, d, f) { apply(orig, _, els) {
if (f[0] === e) return c.apply(t, f); if (els[0] === root) return orig.apply(element, els);
for (let p of f) { for (const el of els) {
let b = (p.slot || "") + r; const name = (el.slot || "") + mark_e;
try { try {
Q(p, "remove", "slot"); elementAttribute(el, "remove", "slot");
} catch { } catch (_error) {
} }
let l = o[b]; const slot = slots[name];
if (!l) return; if (!slot) return;
l.name.startsWith(n) || (l.childNodes.forEach((E) => E.remove()), l.name = n + b), l.append(p); if (!slot.name.startsWith(mark_s)) {
slot.childNodes.forEach((c) => c.remove());
slot.name = mark_s + name;
} }
return t.append = c, t; slot.append(el);
} }
}), t !== e) { element.append = orig;
let c = Array.from(t.childNodes); return element;
t.append(...c);
} }
return e; });
if (element !== root) {
const els = Array.from(element.childNodes);
element.append(...els);
}
return root;
} }
var N = /* @__PURE__ */ new WeakMap(), { setDeleteAttr: $ } = a; var assign_context = /* @__PURE__ */ new WeakMap();
function R(t, ...e) { var { setDeleteAttr: setDeleteAttr2 } = enviroment;
if (!e.length) return t; function assign(element, ...attributes) {
N.set(t, H(t, this)); if (!attributes.length) return element;
for (let [r, n] of Object.entries(Object.assign({}, ...e))) assign_context.set(element, assignContext(element, this));
U.call(this, t, r, n); for (const [key, value] of Object.entries(oAssign({}, ...attributes)))
return N.delete(t), t; assignAttribute.call(this, element, key, value);
assign_context.delete(element);
return element;
} }
function U(t, e, r) { function assignAttribute(element, key, value) {
let { setRemoveAttr: n, s: o } = H(t, this), c = this; const { setRemoveAttr, s } = assignContext(element, this);
r = o.processReactiveAttribute( const _this = this;
t, value = s.processReactiveAttribute(
e, element,
r, key,
(f, p) => U.call(c, t, f, p) value,
(key2, value2) => assignAttribute.call(_this, element, key2, value2)
); );
let [d] = e; const [k] = key;
if (d === "=") return n(e.slice(1), r); if ("=" === k) return setRemoveAttr(key.slice(1), value);
if (d === ".") return F(t, e.slice(1), r); if ("." === k) return setDelete(element, key.slice(1), value);
if (/(aria|data)([A-Z])/.test(e)) if (/(aria|data)([A-Z])/.test(key)) {
return e = e.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(), n(e, r); key = key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
switch (e === "className" && (e = "class"), e) { return setRemoveAttr(key, value);
}
if ("className" === key) key = "class";
switch (key) {
case "xlink:href": case "xlink:href":
return n(e, r, "http://www.w3.org/1999/xlink"); return setRemoveAttr(key, value, "http://www.w3.org/1999/xlink");
case "textContent": case "textContent":
return $(t, e, r); return setDeleteAttr2(element, key, value);
case "style": case "style":
if (typeof r != "object") break; if (typeof value !== "object") break;
/* falls through */ /* falls through */
case "dataset": case "dataset":
return M(o, e, t, r, F.bind(null, t[e])); return forEachEntries(s, key, element, value, setDelete.bind(null, element[key]));
case "ariaset": case "ariaset":
return M(o, e, t, r, (f, p) => n("aria-" + f, p)); return forEachEntries(s, key, element, value, (key2, val) => setRemoveAttr("aria-" + key2, val));
case "classList": case "classList":
return K.call(c, t, r); return classListDeclarative.call(_this, element, value);
} }
return X(t, e) ? $(t, e, r) : n(e, r); return isPropSetter(element, key) ? setDeleteAttr2(element, key, value) : setRemoveAttr(key, value);
} }
function H(t, e) { function assignContext(element, _this) {
if (N.has(t)) return N.get(t); if (assign_context.has(element)) return assign_context.get(element);
let n = (t instanceof a.S ? tt : Y).bind(null, t, "Attribute"), o = S(e); const is_svg = isInstance(element, enviroment.S);
return { setRemoveAttr: n, s: o }; const setRemoveAttr = (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
const s = signals(_this);
return { setRemoveAttr, s };
} }
function K(t, e) { function classListDeclarative(element, toggle) {
let r = S(this); const s = signals(this);
return M( forEachEntries(
r, s,
"classList", "classList",
t, element,
e, toggle,
(n, o) => t.classList.toggle(n, o === -1 ? void 0 : !!o) (class_name, val) => element.classList.toggle(class_name, val === -1 ? void 0 : Boolean(val))
), t; );
return element;
} }
function Q(t, e, r, n) { function elementAttribute(element, op, key, value) {
return t instanceof a.H ? t[e + "Attribute"](r, n) : t[e + "AttributeNS"](null, r, n); if (isInstance(element, enviroment.H))
return element[op + "Attribute"](key, value);
return element[op + "AttributeNS"](null, key, value);
} }
function X(t, e) { function isPropSetter(el, key) {
if (!(e in t)) return !1; if (!(key in el)) return false;
let r = z(t, e); const des = getPropDescriptor(el, key);
return !m(r.set); return !isUndef(des.set);
} }
function z(t, e) { function getPropDescriptor(p, key) {
if (t = Object.getPrototypeOf(t), !t) return {}; p = Object.getPrototypeOf(p);
let r = Object.getOwnPropertyDescriptor(t, e); if (!p) return {};
return r || z(t, e); const des = Object.getOwnPropertyDescriptor(p, key);
if (!des) return getPropDescriptor(p, key);
return des;
} }
function M(t, e, r, n, o) { function forEachEntries(s, target, element, obj, cb) {
let c = String; const S = String;
if (!(typeof n != "object" || n === null)) if (typeof obj !== "object" || obj === null) return;
return Object.entries(n).forEach(function([f, p]) { return Object.entries(obj).forEach(function process([key, val]) {
f && (f = new c(f), f.target = e, p = t.processReactiveAttribute(r, f, p, o), o(f, p)); if (!key) return;
key = new S(key);
key.target = target;
val = s.processReactiveAttribute(element, key, val, cb);
cb(key, val);
}); });
} }
function Y(t, e, r, n) { function setRemove(obj, prop, key, val) {
return t[(m(n) ? "remove" : "set") + e](r, n); return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
} }
function tt(t, e, r, n, o = null) { function setRemoveNS(obj, prop, key, val, ns = null) {
return t[(m(n) ? "remove" : "set") + e + "NS"](o, r, n); return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
} }
function F(t, e, r) { function setDelete(obj, key, val) {
if (Reflect.set(t, e, r), !!m(r)) Reflect.set(obj, key, val);
return Reflect.deleteProperty(t, e); if (!isUndef(val)) return;
return Reflect.deleteProperty(obj, key);
} }
// src/events-observer.js // src/events-observer.js
var _ = a.M ? et() : new Proxy({}, { var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() { get() {
return () => { return () => {
}; };
} }
}); });
function et() { function connectionsChangesObserverConstructor() {
let t = /* @__PURE__ */ new Map(), e = !1, r = (s) => function(u) { const store = /* @__PURE__ */ new Map();
for (let i of u) let is_observing = false;
if (i.type === "childList") { const observerListener = (stop2) => function(mutations) {
if (l(i.addedNodes, !0)) { for (const mutation of mutations) {
s(); if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue; continue;
} }
E(i.removedNodes, !0) && s(); if (observerRemoved(mutation.removedNodes, true))
} stop2();
}, n = new a.M(r(f));
return {
observe(s) {
let u = new a.M(r(() => {
}));
return u.observe(s, { childList: !0, subtree: !0 }), () => u.disconnect();
},
onConnected(s, u) {
d();
let i = c(s);
i.connected.has(u) || (i.connected.add(u), i.length_c += 1);
},
offConnected(s, u) {
if (!t.has(s)) return;
let i = t.get(s);
i.connected.has(u) && (i.connected.delete(u), i.length_c -= 1, o(s, i));
},
onDisconnected(s, u) {
d();
let i = c(s);
i.disconnected.has(u) || (i.disconnected.add(u), i.length_d += 1);
},
offDisconnected(s, u) {
if (!t.has(s)) return;
let i = t.get(s);
i.disconnected.has(u) && (i.disconnected.delete(u), i.length_d -= 1, o(s, i));
} }
}; };
function o(s, u) { const observer = new enviroment.M(observerListener(stop));
u.length_c || u.length_d || (t.delete(s), f()); return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.disconnected.has(listener)) return;
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
} }
function c(s) { };
if (t.has(s)) return t.get(s); function cleanWhenOff(element, ls) {
let u = { if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(), connected: /* @__PURE__ */ new WeakSet(),
length_c: 0, length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(), disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0 length_d: 0
}; };
return t.set(s, u), u; store.set(element, out);
return out;
} }
function d() { function start() {
e || (e = !0, n.observe(a.D.body, { childList: !0, subtree: !0 })); if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
} }
function f() { function stop() {
!e || t.size || (e = !1, n.disconnect()); if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
} }
function p() { function requestIdle() {
return new Promise(function(s) { return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(s); (requestIdleCallback || requestAnimationFrame)(resolve);
}); });
} }
async function b(s) { async function collectChildren(element) {
t.size > 30 && await p(); if (store.size > 30)
let u = []; await requestIdle();
if (!(s instanceof Node)) return u; const out = [];
for (let i of t.keys()) if (!isInstance(element, Node)) return out;
i === s || !(i instanceof Node) || s.contains(i) && u.push(i); for (const el of store.keys()) {
return u; if (el === element || !isInstance(el, Node)) continue;
if (element.contains(el))
out.push(el);
} }
function l(s, u) { return out;
let i = !1;
for (let h of s) {
if (u && b(h).then(l), !t.has(h)) continue;
let A = t.get(h);
A.length_c && (h.dispatchEvent(new Event(v)), A.connected = /* @__PURE__ */ new WeakSet(), A.length_c = 0, A.length_d || t.delete(h), i = !0);
} }
return i; function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
} }
function E(s, u) { return out;
let i = !1;
for (let h of s)
u && b(h).then(E), !(!t.has(h) || !t.get(h).length_d) && ((globalThis.queueMicrotask || setTimeout)(I(h)), i = !0);
return i;
} }
function I(s) { function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => { return () => {
s.isConnected || (s.dispatchEvent(new Event(w)), t.delete(s)); if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
}; };
} }
} }
// src/customElement.js // src/customElement.js
function wt(t, e, r = rt) { function customElementRender(target, render, props = observedAttributes2) {
let n = t.host || t; const custom_element = target.host || target;
O.push({ scope.push({
scope: n, scope: custom_element,
host: (...d) => d.length ? d.forEach((f) => f(n)) : n host: (...c) => c.length ? c.forEach((c2) => c2(custom_element)) : custom_element
}), typeof r == "function" && (r = r.call(n, n)); });
let o = n[x]; if (typeof props === "function") props = props.call(custom_element, custom_element);
o || nt(n); const is_lte = custom_element[keyLTE];
let c = e.call(n, r); if (!is_lte) lifecyclesToEvents(custom_element);
return o || n.dispatchEvent(new Event(v)), t.nodeType === 11 && typeof t.mode == "string" && n.addEventListener(w, _.observe(t), { once: !0 }), O.pop(), t.append(c); const out = render.call(custom_element, props);
if (!is_lte) custom_element.dispatchEvent(new Event(evc));
if (target.nodeType === 11 && typeof target.mode === "string")
custom_element.addEventListener(evd, c_ch_o.observe(target), { once: true });
scope.pop();
return target.append(out);
} }
function nt(t) { function lifecyclesToEvents(class_declaration) {
return j(t.prototype, "connectedCallback", function(e, r, n) { wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail) {
e.apply(r, n), r.dispatchEvent(new Event(v)); target.apply(thisArg, detail);
}), j(t.prototype, "disconnectedCallback", function(e, r, n) { thisArg.dispatchEvent(new Event(evc));
e.apply(r, n), (globalThis.queueMicrotask || setTimeout)( });
() => !r.isConnected && r.dispatchEvent(new Event(w)) wrapMethod(class_declaration.prototype, "disconnectedCallback", function(target, thisArg, detail) {
target.apply(thisArg, detail);
(globalThis.queueMicrotask || setTimeout)(
() => !thisArg.isConnected && thisArg.dispatchEvent(new Event(evd))
); );
}), j(t.prototype, "attributeChangedCallback", function(e, r, n) { });
let [o, , c] = n; wrapMethod(class_declaration.prototype, "attributeChangedCallback", function(target, thisArg, detail) {
r.dispatchEvent(new CustomEvent(y, { const [attribute, , value] = detail;
detail: [o, c] thisArg.dispatchEvent(new CustomEvent(eva, {
})), e.apply(r, n); detail: [attribute, value]
}), t.prototype[x] = !0, t; }));
target.apply(thisArg, detail);
});
class_declaration.prototype[keyLTE] = true;
return class_declaration;
} }
function j(t, e, r) { function wrapMethod(obj, method, apply) {
t[e] = new Proxy(t[e] || (() => { obj[method] = new Proxy(obj[method] || (() => {
}), { apply: r }); }), { apply });
} }
function rt(t) { function observedAttributes2(instance) {
return q(t, (e, r) => e.getAttribute(r)); return observedAttributes(instance, (i, n) => i.getAttribute(n));
} }
// src/events.js // src/events.js
function yt(t, e, r) { function dispatchEvent(name, options, host) {
return e || (e = {}), function(o, ...c) { if (!options) options = {};
r && (c.unshift(o), o = typeof r == "function" ? r() : r); return function dispatch(element, ...d) {
let d = c.length ? new CustomEvent(t, Object.assign({ detail: c[0] }, e)) : new Event(t, e); if (host) {
return o.dispatchEvent(d); d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
}; };
} }
function D(t, e, r) { function on(event, listener, options) {
return function(o) { return function registerElement(element) {
return o.addEventListener(t, e, r), o; element.addEventListener(event, listener, options);
return element;
}; };
} }
var B = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 }); var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
D.connected = function(t, e) { on.connected = function(listener, options) {
return e = B(e), function(n) { options = lifeOptions(options);
return n.addEventListener(v, t, e), n[x] ? n : n.isConnected ? (n.dispatchEvent(new Event(v)), n) : (L(e.signal, () => _.offConnected(n, t)) && _.onConnected(n, t), n); return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
}; };
}; };
D.disconnected = function(t, e) { on.disconnected = function(listener, options) {
return e = B(e), function(n) { options = lifeOptions(options);
return n.addEventListener(w, t, e), n[x] || L(e.signal, () => _.offDisconnected(n, t)) && _.onDisconnected(n, t), n; return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
}; };
}; };
var W = /* @__PURE__ */ new WeakMap(); var store_abort = /* @__PURE__ */ new WeakMap();
D.disconnectedAsAbort = function(t) { on.disconnectedAsAbort = function(host) {
if (W.has(t)) return W.get(t); if (store_abort.has(host)) return store_abort.get(host);
let e = new AbortController(); const a = new AbortController();
return W.set(t, e), t(D.disconnected(() => e.abort())), e; store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a;
}; };
var ot = /* @__PURE__ */ new WeakSet(); var els_attribute_store = /* @__PURE__ */ new WeakSet();
D.attributeChanged = function(t, e) { on.attributeChanged = function(listener, options) {
return typeof e != "object" && (e = {}), function(n) { if (typeof options !== "object")
if (n.addEventListener(y, t, e), n[x] || ot.has(n) || !a.M) return n; options = {};
let o = new a.M(function(d) { return function registerElement(element) {
for (let { attributeName: f, target: p } of d) element.addEventListener(eva, listener, options);
p.dispatchEvent( if (element[keyLTE] || els_attribute_store.has(element))
new CustomEvent(y, { detail: [f, p.getAttribute(f)] }) return element;
if (!enviroment.M) return element;
const observer = new enviroment.M(function(mutations) {
for (const { attributeName, target } of mutations)
target.dispatchEvent(
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
); );
}); });
return L(e.signal, () => o.disconnect()) && o.observe(n, { attributes: !0 }), n; const c = onAbort(options.signal, () => observer.disconnect());
if (c) observer.observe(element, { attributes: true });
return element;
}; };
}; };
globalThis.dde= { globalThis.dde= {
assign: R, assign,
assignAttribute: U, assignAttribute,
chainableAppend: J, chainableAppend,
classListDeclarative: K, classListDeclarative,
createElement: P, createElement,
createElementNS: pt, createElementNS,
customElementRender: wt, customElementRender,
customElementWithDDE: nt, customElementWithDDE: lifecyclesToEvents,
dispatchEvent: yt, dispatchEvent,
el: P, el: createElement,
elNS: pt, elNS: createElementNS,
elementAttribute: Q, elementAttribute,
lifecyclesToEvents: nt, lifecyclesToEvents,
observedAttributes: rt, observedAttributes: observedAttributes2,
on: D, on,
queue: dt, queue,
registerReactivity: Z, registerReactivity,
scope: O, scope,
simulateSlots: lt simulateSlots
}; };
})(); })();

View File

@ -52,9 +52,12 @@ type IsReadonly<T, K extends keyof T> =
* @private * @private
*/ */
type ElementAttributes<T extends SupportedElement>= Partial<{ type ElementAttributes<T extends SupportedElement>= Partial<{
[K in keyof _fromElsInterfaces<T>]: IsReadonly<_fromElsInterfaces<T>, K> extends false [K in keyof _fromElsInterfaces<T>]:
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=> ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]> ? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
: ddeStringable : ddeStringable)
} & AttrsModified> & Record<string, any>; } & AttrsModified> & Record<string, any>;
export function classListDeclarative<El extends SupportedElement>( export function classListDeclarative<El extends SupportedElement>(
element: El, element: El,
@ -515,7 +518,14 @@ interface ddeSVGTSpanElement extends SVGTSpanElement{ append: ddeAppend<ddeSVGTS
interface ddeSVGUseElement extends SVGUseElement{ append: ddeAppend<ddeSVGUseElement>; } interface ddeSVGUseElement extends SVGUseElement{ append: ddeAppend<ddeSVGUseElement>; }
interface ddeSVGViewElement extends SVGViewElement{ append: ddeAppend<ddeSVGViewElement>; } interface ddeSVGViewElement extends SVGViewElement{ append: ddeAppend<ddeSVGViewElement>; }
// editorconfig-checker-enable // editorconfig-checker-enable
export type Signal<V, A>= (set?: V)=> V & A; export interface Signal<V, A> {
/** The current value of the signal */
get(): V;
/** Set new value of the signal */
set(value: V): V;
toJSON(): V;
valueOf(): V;
}
type Action<V>= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof signal._ | void; type Action<V>= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof signal._ | void;
//type SymbolSignal= Symbol; //type SymbolSignal= Symbol;
type SymbolOnclear= symbol; type SymbolOnclear= symbol;
@ -544,7 +554,7 @@ interface signal{
* ```js * ```js
* const name= S("Jan"); * const name= S("Jan");
* const surname= S("Andrle"); * const surname= S("Andrle");
* const fullname= S(()=> name()+" "+surname()); * const fullname= S(()=> name.get()+" "+surname.get());
* ``` * ```
* @param value Initial signal value. Or function computing value from other signals. * @param value Initial signal value. Or function computing value from other signals.
* @param actions Use to define actions on the signal. Such as add item to the array. * @param actions Use to define actions on the signal. Such as add item to the array.

1347
dist/esm-with-signals.js vendored

File diff suppressed because it is too large Load Diff

7
dist/esm.d.ts vendored
View File

@ -52,9 +52,12 @@ type IsReadonly<T, K extends keyof T> =
* @private * @private
*/ */
type ElementAttributes<T extends SupportedElement>= Partial<{ type ElementAttributes<T extends SupportedElement>= Partial<{
[K in keyof _fromElsInterfaces<T>]: IsReadonly<_fromElsInterfaces<T>, K> extends false [K in keyof _fromElsInterfaces<T>]:
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=> ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]> ? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
: ddeStringable : ddeStringable)
} & AttrsModified> & Record<string, any>; } & AttrsModified> & Record<string, any>;
export function classListDeclarative<El extends SupportedElement>( export function classListDeclarative<El extends SupportedElement>(
element: El, element: El,

842
dist/esm.js vendored
View File

@ -1,451 +1,659 @@
// src/signals-common.js
var C = {
isSignal(t) {
return !1;
},
processReactiveAttribute(t, e, r, n) {
return r;
}
};
function Z(t, e = !0) {
return e ? Object.assign(C, t) : (Object.setPrototypeOf(t, C), t);
}
function S(t) {
return C.isPrototypeOf(t) && t !== C ? t : C;
}
// src/helpers.js // src/helpers.js
function m(t) { function isUndef(value) {
return typeof t > "u"; return typeof value === "undefined";
} }
function L(t, e) { function isInstance(obj, cls) {
if (!t || !(t instanceof AbortSignal)) return obj instanceof cls;
return !0; }
if (!t.aborted) function isProtoFrom(obj, cls) {
return t.addEventListener("abort", e), function() { return Object.prototype.isPrototypeOf.call(cls, obj);
t.removeEventListener("abort", e); }
function oAssign(...o) {
return Object.assign(...o);
}
function onAbort(signal, listener) {
if (!signal || !isInstance(signal, AbortSignal))
return true;
if (signal.aborted)
return;
signal.addEventListener("abort", listener);
return function cleanUp() {
signal.removeEventListener("abort", listener);
}; };
} }
function q(t, e) { function observedAttributes(instance, observedAttribute) {
let { observedAttributes: r = [] } = t.constructor; const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
return r.reduce(function(n, o) { return observedAttributes3.reduce(function(out, name) {
return n[G(o)] = e(t, o), n; out[kebabToCamel(name)] = observedAttribute(instance, name);
return out;
}, {}); }, {});
} }
function G(t) { function kebabToCamel(name) {
return t.replace(/-./g, (e) => e[1].toUpperCase()); return name.replace(/-./g, (x) => x[1].toUpperCase());
}
// src/signals-lib/common.js
var signals_global = {
/**
* Checks if a value is a signal
* @param {any} attributes - Value to check
* @returns {boolean} Whether the value is a signal
*/
isSignal(attributes) {
return false;
},
/**
* Processes an attribute that might be reactive
* @param {Element} obj - Element that owns the attribute
* @param {string} key - Attribute name
* @param {any} attr - Attribute value
* @param {Function} set - Function to set the attribute
* @returns {any} Processed attribute value
*/
processReactiveAttribute(obj, key, attr, set) {
return attr;
}
};
function registerReactivity(def, global = true) {
if (global) return oAssign(signals_global, def);
Object.setPrototypeOf(def, signals_global);
return def;
}
function signals(_this) {
return isProtoFrom(_this, signals_global) && _this !== signals_global ? _this : signals_global;
} }
// src/dom-common.js // src/dom-common.js
var a = { var enviroment = {
setDeleteAttr: V, setDeleteAttr,
ssr: "", ssr: "",
D: globalThis.document, D: globalThis.document,
F: globalThis.DocumentFragment, F: globalThis.DocumentFragment,
H: globalThis.HTMLElement, H: globalThis.HTMLElement,
S: globalThis.SVGElement, S: globalThis.SVGElement,
M: globalThis.MutationObserver, M: globalThis.MutationObserver,
q: (t) => t || Promise.resolve() q: (p) => p || Promise.resolve()
}; };
function V(t, e, r) { function setDeleteAttr(obj, prop, val) {
if (Reflect.set(t, e, r), !!m(r)) { Reflect.set(obj, prop, val);
if (Reflect.deleteProperty(t, e), t instanceof a.H && t.getAttribute(e) === "undefined") if (!isUndef(val)) return;
return t.removeAttribute(e); Reflect.deleteProperty(obj, prop);
if (Reflect.get(t, e) === "undefined") if (isInstance(obj, enviroment.H) && obj.getAttribute(prop) === "undefined")
return Reflect.set(t, e, ""); return obj.removeAttribute(prop);
} if (Reflect.get(obj, prop) === "undefined")
return Reflect.set(obj, prop, "");
} }
var x = "__dde_lifecyclesToEvents", v = "dde:connected", w = "dde:disconnected", y = "dde:attributeChanged"; var keyLTE = "__dde_lifecyclesToEvents";
var evc = "dde:connected";
var evd = "dde:disconnected";
var eva = "dde:attributeChanged";
// src/dom.js // src/dom.js
function dt(t) { function queue(promise) {
return a.q(t); return enviroment.q(promise);
} }
var g = [{ var scopes = [{
get scope() { get scope() {
return a.D.body; return enviroment.D.body;
}, },
host: (t) => t ? t(a.D.body) : a.D.body, host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
prevent: !0 prevent: true
}], O = { }];
var scope = {
/**
* Gets the current scope
* @returns {Object} Current scope context
*/
get current() { get current() {
return g[g.length - 1]; return scopes[scopes.length - 1];
}, },
/**
* Gets the host element of the current scope
* @returns {Function} Host accessor function
*/
get host() { get host() {
return this.current.host; return this.current.host;
}, },
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context
*/
preventDefault() { preventDefault() {
let { current: t } = this; const { current } = this;
return t.prevent = !0, t; current.prevent = true;
return current;
}, },
/**
* Gets a copy of the current scope stack
* @returns {Array} Copy of scope stack
*/
get state() { get state() {
return [...g]; return [...scopes];
}, },
push(t = {}) { /**
return g.push(Object.assign({}, this.current, { prevent: !1 }, t)); * Pushes a new scope to the stack
* @param {Object} [s={}] - Scope object to push
* @returns {number} New length of the scope stack
*/
push(s = {}) {
return scopes.push(oAssign({}, this.current, { prevent: false }, s));
}, },
/**
* Pushes the root scope to the stack
* @returns {number} New length of the scope stack
*/
pushRoot() { pushRoot() {
return g.push(g[0]); return scopes.push(scopes[0]);
}, },
/**
* Pops the current scope from the stack
* @returns {Object|undefined} Popped scope or undefined if only one scope remains
*/
pop() { pop() {
if (g.length !== 1) if (scopes.length === 1) return;
return g.pop(); return scopes.pop();
} }
}; };
function k(...t) { function append(...els) {
return this.appendOriginal(...t), this; this.appendOriginal(...els);
return this;
} }
function J(t) { function chainableAppend(el) {
return t.append === k || (t.appendOriginal = t.append, t.append = k), t; if (el.append === append) return el;
el.appendOriginal = el.append;
el.append = append;
return el;
} }
var T; var namespace;
function P(t, e, ...r) { function createElement(tag, attributes, ...addons) {
let n = S(this), o = 0, c, d; const s = signals(this);
switch ((Object(e) !== e || n.isSignal(e)) && (e = { textContent: e }), !0) { let scoped = 0;
case typeof t == "function": { let el, el_host;
o = 1; if (Object(attributes) !== attributes || s.isSignal(attributes))
let f = (...l) => l.length ? (o === 1 ? r.unshift(...l) : l.forEach((E) => E(d)), void 0) : d; attributes = { textContent: attributes };
O.push({ scope: t, host: f }), c = t(e || void 0); switch (true) {
let p = c instanceof a.F; case typeof tag === "function": {
if (c.nodeName === "#comment") break; scoped = 1;
let b = P.mark({ const host = (...c) => !c.length ? el_host : (scoped === 1 ? addons.unshift(...c) : c.forEach((c2) => c2(el_host)), void 0);
scope.push({ scope: tag, host });
el = tag(attributes || void 0);
const is_fragment = isInstance(el, enviroment.F);
if (el.nodeName === "#comment") break;
const el_mark = createElement.mark({
type: "component", type: "component",
name: t.name, name: tag.name,
host: p ? "this" : "parentElement" host: is_fragment ? "this" : "parentElement"
}); });
c.prepend(b), p && (d = b); el.prepend(el_mark);
if (is_fragment) el_host = el_mark;
break; break;
} }
case t === "#text": case tag === "#text":
c = R.call(this, a.D.createTextNode(""), e); el = assign.call(this, enviroment.D.createTextNode(""), attributes);
break; break;
case (t === "<>" || !t): case (tag === "<>" || !tag):
c = R.call(this, a.D.createDocumentFragment(), e); el = assign.call(this, enviroment.D.createDocumentFragment(), attributes);
break; break;
case !!T: case Boolean(namespace):
c = R.call(this, a.D.createElementNS(T, t), e); el = assign.call(this, enviroment.D.createElementNS(namespace, tag), attributes);
break; break;
case !c: case !el:
c = R.call(this, a.D.createElement(t), e); el = assign.call(this, enviroment.D.createElement(tag), attributes);
} }
return J(c), d || (d = c), r.forEach((f) => f(d)), o && O.pop(), o = 2, c; chainableAppend(el);
if (!el_host) el_host = el;
addons.forEach((c) => c(el_host));
if (scoped) scope.pop();
scoped = 2;
return el;
} }
P.mark = function(t, e = !1) { createElement.mark = function(attrs, is_open = false) {
t = Object.entries(t).map(([o, c]) => o + `="${c}"`).join(" "); attrs = Object.entries(attrs).map(([n, v]) => n + `="${v}"`).join(" ");
let r = e ? "" : "/", n = a.D.createComment(`<dde:mark ${t}${a.ssr}${r}>`); const end = is_open ? "" : "/";
return e && (n.end = a.D.createComment("</dde:mark>")), n; const out = enviroment.D.createComment(`<dde:mark ${attrs}${enviroment.ssr}${end}>`);
if (is_open) out.end = enviroment.D.createComment("</dde:mark>");
return out;
}; };
function pt(t) { function createElementNS(ns) {
let e = this; const _this = this;
return function(...n) { return function createElementNSCurried(...rest) {
T = t; namespace = ns;
let o = P.call(e, ...n); const el = createElement.call(_this, ...rest);
return T = void 0, o; namespace = void 0;
return el;
}; };
} }
function lt(t, e = t) { function simulateSlots(element, root = element) {
let r = "\xB9\u2070", n = "\u2713", o = Object.fromEntries( const mark_e = "\xB9\u2070", mark_s = "\u2713";
Array.from(e.querySelectorAll("slot")).filter((c) => !c.name.endsWith(r)).map((c) => [c.name += r, c]) const slots = Object.fromEntries(
Array.from(root.querySelectorAll("slot")).filter((s) => !s.name.endsWith(mark_e)).map((s) => [s.name += mark_e, s])
); );
if (t.append = new Proxy(t.append, { element.append = new Proxy(element.append, {
apply(c, d, f) { apply(orig, _, els) {
if (f[0] === e) return c.apply(t, f); if (els[0] === root) return orig.apply(element, els);
for (let p of f) { for (const el of els) {
let b = (p.slot || "") + r; const name = (el.slot || "") + mark_e;
try { try {
Q(p, "remove", "slot"); elementAttribute(el, "remove", "slot");
} catch { } catch (_error) {
} }
let l = o[b]; const slot = slots[name];
if (!l) return; if (!slot) return;
l.name.startsWith(n) || (l.childNodes.forEach((E) => E.remove()), l.name = n + b), l.append(p); if (!slot.name.startsWith(mark_s)) {
slot.childNodes.forEach((c) => c.remove());
slot.name = mark_s + name;
} }
return t.append = c, t; slot.append(el);
} }
}), t !== e) { element.append = orig;
let c = Array.from(t.childNodes); return element;
t.append(...c);
} }
return e; });
if (element !== root) {
const els = Array.from(element.childNodes);
element.append(...els);
}
return root;
} }
var N = /* @__PURE__ */ new WeakMap(), { setDeleteAttr: $ } = a; var assign_context = /* @__PURE__ */ new WeakMap();
function R(t, ...e) { var { setDeleteAttr: setDeleteAttr2 } = enviroment;
if (!e.length) return t; function assign(element, ...attributes) {
N.set(t, H(t, this)); if (!attributes.length) return element;
for (let [r, n] of Object.entries(Object.assign({}, ...e))) assign_context.set(element, assignContext(element, this));
U.call(this, t, r, n); for (const [key, value] of Object.entries(oAssign({}, ...attributes)))
return N.delete(t), t; assignAttribute.call(this, element, key, value);
assign_context.delete(element);
return element;
} }
function U(t, e, r) { function assignAttribute(element, key, value) {
let { setRemoveAttr: n, s: o } = H(t, this), c = this; const { setRemoveAttr, s } = assignContext(element, this);
r = o.processReactiveAttribute( const _this = this;
t, value = s.processReactiveAttribute(
e, element,
r, key,
(f, p) => U.call(c, t, f, p) value,
(key2, value2) => assignAttribute.call(_this, element, key2, value2)
); );
let [d] = e; const [k] = key;
if (d === "=") return n(e.slice(1), r); if ("=" === k) return setRemoveAttr(key.slice(1), value);
if (d === ".") return F(t, e.slice(1), r); if ("." === k) return setDelete(element, key.slice(1), value);
if (/(aria|data)([A-Z])/.test(e)) if (/(aria|data)([A-Z])/.test(key)) {
return e = e.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(), n(e, r); key = key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
switch (e === "className" && (e = "class"), e) { return setRemoveAttr(key, value);
}
if ("className" === key) key = "class";
switch (key) {
case "xlink:href": case "xlink:href":
return n(e, r, "http://www.w3.org/1999/xlink"); return setRemoveAttr(key, value, "http://www.w3.org/1999/xlink");
case "textContent": case "textContent":
return $(t, e, r); return setDeleteAttr2(element, key, value);
case "style": case "style":
if (typeof r != "object") break; if (typeof value !== "object") break;
/* falls through */ /* falls through */
case "dataset": case "dataset":
return M(o, e, t, r, F.bind(null, t[e])); return forEachEntries(s, key, element, value, setDelete.bind(null, element[key]));
case "ariaset": case "ariaset":
return M(o, e, t, r, (f, p) => n("aria-" + f, p)); return forEachEntries(s, key, element, value, (key2, val) => setRemoveAttr("aria-" + key2, val));
case "classList": case "classList":
return K.call(c, t, r); return classListDeclarative.call(_this, element, value);
} }
return X(t, e) ? $(t, e, r) : n(e, r); return isPropSetter(element, key) ? setDeleteAttr2(element, key, value) : setRemoveAttr(key, value);
} }
function H(t, e) { function assignContext(element, _this) {
if (N.has(t)) return N.get(t); if (assign_context.has(element)) return assign_context.get(element);
let n = (t instanceof a.S ? tt : Y).bind(null, t, "Attribute"), o = S(e); const is_svg = isInstance(element, enviroment.S);
return { setRemoveAttr: n, s: o }; const setRemoveAttr = (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
const s = signals(_this);
return { setRemoveAttr, s };
} }
function K(t, e) { function classListDeclarative(element, toggle) {
let r = S(this); const s = signals(this);
return M( forEachEntries(
r, s,
"classList", "classList",
t, element,
e, toggle,
(n, o) => t.classList.toggle(n, o === -1 ? void 0 : !!o) (class_name, val) => element.classList.toggle(class_name, val === -1 ? void 0 : Boolean(val))
), t; );
return element;
} }
function Q(t, e, r, n) { function elementAttribute(element, op, key, value) {
return t instanceof a.H ? t[e + "Attribute"](r, n) : t[e + "AttributeNS"](null, r, n); if (isInstance(element, enviroment.H))
return element[op + "Attribute"](key, value);
return element[op + "AttributeNS"](null, key, value);
} }
function X(t, e) { function isPropSetter(el, key) {
if (!(e in t)) return !1; if (!(key in el)) return false;
let r = z(t, e); const des = getPropDescriptor(el, key);
return !m(r.set); return !isUndef(des.set);
} }
function z(t, e) { function getPropDescriptor(p, key) {
if (t = Object.getPrototypeOf(t), !t) return {}; p = Object.getPrototypeOf(p);
let r = Object.getOwnPropertyDescriptor(t, e); if (!p) return {};
return r || z(t, e); const des = Object.getOwnPropertyDescriptor(p, key);
if (!des) return getPropDescriptor(p, key);
return des;
} }
function M(t, e, r, n, o) { function forEachEntries(s, target, element, obj, cb) {
let c = String; const S = String;
if (!(typeof n != "object" || n === null)) if (typeof obj !== "object" || obj === null) return;
return Object.entries(n).forEach(function([f, p]) { return Object.entries(obj).forEach(function process([key, val]) {
f && (f = new c(f), f.target = e, p = t.processReactiveAttribute(r, f, p, o), o(f, p)); if (!key) return;
key = new S(key);
key.target = target;
val = s.processReactiveAttribute(element, key, val, cb);
cb(key, val);
}); });
} }
function Y(t, e, r, n) { function setRemove(obj, prop, key, val) {
return t[(m(n) ? "remove" : "set") + e](r, n); return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
} }
function tt(t, e, r, n, o = null) { function setRemoveNS(obj, prop, key, val, ns = null) {
return t[(m(n) ? "remove" : "set") + e + "NS"](o, r, n); return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
} }
function F(t, e, r) { function setDelete(obj, key, val) {
if (Reflect.set(t, e, r), !!m(r)) Reflect.set(obj, key, val);
return Reflect.deleteProperty(t, e); if (!isUndef(val)) return;
return Reflect.deleteProperty(obj, key);
} }
// src/events-observer.js // src/events-observer.js
var _ = a.M ? et() : new Proxy({}, { var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get() { get() {
return () => { return () => {
}; };
} }
}); });
function et() { function connectionsChangesObserverConstructor() {
let t = /* @__PURE__ */ new Map(), e = !1, r = (s) => function(u) { const store = /* @__PURE__ */ new Map();
for (let i of u) let is_observing = false;
if (i.type === "childList") { const observerListener = (stop2) => function(mutations) {
if (l(i.addedNodes, !0)) { for (const mutation of mutations) {
s(); if (mutation.type !== "childList") continue;
if (observerAdded(mutation.addedNodes, true)) {
stop2();
continue; continue;
} }
E(i.removedNodes, !0) && s(); if (observerRemoved(mutation.removedNodes, true))
} stop2();
}, n = new a.M(r(f));
return {
observe(s) {
let u = new a.M(r(() => {
}));
return u.observe(s, { childList: !0, subtree: !0 }), () => u.disconnect();
},
onConnected(s, u) {
d();
let i = c(s);
i.connected.has(u) || (i.connected.add(u), i.length_c += 1);
},
offConnected(s, u) {
if (!t.has(s)) return;
let i = t.get(s);
i.connected.has(u) && (i.connected.delete(u), i.length_c -= 1, o(s, i));
},
onDisconnected(s, u) {
d();
let i = c(s);
i.disconnected.has(u) || (i.disconnected.add(u), i.length_d += 1);
},
offDisconnected(s, u) {
if (!t.has(s)) return;
let i = t.get(s);
i.disconnected.has(u) && (i.disconnected.delete(u), i.length_d -= 1, o(s, i));
} }
}; };
function o(s, u) { const observer = new enviroment.M(observerListener(stop));
u.length_c || u.length_d || (t.delete(s), f()); return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element) {
const o = new enviroment.M(observerListener(() => {
}));
o.observe(element, { childList: true, subtree: true });
return () => o.disconnect();
},
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.connected.has(listener)) return;
listeners.connected.add(listener);
listeners.length_c += 1;
},
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.connected.has(listener)) return;
ls.connected.delete(listener);
ls.length_c -= 1;
cleanWhenOff(element, ls);
},
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener) {
start();
const listeners = getElementStore(element);
if (listeners.disconnected.has(listener)) return;
listeners.disconnected.add(listener);
listeners.length_d += 1;
},
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener) {
if (!store.has(element)) return;
const ls = store.get(element);
if (!ls.disconnected.has(listener)) return;
ls.disconnected.delete(listener);
ls.length_d -= 1;
cleanWhenOff(element, ls);
} }
function c(s) { };
if (t.has(s)) return t.get(s); function cleanWhenOff(element, ls) {
let u = { if (ls.length_c || ls.length_d)
return;
store.delete(element);
stop();
}
function getElementStore(element) {
if (store.has(element)) return store.get(element);
const out = {
connected: /* @__PURE__ */ new WeakSet(), connected: /* @__PURE__ */ new WeakSet(),
length_c: 0, length_c: 0,
disconnected: /* @__PURE__ */ new WeakSet(), disconnected: /* @__PURE__ */ new WeakSet(),
length_d: 0 length_d: 0
}; };
return t.set(s, u), u; store.set(element, out);
return out;
} }
function d() { function start() {
e || (e = !0, n.observe(a.D.body, { childList: !0, subtree: !0 })); if (is_observing) return;
is_observing = true;
observer.observe(enviroment.D.body, { childList: true, subtree: true });
} }
function f() { function stop() {
!e || t.size || (e = !1, n.disconnect()); if (!is_observing || store.size) return;
is_observing = false;
observer.disconnect();
} }
function p() { function requestIdle() {
return new Promise(function(s) { return new Promise(function(resolve) {
(requestIdleCallback || requestAnimationFrame)(s); (requestIdleCallback || requestAnimationFrame)(resolve);
}); });
} }
async function b(s) { async function collectChildren(element) {
t.size > 30 && await p(); if (store.size > 30)
let u = []; await requestIdle();
if (!(s instanceof Node)) return u; const out = [];
for (let i of t.keys()) if (!isInstance(element, Node)) return out;
i === s || !(i instanceof Node) || s.contains(i) && u.push(i); for (const el of store.keys()) {
return u; if (el === element || !isInstance(el, Node)) continue;
if (element.contains(el))
out.push(el);
} }
function l(s, u) { return out;
let i = !1;
for (let h of s) {
if (u && b(h).then(l), !t.has(h)) continue;
let A = t.get(h);
A.length_c && (h.dispatchEvent(new Event(v)), A.connected = /* @__PURE__ */ new WeakSet(), A.length_c = 0, A.length_d || t.delete(h), i = !0);
} }
return i; function observerAdded(addedNodes, is_root) {
let out = false;
for (const element of addedNodes) {
if (is_root) collectChildren(element).then(observerAdded);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_c) continue;
element.dispatchEvent(new Event(evc));
ls.connected = /* @__PURE__ */ new WeakSet();
ls.length_c = 0;
if (!ls.length_d) store.delete(element);
out = true;
} }
function E(s, u) { return out;
let i = !1;
for (let h of s)
u && b(h).then(E), !(!t.has(h) || !t.get(h).length_d) && ((globalThis.queueMicrotask || setTimeout)(I(h)), i = !0);
return i;
} }
function I(s) { function observerRemoved(removedNodes, is_root) {
let out = false;
for (const element of removedNodes) {
if (is_root) collectChildren(element).then(observerRemoved);
if (!store.has(element)) continue;
const ls = store.get(element);
if (!ls.length_d) continue;
(globalThis.queueMicrotask || setTimeout)(dispatchRemove(element));
out = true;
}
return out;
}
function dispatchRemove(element) {
return () => { return () => {
s.isConnected || (s.dispatchEvent(new Event(w)), t.delete(s)); if (element.isConnected) return;
element.dispatchEvent(new Event(evd));
store.delete(element);
}; };
} }
} }
// src/customElement.js // src/customElement.js
function wt(t, e, r = rt) { function customElementRender(target, render, props = observedAttributes2) {
let n = t.host || t; const custom_element = target.host || target;
O.push({ scope.push({
scope: n, scope: custom_element,
host: (...d) => d.length ? d.forEach((f) => f(n)) : n host: (...c) => c.length ? c.forEach((c2) => c2(custom_element)) : custom_element
}), typeof r == "function" && (r = r.call(n, n)); });
let o = n[x]; if (typeof props === "function") props = props.call(custom_element, custom_element);
o || nt(n); const is_lte = custom_element[keyLTE];
let c = e.call(n, r); if (!is_lte) lifecyclesToEvents(custom_element);
return o || n.dispatchEvent(new Event(v)), t.nodeType === 11 && typeof t.mode == "string" && n.addEventListener(w, _.observe(t), { once: !0 }), O.pop(), t.append(c); const out = render.call(custom_element, props);
if (!is_lte) custom_element.dispatchEvent(new Event(evc));
if (target.nodeType === 11 && typeof target.mode === "string")
custom_element.addEventListener(evd, c_ch_o.observe(target), { once: true });
scope.pop();
return target.append(out);
} }
function nt(t) { function lifecyclesToEvents(class_declaration) {
return j(t.prototype, "connectedCallback", function(e, r, n) { wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail) {
e.apply(r, n), r.dispatchEvent(new Event(v)); target.apply(thisArg, detail);
}), j(t.prototype, "disconnectedCallback", function(e, r, n) { thisArg.dispatchEvent(new Event(evc));
e.apply(r, n), (globalThis.queueMicrotask || setTimeout)( });
() => !r.isConnected && r.dispatchEvent(new Event(w)) wrapMethod(class_declaration.prototype, "disconnectedCallback", function(target, thisArg, detail) {
target.apply(thisArg, detail);
(globalThis.queueMicrotask || setTimeout)(
() => !thisArg.isConnected && thisArg.dispatchEvent(new Event(evd))
); );
}), j(t.prototype, "attributeChangedCallback", function(e, r, n) { });
let [o, , c] = n; wrapMethod(class_declaration.prototype, "attributeChangedCallback", function(target, thisArg, detail) {
r.dispatchEvent(new CustomEvent(y, { const [attribute, , value] = detail;
detail: [o, c] thisArg.dispatchEvent(new CustomEvent(eva, {
})), e.apply(r, n); detail: [attribute, value]
}), t.prototype[x] = !0, t; }));
target.apply(thisArg, detail);
});
class_declaration.prototype[keyLTE] = true;
return class_declaration;
} }
function j(t, e, r) { function wrapMethod(obj, method, apply) {
t[e] = new Proxy(t[e] || (() => { obj[method] = new Proxy(obj[method] || (() => {
}), { apply: r }); }), { apply });
} }
function rt(t) { function observedAttributes2(instance) {
return q(t, (e, r) => e.getAttribute(r)); return observedAttributes(instance, (i, n) => i.getAttribute(n));
} }
// src/events.js // src/events.js
function yt(t, e, r) { function dispatchEvent(name, options, host) {
return e || (e = {}), function(o, ...c) { if (!options) options = {};
r && (c.unshift(o), o = typeof r == "function" ? r() : r); return function dispatch(element, ...d) {
let d = c.length ? new CustomEvent(t, Object.assign({ detail: c[0] }, e)) : new Event(t, e); if (host) {
return o.dispatchEvent(d); d.unshift(element);
element = typeof host === "function" ? host() : host;
}
const event = d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event);
}; };
} }
function D(t, e, r) { function on(event, listener, options) {
return function(o) { return function registerElement(element) {
return o.addEventListener(t, e, r), o; element.addEventListener(event, listener, options);
return element;
}; };
} }
var B = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 }); var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
D.connected = function(t, e) { on.connected = function(listener, options) {
return e = B(e), function(n) { options = lifeOptions(options);
return n.addEventListener(v, t, e), n[x] ? n : n.isConnected ? (n.dispatchEvent(new Event(v)), n) : (L(e.signal, () => _.offConnected(n, t)) && _.onConnected(n, t), n); return function registerElement(element) {
element.addEventListener(evc, listener, options);
if (element[keyLTE]) return element;
if (element.isConnected) return element.dispatchEvent(new Event(evc)), element;
const c = onAbort(options.signal, () => c_ch_o.offConnected(element, listener));
if (c) c_ch_o.onConnected(element, listener);
return element;
}; };
}; };
D.disconnected = function(t, e) { on.disconnected = function(listener, options) {
return e = B(e), function(n) { options = lifeOptions(options);
return n.addEventListener(w, t, e), n[x] || L(e.signal, () => _.offDisconnected(n, t)) && _.onDisconnected(n, t), n; return function registerElement(element) {
element.addEventListener(evd, listener, options);
if (element[keyLTE]) return element;
const c = onAbort(options.signal, () => c_ch_o.offDisconnected(element, listener));
if (c) c_ch_o.onDisconnected(element, listener);
return element;
}; };
}; };
var W = /* @__PURE__ */ new WeakMap(); var store_abort = /* @__PURE__ */ new WeakMap();
D.disconnectedAsAbort = function(t) { on.disconnectedAsAbort = function(host) {
if (W.has(t)) return W.get(t); if (store_abort.has(host)) return store_abort.get(host);
let e = new AbortController(); const a = new AbortController();
return W.set(t, e), t(D.disconnected(() => e.abort())), e; store_abort.set(host, a);
host(on.disconnected(() => a.abort()));
return a;
}; };
var ot = /* @__PURE__ */ new WeakSet(); var els_attribute_store = /* @__PURE__ */ new WeakSet();
D.attributeChanged = function(t, e) { on.attributeChanged = function(listener, options) {
return typeof e != "object" && (e = {}), function(n) { if (typeof options !== "object")
if (n.addEventListener(y, t, e), n[x] || ot.has(n) || !a.M) return n; options = {};
let o = new a.M(function(d) { return function registerElement(element) {
for (let { attributeName: f, target: p } of d) element.addEventListener(eva, listener, options);
p.dispatchEvent( if (element[keyLTE] || els_attribute_store.has(element))
new CustomEvent(y, { detail: [f, p.getAttribute(f)] }) return element;
if (!enviroment.M) return element;
const observer = new enviroment.M(function(mutations) {
for (const { attributeName, target } of mutations)
target.dispatchEvent(
new CustomEvent(eva, { detail: [attributeName, target.getAttribute(attributeName)] })
); );
}); });
return L(e.signal, () => o.disconnect()) && o.observe(n, { attributes: !0 }), n; const c = onAbort(options.signal, () => observer.disconnect());
if (c) observer.observe(element, { attributes: true });
return element;
}; };
}; };
export { export {
R as assign, assign,
U as assignAttribute, assignAttribute,
J as chainableAppend, chainableAppend,
K as classListDeclarative, classListDeclarative,
P as createElement, createElement,
pt as createElementNS, createElementNS,
wt as customElementRender, customElementRender,
nt as customElementWithDDE, lifecyclesToEvents as customElementWithDDE,
yt as dispatchEvent, dispatchEvent,
P as el, createElement as el,
pt as elNS, createElementNS as elNS,
Q as elementAttribute, elementAttribute,
nt as lifecyclesToEvents, lifecyclesToEvents,
rt as observedAttributes, observedAttributes2 as observedAttributes,
D as on, on,
dt as queue, queue,
Z as registerReactivity, registerReactivity,
O as scope, scope,
lt as simulateSlots simulateSlots
}; };

View File

@ -1,28 +1,179 @@
import { registerClientFile, styles } from "../ssr.js"; import { registerClientFile, styles } from "../ssr.js";
const host= "."+code.name; const host= "."+code.name;
styles.css` styles.css`
${host}{ /* Code block styling */
--shiki-color-text: #e9eded; ${host} {
--shiki-color-background: #212121; /* Theme for dark mode - matches Flems/CodeMirror dark theme */
--shiki-color-text: #f8f8f2;
--shiki-color-background: var(--code-bg);
--shiki-token-constant: #82b1ff; --shiki-token-constant: #82b1ff;
--shiki-token-string: #c3e88d; --shiki-token-string: #c3e88d;
--shiki-token-comment: #546e7a; --shiki-token-comment: #546e7a;
--shiki-token-keyword: #c792ea; --shiki-token-keyword: #c792ea;
--shiki-token-parameter: #AA0000; --shiki-token-parameter: #fd971f;
--shiki-token-function: #80cbae; --shiki-token-function: #80cbae;
--shiki-token-string-expression: #c3e88d; --shiki-token-string-expression: #c3e88d;
--shiki-token-punctuation: var(--code); --shiki-token-punctuation: #89ddff;
--shiki-token-link: #EE0000; --shiki-token-link: #82aaff;
--shiki-token-variable: #f8f8f2;
--shiki-token-number: #f78c6c;
--shiki-token-boolean: #82b1ff;
--shiki-token-tag: #f07178;
--shiki-token-attribute: #ffcb6b;
--shiki-token-property: #82b1ff;
--shiki-token-operator: #89ddff;
--shiki-token-regex: #c3e88d;
--shiki-token-class: #ffcb6b;
/* Basic styling */
white-space: pre; white-space: pre;
tab-size: 2; /* TODO: allow custom tab size?! */ tab-size: 2;
overflow: scroll; overflow: auto;
border-radius: var(--border-radius);
font-family: var(--font-mono);
font-size: 0.8rem;
line-height: 1.5;
position: relative;
margin-block: 1rem;
width: 100%;
} }
${host}[data-js=todo]{
/* Light mode overrides to match GitHub-like theme */
@media (prefers-color-scheme: light) {
${host} {
--shiki-color-text: #24292e;
--shiki-color-background: var(--code-bg);
--shiki-token-constant: #005cc5;
--shiki-token-string: #22863a;
--shiki-token-comment: #6a737d;
--shiki-token-keyword: #d73a49;
--shiki-token-parameter: #e36209;
--shiki-token-function: #6f42c1;
--shiki-token-string-expression: #22863a;
--shiki-token-punctuation: #24292e;
--shiki-token-link: #0366d6;
--shiki-token-variable: #24292e;
--shiki-token-number: #005cc5;
--shiki-token-boolean: #005cc5;
--shiki-token-tag: #22863a;
--shiki-token-attribute: #005cc5;
--shiki-token-property: #005cc5;
--shiki-token-operator: #d73a49;
--shiki-token-regex: #032f62;
--shiki-token-class: #6f42c1;
}
}
/* Support for theme toggles */
html[data-theme="light"] ${host} {
--shiki-color-text: #24292e;
--shiki-color-background: var(--code-bg);
--shiki-token-constant: #005cc5;
--shiki-token-string: #22863a;
--shiki-token-comment: #6a737d;
--shiki-token-keyword: #d73a49;
--shiki-token-parameter: #e36209;
--shiki-token-function: #6f42c1;
--shiki-token-string-expression: #22863a;
--shiki-token-punctuation: #24292e;
--shiki-token-link: #0366d6;
--shiki-token-variable: #24292e;
--shiki-token-number: #005cc5;
--shiki-token-boolean: #005cc5;
--shiki-token-tag: #22863a;
--shiki-token-attribute: #005cc5;
--shiki-token-property: #005cc5;
--shiki-token-operator: #d73a49;
--shiki-token-regex: #032f62;
--shiki-token-class: #6f42c1;
}
html[data-theme="dark"] ${host} {
--shiki-color-text: #f8f8f2;
--shiki-color-background: var(--code-bg);
--shiki-token-constant: #82b1ff;
--shiki-token-string: #c3e88d;
--shiki-token-comment: #546e7a;
--shiki-token-keyword: #c792ea;
--shiki-token-parameter: #fd971f;
--shiki-token-function: #80cbae;
--shiki-token-string-expression: #c3e88d;
--shiki-token-punctuation: #89ddff;
--shiki-token-link: #82aaff;
--shiki-token-variable: #f8f8f2;
--shiki-token-number: #f78c6c;
--shiki-token-boolean: #82b1ff;
--shiki-token-tag: #f07178;
--shiki-token-attribute: #ffcb6b;
--shiki-token-property: #82b1ff;
--shiki-token-operator: #89ddff;
--shiki-token-regex: #c3e88d;
--shiki-token-class: #ffcb6b;
}
/* Code block with syntax highlighting waiting for JS */
${host}[data-js=todo] {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--standard-border-radius); border-radius: var(--border-radius);
margin-bottom: 1rem; padding: 1rem;
margin-top: 18.4px; /* to fix shift when → dataJS=done */ background-color: var(--code-bg);
padding: 1rem 1.4rem; position: relative;
}
/* Add a subtle loading indicator */
${host}[data-js=todo]::before {
content: "Loading syntax highlighting...";
position: absolute;
top: 0.5rem;
right: 0.5rem;
font-size: 0.75rem;
color: var(--text-light);
background-color: var(--bg);
padding: 0.25rem 0.5rem;
border-radius: var(--border-radius);
opacity: 0.7;
}
/* All code blocks should have consistent font and sizing */
${host} code {
font-family: var(--font-mono);
font-size: inherit;
line-height: 1.5;
padding: 0;
}
${host} pre {
margin-block: 0;
font-size: inherit;
}
/* Ensure line numbers (if added) are styled appropriately */
${host} .line-number {
user-select: none;
opacity: 0.5;
margin-right: 1rem;
min-width: 1.5rem;
display: inline-block;
text-align: right;
}
/* If there's a copy button, style it */
${host} .copy-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background-color: var(--bg);
color: var(--text);
border: 1px solid var(--border);
border-radius: var(--border-radius);
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
}
${host}:hover .copy-button {
opacity: 1;
} }
`; `;
import { el } from "deka-dom-el"; import { el } from "deka-dom-el";
@ -52,12 +203,46 @@ let is_registered= {};
function registerClientPart(page_id){ function registerClientPart(page_id){
if(is_registered[page_id]) return; if(is_registered[page_id]) return;
// Add Shiki with a more reliable loading method
document.head.append( document.head.append(
el("script", { src: "https://cdn.jsdelivr.net/npm/shiki@0.9", defer: true }), // Use a newer version of Shiki with better performance
el("script", { src: "https://cdn.jsdelivr.net/npm/shiki@0.14.3/dist/index.unpkg.iife.js", defer: true }),
// Make sure we can match Flems styling in dark/light mode
el("style", `
/* Ensure CodeMirror and Shiki use the same font */
.CodeMirror *, .shiki * {
font-family: var(--font-mono) !important;
}
/* Style Shiki's output to match our theme */
.shiki {
background-color: var(--shiki-color-background) !important;
color: var(--shiki-color-text) !important;
padding: 1rem;
border-radius: var(--border-radius);
tab-size: 2;
}
/* Ensure Shiki code tokens use our CSS variables */
.shiki .keyword { color: var(--shiki-token-keyword) !important; }
.shiki .constant { color: var(--shiki-token-constant) !important; }
.shiki .string { color: var(--shiki-token-string) !important; }
.shiki .comment { color: var(--shiki-token-comment) !important; }
.shiki .function { color: var(--shiki-token-function) !important; }
.shiki .operator, .shiki .punctuation { color: var(--shiki-token-punctuation) !important; }
.shiki .parameter { color: var(--shiki-token-parameter) !important; }
.shiki .variable { color: var(--shiki-token-variable) !important; }
.shiki .property { color: var(--shiki-token-property) !important; }
`),
); );
// Register our highlighting script to run after Shiki loads
const scriptElement = el("script", { type: "module" });
registerClientFile( registerClientFile(
new URL("./code.js.js", import.meta.url), new URL("./code.js.js", import.meta.url),
el("script", { type: "module" }) scriptElement
); );
is_registered[page_id]= true; is_registered[page_id]= true;
} }

View File

@ -1,12 +1,61 @@
const highlighter= await globalThis.shiki.getHighlighter({ try {
// Initialize Shiki with our custom theme
const highlighter = await globalThis.shiki.getHighlighter({
theme: "css-variables", theme: "css-variables",
langs: ["js", "ts", "css", "html", "shell"], langs: ["javascript", "typescript", "css", "html", "shell"],
}); });
const codeBlocks= document.querySelectorAll('code[class*="language-"]');
codeBlocks.forEach((block)=> { // Find all code blocks that need highlighting
const lang= block.className.replace("language-", ""); const codeBlocks = document.querySelectorAll('div[data-js="todo"] code[class*="language-"]');
block.parentElement.dataset.js= "done";
const html= highlighter.codeToHtml(block.textContent, { lang }); // Process each code block
block.innerHTML= html; codeBlocks.forEach((block) => {
}); try {
// Get the language from the class
const langClass = block.className.match(/language-(\w+)/);
// Map the language to Shiki format
let lang = langClass ? langClass[1] : 'javascript';
if (lang === 'js') lang = 'javascript';
if (lang === 'ts') lang = 'typescript';
// Mark the container as processed
block.parentElement.dataset.js = "done";
// Highlight the code
const code = block.textContent;
const html = highlighter.codeToHtml(code, { lang });
// Insert the highlighted HTML
block.innerHTML = html;
// Add copy button functionality
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-button';
copyBtn.textContent = 'Copy';
copyBtn.setAttribute('aria-label', 'Copy code to clipboard');
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(code).then(() => {
copyBtn.textContent = 'Copied!';
setTimeout(() => {
copyBtn.textContent = 'Copy';
}, 2000);
});
});
// Add the copy button to the code block container
block.parentElement.appendChild(copyBtn);
} catch (err) {
console.error('Error highlighting code block:', err);
// Make sure we don't leave the block in a pending state
block.parentElement.dataset.js = "error";
}
});
} catch (err) {
console.error('Failed to initialize Shiki:', err);
// Fallback: at least mark blocks as processed so they don't show loading indicator
document.querySelectorAll('div[data-js="todo"]').forEach(block => {
block.dataset.js = "error";
});
}

View File

@ -1,16 +1,103 @@
import { styles } from "../ssr.js"; import { styles } from "../ssr.js";
const host= "."+example.name; const host= "."+example.name;
styles.css` styles.css`
${host}{ ${host} {
grid-column: full-main; grid-column: full-main;
width: 100%; width: 100%;
max-width: calc(9/5 * var(--body-max-width)); max-width: calc(9/5 * var(--body-max-width));
height: calc(3/5 * var(--body-max-width)); height: calc(3/5 * var(--body-max-width));
margin-inline: auto; margin: 2rem auto;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
border: 1px solid var(--border);
} }
${host} .runtime {
background-color: whitesmoke;
}
/* CodeMirror styling to match our theme */
.CodeMirror {
height: 100% !important;
font-family: var(--font-mono) !important;
font-size: 0.8rem !important;
line-height: 1.5 !important;
}
/* Dark mode styles for CodeMirror */
.CodeMirror, .CodeMirror-gutters { .CodeMirror, .CodeMirror-gutters {
background: #212121 !important; background: var(--code-bg) !important;
border: 1px solid white; border: 1px solid var(--border) !important;
color: var(--text) !important;
}
/* Light mode adjustments for CodeMirror - using CSS variables */
@media (prefers-color-scheme: light) {
/* Core syntax elements */
.cm-s-material .cm-keyword { color: var(--shiki-token-keyword, #d73a49) !important; }
.cm-s-material .cm-atom { color: var(--shiki-token-constant, #005cc5) !important; }
.cm-s-material .cm-number { color: var(--shiki-token-number, #005cc5) !important; }
.cm-s-material .cm-def { color: var(--shiki-token-function, #6f42c1) !important; }
.cm-s-material .cm-variable { color: var(--shiki-token-variable, #24292e) !important; }
.cm-s-material .cm-variable-2 { color: var(--shiki-token-variable, #24292e) !important; }
.cm-s-material .cm-variable-3 { color: var(--shiki-token-variable, #24292e) !important; }
.cm-s-material .cm-property { color: var(--shiki-token-property, #005cc5) !important; }
.cm-s-material .cm-operator { color: var(--shiki-token-operator, #d73a49) !important; }
.cm-s-material .cm-comment { color: var(--shiki-token-comment, #6a737d) !important; }
.cm-s-material .cm-string { color: var(--shiki-token-string, #22863a) !important; }
.cm-s-material .cm-string-2 { color: var(--shiki-token-string, #22863a) !important; }
.cm-s-material .cm-tag { color: var(--shiki-token-tag, #22863a) !important; }
.cm-s-material .cm-attribute { color: var(--shiki-token-attribute, #005cc5) !important; }
.cm-s-material .cm-bracket { color: var(--shiki-token-punctuation, #24292e) !important; }
.cm-s-material .cm-punctuation { color: var(--shiki-token-punctuation, #24292e) !important; }
.cm-s-material .cm-link { color: var(--shiki-token-link, #0366d6) !important; }
.cm-s-material .cm-error { color: #f44336 !important; }
}
/* Handle theme toggle */
html[data-theme="light"] .CodeMirror {
background: #f5f7fa !important;
}
html[data-theme="light"] .CodeMirror-gutters {
background: #f5f7fa !important;
border-right: 1px solid #e5e7eb !important;
}
/* Also apply the same styles to CodeMirror with data-theme */
html[data-theme="light"] .cm-s-material .cm-keyword { color: var(--shiki-token-keyword, #d73a49) !important; }
html[data-theme="light"] .cm-s-material .cm-atom { color: var(--shiki-token-constant, #005cc5) !important; }
html[data-theme="light"] .cm-s-material .cm-number { color: var(--shiki-token-number, #005cc5) !important; }
html[data-theme="light"] .cm-s-material .cm-def { color: var(--shiki-token-function, #6f42c1) !important; }
html[data-theme="light"] .cm-s-material .cm-variable { color: var(--shiki-token-variable, #24292e) !important; }
html[data-theme="light"] .cm-s-material .cm-variable-2 { color: var(--shiki-token-variable, #24292e) !important; }
html[data-theme="light"] .cm-s-material .cm-variable-3 { color: var(--shiki-token-variable, #24292e) !important; }
html[data-theme="light"] .cm-s-material .cm-property { color: var(--shiki-token-property, #005cc5) !important; }
html[data-theme="light"] .cm-s-material .cm-operator { color: var(--shiki-token-operator, #d73a49) !important; }
html[data-theme="light"] .cm-s-material .cm-comment { color: var(--shiki-token-comment, #6a737d) !important; }
html[data-theme="light"] .cm-s-material .cm-string { color: var(--shiki-token-string, #22863a) !important; }
html[data-theme="light"] .cm-s-material .cm-string-2 { color: var(--shiki-token-string, #22863a) !important; }
html[data-theme="light"] .cm-s-material .cm-tag { color: var(--shiki-token-tag, #22863a) !important; }
html[data-theme="light"] .cm-s-material .cm-attribute { color: var(--shiki-token-attribute, #005cc5) !important; }
html[data-theme="light"] .cm-s-material .cm-bracket { color: var(--shiki-token-punctuation, #24292e) !important; }
html[data-theme="light"] .cm-s-material .cm-punctuation { color: var(--shiki-token-punctuation, #24292e) !important; }
html[data-theme="light"] .cm-s-material .cm-link { color: var(--shiki-token-link, #0366d6) !important; }
html[data-theme="light"] .cm-s-material .cm-error { color: #f44336 !important; }
/* Mobile adjustments */
@media (max-width: 767px) {
${host} {
height: 50vh;
max-width: 100%;
}
${host} main {
flex-grow: 1;
display: flex;
flex-direction: column;
}
${host} main > * {
width: 100%;
max-width: 100% !important;
}
} }
`; `;

View File

@ -26,7 +26,7 @@ function ddeComponent({ attr }){
on.connected(e=> console.log(( /** @type {HTMLParagraphElement} */ (e.target)).outerHTML)), on.connected(e=> console.log(( /** @type {HTMLParagraphElement} */ (e.target)).outerHTML)),
); );
return el().append( return el().append(
el("p", S(()=> `Hello from Custom Element with attribute '${attr()}'`)) el("p", S(()=> `Hello from Custom Element with attribute '${attr.get()}'`))
); );
} }
customElementWithDDE(HTMLCustomElement); customElementWithDDE(HTMLCustomElement);

View File

@ -4,11 +4,11 @@ const threePS= ({ emoji= "🚀" })=> {
const clicks= S(0); // A const clicks= S(0); // A
return el().append( return el().append(
el("p", S(()=> el("p", S(()=>
"Hello World "+emoji.repeat(clicks()) // B "Hello World "+emoji.repeat(clicks.get()) // B
)), )),
el("button", { el("button", {
type: "button", type: "button",
onclick: ()=> clicks(clicks()+1), // C onclick: ()=> clicks.set(clicks.get()+1), // C
textContent: "Fire", textContent: "Fire",
}) })
); );

View File

@ -22,9 +22,9 @@ const onsubmit= on("submit", function(event){
S.action(todos, "push", data.get("todo")); S.action(todos, "push", data.get("todo"));
break; break;
case "E"/*dit*/: { case "E"/*dit*/: {
const last= todos().at(-1); const last= todos.get().at(-1);
if(!last) break; if(!last) break;
last(data.get("todo")); last.set(data.get("todo"));
break; break;
} }
case "R"/*emove*/: case "R"/*emove*/:

View File

@ -1,15 +1,15 @@
import { S } from "deka-dom-el/signals"; import { S } from "deka-dom-el/signals";
const signal= S(0); const signal= S(0);
// computation pattern // computation pattern
const double= S(()=> 2*signal()); const double= S(()=> 2*signal.get());
const ac= new AbortController(); const ac= new AbortController();
S.on(signal, v=> console.log("signal", v), { signal: ac.signal }); S.on(signal, v=> console.log("signal", v), { signal: ac.signal });
S.on(double, v=> console.log("double", v), { signal: ac.signal }); S.on(double, v=> console.log("double", v), { signal: ac.signal });
signal(signal()+1); signal.set(signal.get()+1);
const interval= 5 * 1000; const interval= 5 * 1000;
const id= setInterval(()=> signal(signal()+1), interval); const id= setInterval(()=> signal.set(signal.get()+1), interval);
ac.signal.addEventListener("abort", ac.signal.addEventListener("abort",
()=> setTimeout(()=> clearInterval(id), 2*interval)); ()=> setTimeout(()=> clearInterval(id), 2*interval));

View File

@ -3,8 +3,8 @@ const count= S(0);
import { el } from "deka-dom-el"; import { el } from "deka-dom-el";
document.body.append( document.body.append(
el("p", S(()=> "Currently: "+count())), el("p", S(()=> "Currently: "+count.get())),
el("p", { classList: { red: S(()=> count()%2 === 0) }, dataset: { count }, textContent: "Attributes example" }), el("p", { classList: { red: S(()=> count.get()%2 === 0) }, dataset: { count }, textContent: "Attributes example" }),
); );
document.head.append( document.head.append(
el("style", ".red { color: red; }") el("style", ".red { color: red; }")
@ -12,4 +12,4 @@ document.head.append(
const interval= 5 * 1000; const interval= 5 * 1000;
setTimeout(clearInterval, 10*interval, setTimeout(clearInterval, 10*interval,
setInterval(()=> count(count()+1), interval)); setInterval(()=> count.set(count.get()+1), interval));

View File

@ -2,7 +2,7 @@ import { S } from "deka-dom-el/signals";
const count= S(0, { const count= S(0, {
add(){ this.value= this.value + Math.round(Math.random()*10); } add(){ this.value= this.value + Math.round(Math.random()*10); }
}); });
const numbers= S([ count() ], { const numbers= S([ count.get() ], {
push(next){ this.value.push(next); } push(next){ this.value.push(next); }
}); });
@ -22,5 +22,5 @@ document.body.append(
const interval= 5*1000; const interval= 5*1000;
setTimeout(clearInterval, 10*interval, setInterval(function(){ setTimeout(clearInterval, 10*interval, setInterval(function(){
S.action(count, "add"); S.action(count, "add");
S.action(numbers, "push", count()); S.action(numbers, "push", count.get());
}, interval)); }, interval));

View File

@ -4,7 +4,7 @@ const signal= S(0);
// β — just reacts on signal changes // β — just reacts on signal changes
S.on(signal, console.log); S.on(signal, console.log);
// γ — just updates the value // γ — just updates the value
const update= ()=> signal(signal()+1); const update= ()=> signal.set(signal.get()+1);
update(); update();
const interval= 5*1000; const interval= 5*1000;

View File

@ -1,18 +1,67 @@
import { pages, styles } from "../ssr.js"; import { pages, styles } from "../ssr.js";
const host= "."+prevNext.name; const host= "."+prevNext.name;
styles.css` styles.css`
${host}{ /* Previous/Next navigation */
display: grid; ${host} {
grid-template-columns: 1fr 2fr 1fr; display: flex;
margin-top: 1rem; justify-content: space-between;
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
gap: 1rem;
width: 100%;
} }
${host} [rel=prev]{
grid-column: 1; ${host} a {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
border-radius: var(--border-radius);
background-color: var(--primary-dark); /* Darker background for better contrast */
color: white;
font-weight: 600; /* Bolder text for better readability */
text-decoration: none;
transition: background-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
max-width: 45%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); /* Subtle shadow for better visibility */
} }
${host} [rel=next]{
grid-column: 3; ${host} a:hover {
text-align: right; background-color: var(--primary);
transform: translateY(-2px);
text-decoration: none;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* Enhanced shadow on hover */
}
${host} [rel=prev] {
margin-right: auto;
}
${host} [rel=next] {
margin-left: auto;
}
${host} [rel=prev]::before {
content: "←";
margin-right: 0.75rem;
font-size: 1.2em;
}
${host} [rel=next]::after {
content: "→";
margin-left: 0.75rem;
font-size: 1.2em;
}
/* If there's no previous/next, ensure the spacing still works */
${host} a:only-child {
margin-left: auto;
}
@media (max-width: 640px) {
${host} a {
font-size: 0.9rem;
}
} }
`; `;
import { el } from "../../index.js"; import { el } from "../../index.js";
@ -24,8 +73,15 @@ import { el } from "../../index.js";
export function h3({ textContent, id }){ export function h3({ textContent, id }){
if(!id) id= "h-"+textContent.toLowerCase().replaceAll(/\s/g, "-").replaceAll(/[^a-z-]/g, ""); if(!id) id= "h-"+textContent.toLowerCase().replaceAll(/\s/g, "-").replaceAll(/[^a-z-]/g, "");
return el("h3", { id }).append( return el("h3", { id }).append(
el("a", { textContent: "#", href: "#"+id, tabIndex: -1 }), el("a", {
" ", textContent className: "heading-anchor",
href: "#"+id,
textContent: "#",
title: `Link to this section: ${textContent}`,
"aria-label": `Link to section ${textContent}`
}),
" ",
textContent,
); );
} }
/** /**
@ -46,9 +102,20 @@ export function prevNext(page){
function pageLink({ rel, page }){ function pageLink({ rel, page }){
if(!page) return el(); if(!page) return el();
let { href, title, description }= page; let { href, title, description }= page;
return el("a", { rel, href, title: description }).append(
rel==="next" ?"(next) " : "", // Find the page index to show numbering
title, const pageIndex = pages.findIndex(p => p === page);
rel==="prev" ? " (previous)" : "", const pageNumber = pageIndex + 1;
const linkTitle = rel === "prev"
? `Previous: ${pageNumber}. ${title}`
: `Next: ${pageNumber}. ${title}`;
return el("a", {
rel,
href,
title: description || linkTitle
}).append(
title
); );
} }

View File

@ -1,124 +1,426 @@
import { styles } from "./ssr.js"; import { styles } from "./ssr.js";
styles.css` styles.css`
@import url(https://cdn.simplecss.org/simple.min.css); :root {
:root{ --primary: #b71c1c;
--body-max-width: 45rem; --primary-light: #f05545;
--marked: #fb3779; --primary-dark: #7f0000;
--code: #0d47a1; --primary-rgb: 183, 28, 28;
--accent: #d81b60; --secondary: #700037;
--secondary-light: #ae1357;
--secondary-dark: #4a0027;
--secondary-rgb: 112, 0, 55;
--font-main: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-mono: 'Fira Code', 'JetBrains Mono', 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
--body-max-width: 40rem;
--sidebar-width: 20rem;
--header-height: 4rem;
--border-radius: 0.375rem;
--bg: #ffffff;
--bg-sidebar: #fff5f5;
--text: #1a1313;
--text-light: #555050;
--code-bg: #f9f2f2;
--code-text: #9a0000;
--border: #d8c0c0;
--selection: rgba(183, 28, 28, 0.15);
--marked: #b71c1c;
--accent: var(--secondary);
--shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
--link-color: #9a0000;
--link-hover: #7f0000;
--button-text: #ffffff;
} }
@media (prefers-color-scheme:dark) {
@media (prefers-color-scheme: dark) {
:root { :root {
--accent: #f06292; --bg: #121212;
--code: #62c1f0; --bg-sidebar: #1a1212;
--text: #ffffff;
--text-light: #cccccc;
--code-bg: #2c2020;
--code-text: #ff9e80;
--border: #4d3939;
--selection: rgba(255, 99, 71, 0.25);
--primary: #b74141;
--primary-light: #ff867f;
--primary-dark: #c62828;
--secondary: #f02b47;
--secondary-light: #ff6090;
--secondary-dark: #b0003a;
--accent: var(--secondary);
--link-color: #ff5252;
--link-hover: #ff867f;
--button-text: #ffffff;
--nav-current-bg: #aa2222;
--nav-current-text: #ffffff;
--primary-rgb: 255, 82, 82;
--secondary-rgb: 233, 30, 99;
} }
} }
body {
grid-template-columns: 100%; /* Base styling */
grid-template-areas: "header" "sidebar" "content"; * {
} box-sizing: border-box;
@media (min-width:768px) {
body{
grid-template-rows: auto auto;
grid-template-columns: calc(10 * var(--body-max-width) / 27) auto;
grid-template-areas:
"header header"
"sidebar content"
}
}
body > *{
grid-column: unset;
}
body > header{
grid-area: header;
padding: 0;
}
body > nav{
grid-area: sidebar;
background-color: var(--accent-bg);
display: flex;
flex-flow: column nowrap;
}
body > nav {
font-size: 1rem;
line-height: 2;
padding: 1rem 0 0 0;
}
body > nav ol,
body > nav ul {
align-content: space-around;
align-items: center;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
list-style-type: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
body > nav ol li,
body > nav ul li { html {
display:inline-block scroll-behavior: smooth;
} }
body > nav a,
body > nav a:visited { /* Accessibility improvements */
margin: 0 .5rem 1rem .5rem; :focus {
border: 1px solid currentColor; outline: 3px solid rgba(63, 81, 181, 0.5);
border-radius: var(--standard-border-radius); outline-offset: 2px;
color: var(--text-light); }
display: inline-block;
padding: .1rem 1rem; :focus:not(:focus-visible) {
outline: none;
}
:focus-visible {
outline: 3px solid rgba(63, 81, 181, 0.5);
outline-offset: 2px;
}
/* Ensure reduced motion preferences are respected */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Skip link for better keyboard navigation */
.skip-link {
position: absolute;
top: 0;
left: 0;
transform: translateX(-100%);
z-index: 9999;
background-color: var(--primary);
color: white;
padding: 0.5rem 1rem;
text-decoration: none; text-decoration: none;
cursor: pointer; transition: transform 0.3s ease-in-out;
transition: all .15s;
} }
body > nav a.current,
body > nav a[aria-current=page] { .skip-link:focus {
transform: translateX(0);
}
body {
font-family: var(--font-main);
background-color: var(--bg); background-color: var(--bg);
color: var(--text); color: var(--text);
line-height: 1.6;
font-size: 1rem;
display: grid;
grid-template-columns: 100%;
grid-template-areas:
"header"
"sidebar"
"content";
min-height: 100vh;
} }
body > nav a:hover{
background-color: var(--bg); ::selection {
color: var(--accent); background-color: var(--selection);
} }
@media only screen and (max-width:720px) {
body > nav{ /* Typography */
flex-flow: row wrap; h1, h2, h3, h4, h5, h6 {
padding-block: .5rem; margin-bottom: 1rem;
} margin-top: 2rem;
body > nav a { font-weight: 700;
border:none; line-height: 1.25;
text-decoration:underline; color: var(--text);
margin-block: .1rem; }
padding-block:.1rem;
line-height: 1rem; h1 {
font-size: .9rem; font-size: 2.25rem;
margin-top: 0;
color: var(--primary-dark);
}
h1 > a {
font-weight: unset;
color: unset;
}
h2 {
font-size: 1.5rem;
border-bottom: 2px solid var(--border);
padding-bottom: 0.5rem;
color: var(--primary);
}
h3 {
font-size: 1.25rem;
color: var(--secondary);
}
p {
margin-bottom: 1.5rem;
}
a {
color: var(--link-color, var(--primary));
transition: color 0.2s ease;
font-weight: 500;
text-underline-offset: 3px;
transition: color 0.2s ease, text-underline-offset 0.2s ease;
}
a:visited {
--link-color: var(--secondary, #700037);
}
a:hover {
--link-color: var(--link-hover, var(--primary-light));
text-underline-offset: 5px;
}
code, pre {
font-family: var(--font-mono);
font-size: 0.9em;
border-radius: var(--border-radius);
}
code {
background-color: var(--code-bg);
color: var(--code-text);
padding: 0.2em 0.4em;
}
pre {
background-color: var(--code-bg);
padding: 1rem;
overflow-x: auto;
margin-bottom: 1.5rem;
border: 1px solid var(--border);
}
pre code {
background-color: transparent;
padding: 0;
}
/* Layout */
@media (min-width: 768px) {
body {
grid-template-rows: var(--header-height) 1fr;
grid-template-columns: var(--sidebar-width) 1fr;
grid-template-areas:
"header header"
"sidebar content";
} }
} }
main{
/* Main content */
body > main {
grid-area: content; grid-area: content;
padding: 2rem;
max-width: 100%;
overflow-x: hidden;
display: grid; display: grid;
grid-template-columns: grid-template-columns:
[full-main-start] 1fr [full-main-start] 1fr
[main-start] min(var(--body-max-width), 90%) [main-end] [main-start] min(var(--body-max-width), 90%) [main-end]
1fr [full-main-end]; 1fr [full-main-end];
} }
main > *, main slot > *{
body > main > *, body > main slot > * {
width: 100%;
max-width: 100%;
margin-inline: auto;
grid-column: main; grid-column: main;
} }
/* Page title with ID anchor for skip link */
body > main .page-title {
margin-top: 0;
border-bottom: 1px solid var(--border);
padding-bottom: 0.75rem;
margin-bottom: 1.5rem;
color: var(--primary);
position: relative;
}
/* Section headings with better visual hierarchy */
body > main h2, body > main h3 {
scroll-margin-top: calc(var(--header-height) + 1rem);
position: relative;
}
body > main h3 {
border-left: 3px solid var(--primary);
position: relative;
left: -1.5rem;
padding-inline-start: 1em;
}
/* Make clickable heading links for better navigation */
.heading-anchor {
position: absolute;
color: var(--text-light);
left: -2rem;
text-decoration: none;
font-weight: normal;
opacity: 0;
transition: opacity 0.2s;
}
h2:hover .heading-anchor,
h3:hover .heading-anchor {
opacity: 0.8;
}
@media (max-width: 767px) {
body > main {
padding: 1.5rem 1rem;
}
body > main h2, body > main h3 {
left: 1rem;
width: calc(100% - 1rem);
}
.heading-anchor {
opacity: 0.4;
}
}
/* Example boxes */
.example {
border: 1px solid var(--border);
border-radius: var(--border-radius);
margin: 2rem 0;
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: box-shadow 0.2s;
}
.example:hover {
box-shadow: var(--shadow);
}
.example-header {
background-color: var(--bg-sidebar);
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.example-content {
padding: 1.25rem;
}
/* Icon styling */
.icon { .icon {
vertical-align: sub;
padding-right: .25rem;
display: inline-block; display: inline-block;
width: 1em; width: 1em;
height: 1.3em; height: 1em;
margin-right: 0.2rem; vertical-align: -0.125em;
stroke-width: 0; stroke-width: 0;
stroke: currentColor; stroke: currentColor;
fill: currentColor; fill: currentColor;
} }
.note{
font-size: .9rem; /* Information blocks */
font-style: italic; .note, .tip, .warning {
padding: 1rem 1.25rem;
margin: 1.5rem 0;
border-radius: var(--border-radius);
position: relative;
font-size: 0.95rem;
line-height: 1.5;
}
.note {
background-color: rgba(63, 81, 181, 0.08);
border-left: 4px solid var(--primary);
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.tip {
background-color: rgba(46, 204, 113, 0.08);
border-left: 4px solid #2ecc71;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.warning {
background-color: rgba(241, 196, 15, 0.08);
border-left: 4px solid #f1c40f;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.note::before, .tip::before, .warning::before {
font-weight: 600;
display: block;
margin-bottom: 0.5rem;
}
.note::before {
content: "Note";
color: var(--primary);
}
.tip::before {
content: "Tip";
color: #2ecc71;
}
.warning::before {
content: "Warning";
color: #f1c40f;
}
/* Prev/Next buttons */
.prev-next {
display: flex;
justify-content: space-between;
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
.prev-next a {
display: flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
background-color: var(--primary);
color: white;
transition: background-color 0.2s ease;
}
.prev-next a:hover {
background-color: var(--primary-dark);
text-decoration: none;
}
.prev-next a:empty {
display: none;
}
.prev-next a[rel="prev"]::before {
content: "←";
margin-right: 0.5rem;
}
.prev-next a[rel="next"]::after {
content: "→";
margin-left: 0.5rem;
} }
`; `;

View File

@ -1,38 +1,239 @@
import { el, elNS } from "deka-dom-el"; import { el, elNS } from "deka-dom-el";
import { pages } from "../ssr.js"; import { pages, styles } from "../ssr.js";
const host= "."+header.name;
const host_nav= "."+nav.name;
styles.css`
/* Header */
${host} {
grid-area: header;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.5rem;
background-color: var(--primary);
color: white;
box-shadow: var(--shadow);
min-height: var(--header-height);
}
${host} .header-title {
display: flex;
align-items: center;
gap: 0.75rem;
}
${host} h1 {
font-size: 1.25rem;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: white;
}
${host} .version-badge {
font-size: 0.75rem;
background-color: rgba(150, 150, 150, 0.2);
padding: 0.25rem 0.5rem;
border-radius: var(--border-radius);
}
${host} p {
display: block;
font-size: 0.875rem;
opacity: 0.9;
margin: 0;
}
${host} .github-link {
display: flex;
align-items: center;
gap: 0.5rem;
color: white;
font-size: 0.875rem;
padding: 0.375rem 0.75rem;
border-radius: var(--border-radius);
background-color: rgba(0, 0, 0, 0.2);
text-decoration: none;
transition: background-color 0.2s;
}
${host} .github-link:hover {
background-color: rgba(0, 0, 0, 0.3);
text-decoration: none;
}
/* Navigation */
${host_nav} {
grid-area: sidebar;
background-color: var(--bg-sidebar);
border-right: 1px solid var(--border);
padding: 1.5rem 1rem;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
${host_nav} a {
display: flex;
align-items: center;
padding: 0.625rem 0.75rem;
border-radius: var(--border-radius);
color: var(--text);
text-decoration: none;
transition: background-color 0.2s ease, color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
line-height: 1.2;
}
${host_nav} a:hover {
background-color: rgba(var(--primary-rgb), 0.08); /* Using CSS variables for better theming */
text-decoration: none;
transform: translateY(-1px);
color: var(--primary);
}
${host_nav} a.current,
${host_nav} a[aria-current=page] {
background-color: var(--nav-current-bg, var(--primary-dark));
color: var(--nav-current-text, white);
font-weight: 600;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
}
${host_nav} a.current:hover,
${host_nav} a[aria-current=page]:hover {
background-color: var(--primary);
color: white;
transform: translateY(-1px);
}
${host_nav} a .nav-number {
display: inline-block;
width: 1.5rem;
text-align: right;
margin-right: 0.5rem;
opacity: 0.7;
}
${host_nav} a:first-child {
display: flex;
align-items: center;
font-weight: 600;
margin-bottom: 0.5rem;
}
/* Mobile navigation */
@media (max-width: 767px) {
${host_nav} {
padding: 0.75rem;
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 0.5rem;
border-bottom: 1px solid var(--border);
border-right: none;
justify-content: center;
}
${host_nav} a {
font-size: 0.875rem;
padding: 0.375rem 0.75rem;
white-space: nowrap;
}
${host_nav} a .nav-number {
width: auto;
margin-right: 0.25rem;
}
${host_nav} a:first-child {
margin-bottom: 0;
margin-right: 0.5rem;
min-width: 100%;
justify-content: center;
}
}
`;
/** /**
* @param {object} def * @param {object} def
* @param {import("../types.d.ts").Info} def.info * @param {import("../types.d.ts").Info} def.info
* @param {import("../types.d.ts").Pkg} def.pkg Package information. * @param {import("../types.d.ts").Pkg} def.pkg Package information.
* */ * */
export function header({ info: { href, title, description }, pkg }){ export function header({ info: { href, title, description }, pkg }){
title= `\`${pkg.name}\`${title}`; const pageTitle = `${pkg.name}${title}`;
// Add meta elements to the head
document.head.append( document.head.append(
head({ title, description, pkg }) head({ title: pageTitle, description, pkg })
); );
// Add theme color meta tag
document.head.append(
el("meta", { name: "theme-color", content: "#3f51b5" })
);
return el().append( return el().append(
el("header").append( // Header section with accessibility support
el("h1", title), el("header", { role: "banner", className: header.name }).append(
el("p", description) el("div", { className: "header-title" }).append(
), el("a", {
el("nav").append( href: pkg.homepage,
el("a", { href: pkg.homepage }).append( className: "github-link",
"aria-label": "View on GitHub",
target: "_blank",
rel: "noopener noreferrer"
}).append(
el(iconGitHub), el(iconGitHub),
"GitHub"
), ),
...pages.map((p, i)=> el("a", { el("h1").append(
href: p.href==="index" ? "./" : p.href, el("a", { href: pages[0].href, textContent: pkg.name, title: "Go to documentation homepage" }),
textContent: (i+1) + ". " + p.title, ),
title: p.description, el("span", {
classList: { current: p.href===href } className: "version-badge",
})) "aria-label": "Version",
) textContent: pkg.version || ""
})
),
el("p", description),
),
// Navigation between pages
nav({ href })
);
}
function nav({ href }){
return el("nav", {
role: "navigation",
"aria-label": "Main navigation",
className: nav.name
}).append(
...pages.map((p, i) => {
const isIndex = p.href === "index";
const isCurrent = p.href === href;
return el("a", {
href: isIndex ? "./" : p.href,
title: p.description || `Go to ${p.title}`,
"aria-current": isCurrent ? "page" : null,
classList: { current: isCurrent }
}).append(
el("span", {
className: "nav-number",
"aria-hidden": "true",
textContent: `${i+1}.`
}),
p.title
);
})
); );
} }
function head({ title, description, pkg }){ function head({ title, description, pkg }){
return el().append( return el().append(
el("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), el("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
el("meta", { name: "description", content: description }), el("meta", { name: "description", content: description }),
el("meta", { name: "theme-color", content: "#b71c1c" }),
el("title", title), el("title", title),
el(metaAuthor), el(metaAuthor),
el(metaTwitter, pkg), el(metaTwitter, pkg),

View File

@ -7,9 +7,25 @@ import { prevNext } from "../components/pageUtils.html.js";
/** @param {Pick<import("../types.d.ts").PageAttrs, "pkg" | "info">} attrs */ /** @param {Pick<import("../types.d.ts").PageAttrs, "pkg" | "info">} attrs */
export function simplePage({ pkg, info }){ export function simplePage({ pkg, info }){
return simulateSlots(el().append( return simulateSlots(el().append(
// Skip link for keyboard navigation
el("a", {
href: "#main-content",
className: "skip-link",
textContent: "Skip to main content"
}),
// Header with site information
el(header, { info, pkg }), el(header, { info, pkg }),
el("main").append(
// Main content area
el("main", { id: "main-content", role: "main" }).append(
// Page title as an h1
el("h1", { className: "page-title", textContent: info.title }),
// Main content from child elements
el("slot"), el("slot"),
// Navigation between pages
el(prevNext, info) el(prevNext, info)
) )
)); ));

View File

@ -1,7 +1,7 @@
import { T, t } from "./utils/index.js"; import { T, t } from "./utils/index.js";
export const info= { export const info= {
title: t`Signals and reactivity`, title: t`Signals and reactivity`,
description: t`Handling reactivity in UI via signals.`, description: t`Managing reactive UI state with signals.`,
}; };
import { el } from "deka-dom-el"; import { el } from "deka-dom-el";
@ -69,9 +69,9 @@ export function page({ pkg, info }){
some manner independent and still connected to the same reactive entity. some manner independent and still connected to the same reactive entity.
`), `),
el("p").append(...T` el("p").append(...T`
Signals are implemented in the library as functions. To see current value of signal, just call it without Signals are implemented in the library as objects with methods. To see current value of signal,
any arguments ${el("code", "console.log(signal())")}. To update the signal value, pass any argument call the get method ${el("code", "console.log(signal.get())")}. To update the signal value, use the set method
${el("code", `signal('${t`a new value`}')`)}. For listenning the signal value changes, use ${el("code", `signal.set('${t`a new value`}')`)}. For listenning the signal value changes, use
${el("code", "S.on(signal, console.log)")}. ${el("code", "S.on(signal, console.log)")}.
`), `),
el("p").append(...T` el("p").append(...T`
@ -100,10 +100,10 @@ export function page({ pkg, info }){
`), `),
el(h3, t`Reactive DOM attributes and elements`), el(h3, t`Reactive DOM attributes and elements`),
el("p", t`There are on basic level two distinc situation to mirror dynamic value into the DOM/UI`), el("p", t`There are two fundamental ways to make your DOM reactive with signals:`),
el("ol").append( el("ol").append(
el("li", t`to change some attribute(s) of existing element(s)`), el("li", t`Reactive attributes: Update properties, attributes, and styles of existing elements`),
el("li", t`to generate elements itself dynamically this covers conditions and loops`) el("li", t`Reactive elements: Dynamically create or update DOM elements based on data changes (for conditions and loops)`)
), ),
el(example, { src: fileURL("./components/examples/signals/dom-attrs.js"), page_id }), el(example, { src: fileURL("./components/examples/signals/dom-attrs.js"), page_id }),
el("p").append(...T` el("p").append(...T`
@ -114,7 +114,7 @@ export function page({ pkg, info }){
`), `),
el("p").append(...T` el("p").append(...T`
For computation, you can use the “derived signal” (see above) like For computation, you can use the “derived signal” (see above) like
${el("code", "assign(element, { textContent: S(()=> 'Hello '+WorldSignal()) })")}. This is read-only signal ${el("code", "assign(element, { textContent: S(()=> 'Hello '+WorldSignal.get()) })")}. This is read-only signal
its value is computed based on given function and updated when any signal used in the function changes. its value is computed based on given function and updated when any signal used in the function changes.
`), `),
el("p").append(...T` el("p").append(...T`

View File

@ -15,7 +15,7 @@ export function thirdParty(){
}, { }, {
set(key, value){ set(key, value){
const p= this.value[key] || S(); const p= this.value[key] || S();
p(value); p.set(value);
this.value[key]= p; this.value[key]= p;
} }
}); });
@ -32,7 +32,7 @@ export function thirdParty(){
})(); })();
return el("input", { return el("input", {
className, className,
value: store().value(), value: store.get().value.get(),
type: "text", type: "text",
onchange: ev=> S.action(store, "set", "value", ev.target.value) onchange: ev=> S.action(store, "set", "value", ev.target.value)
}); });

View File

@ -9,24 +9,24 @@ export function fullNameComponent(){
const labels= [ "Name", "Surname" ]; const labels= [ "Name", "Surname" ];
const name= labels.map(_=> S("")); const name= labels.map(_=> S(""));
const full_name= S(()=> const full_name= S(()=>
name.map(l=> l()).filter(Boolean).join(" ") || "-"); name.map(l=> l.get()).filter(Boolean).join(" ") || "-");
scope.host( scope.host(
on.connected(()=> console.log(fullNameComponent)), on.connected(()=> console.log(fullNameComponent)),
on.disconnected(()=> console.log(fullNameComponent)) on.disconnected(()=> console.log(fullNameComponent))
); );
const count= S(0); const count= S(0);
setInterval(()=> count(count()+1), 5000); setInterval(()=> count.set(count.get()+1), 5000);
const style= { height: "80px", display: "block", fill: "currentColor" }; const style= { height: "80px", display: "block", fill: "currentColor" };
const elSVG= elNS("http://www.w3.org/2000/svg"); const elSVG= elNS("http://www.w3.org/2000/svg");
return el("div", { className }).append( return el("div", { className }).append(
el("h2", "Simple form:"), el("h2", "Simple form:"),
el("p", { textContent: S(()=> "Count: "+count()), el("p", { textContent: S(()=> "Count: "+count.get()),
dataset: { count }, classList: { count: S(() => count()%2 === 0) } }), dataset: { count }, classList: { count: S(() => count.get()%2 === 0) } }),
el("form", { onsubmit: ev=> ev.preventDefault() }).append( el("form", { onsubmit: ev=> ev.preventDefault() }).append(
...name.map((v, i)=> ...name.map((v, i)=>
el("label", labels[i]).append( el("label", labels[i]).append(
el("input", { type: "text", name: labels[i], value: v() }, on("change", ev=> v(ev.target.value))) el("input", { type: "text", name: labels[i], value: v.get() }, on("change", ev=> v.set(ev.target.value)))
)) ))
), ),
el("p").append( el("p").append(

View File

@ -58,7 +58,7 @@ export function todosComponent({ todos= [ "Task A" ] }= {}){
), ),
el("div").append( el("div").append(
el("h3", "Output (JSON):"), el("h3", "Output (JSON):"),
el("output", S(()=> JSON.stringify(Array.from(todosO()), null, "\t"))) el("output", S(()=> JSON.stringify(Array.from(todosO.get()), null, "\t")))
) )
) )
} }
@ -80,13 +80,13 @@ function todoComponent({ textContent, value }){
const is_editable= S(false); const is_editable= S(false);
const onedited= on("change", ev=> { const onedited= on("change", ev=> {
const el= /** @type {HTMLInputElement} */ (ev.target); const el= /** @type {HTMLInputElement} */ (ev.target);
textContent(el.value); textContent.set(el.value);
is_editable(false); is_editable.set(false);
}); });
return el("li").append( return el("li").append(
S.el(is_editable, is=> is S.el(is_editable, is=> is
? el("input", { value: textContent(), type: "text" }, onedited) ? el("input", { value: textContent.get(), type: "text" }, onedited)
: el("span", { textContent, onclick: is_editable.bind(null, true) }) : el("span", { textContent, onclick: ()=> is_editable.set(true) })
), ),
el("button", { type: "button", value, textContent: "-" }, onclick) el("button", { type: "button", value, textContent: "-" }, onclick)
); );

View File

@ -34,7 +34,7 @@ export class CustomHTMLTestElement extends HTMLElement{
text(test), text(test),
text(name), text(name),
text(preName), text(preName),
el("button", { type: "button", textContent: "pre-name", onclick: ()=> preName("Ahoj") }), el("button", { type: "button", textContent: "pre-name", onclick: ()=> preName.set("Ahoj") }),
" | ", " | ",
el("slot", { className: "test", name: "test" }), el("slot", { className: "test", name: "test" }),
); );

View File

@ -20,7 +20,7 @@ document.body.append(
el("span", { textContent: "test", slot: "test" }), el("span", { textContent: "test", slot: "test" }),
), ),
el(thirdParty), el(thirdParty),
el(CustomSlottingHTMLElement.tagName, { onclick: ()=> toggle(!toggle()) }).append( el(CustomSlottingHTMLElement.tagName, { onclick: ()=> toggle.set(!toggle.get()) }).append(
el("strong", { slot: "name", textContent: "Honzo" }), el("strong", { slot: "name", textContent: "Honzo" }),
S.el(toggle, is=> is S.el(toggle, is=> is
? el("span", "…default slot") ? el("span", "…default slot")

7
index.d.ts vendored
View File

@ -52,9 +52,12 @@ type IsReadonly<T, K extends keyof T> =
* @private * @private
*/ */
type ElementAttributes<T extends SupportedElement>= Partial<{ type ElementAttributes<T extends SupportedElement>= Partial<{
[K in keyof _fromElsInterfaces<T>]: IsReadonly<_fromElsInterfaces<T>, K> extends false [K in keyof _fromElsInterfaces<T>]:
_fromElsInterfaces<T>[K] extends ((...p: any[])=> any)
? _fromElsInterfaces<T>[K] | ((...p: Parameters<_fromElsInterfaces<T>[K]>)=> ddeSignal<ReturnType<_fromElsInterfaces<T>[K]>>)
: (IsReadonly<_fromElsInterfaces<T>, K> extends false
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]> ? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
: ddeStringable : ddeStringable)
} & AttrsModified> & Record<string, any>; } & AttrsModified> & Record<string, any>;
export function classListDeclarative<El extends SupportedElement>( export function classListDeclarative<El extends SupportedElement>(
element: El, element: El,

View File

@ -29,8 +29,8 @@
"types": "./jsdom.d.ts" "types": "./jsdom.d.ts"
}, },
"./src/signals-lib": { "./src/signals-lib": {
"import": "./src/signals-lib.js", "import": "./src/signals-lib/signals-lib.js",
"types": "./src/signals-lib.d.ts" "types": "./src/signals-lib/signals-lib.d.ts"
} }
}, },
"files": [ "files": [
@ -53,7 +53,8 @@
"globals": { "globals": {
"requestIdleCallback": false, "requestIdleCallback": false,
"AbortController": false, "AbortController": false,
"AbortSignal": false "AbortSignal": false,
"FinalizationRegistry": false
} }
}, },
"size-limit": [ "size-limit": [
@ -65,7 +66,7 @@
}, },
{ {
"path": "./signals.js", "path": "./signals.js",
"limit": "12 kB", "limit": "12.5 kB",
"gzip": false, "gzip": false,
"brotli": false "brotli": false
}, },
@ -77,7 +78,7 @@
}, },
{ {
"path": "./index-with-signals.js", "path": "./index-with-signals.js",
"limit": "5.25 kB" "limit": "5.5 kB"
} }
], ],
"modifyEsbuildConfig": { "modifyEsbuildConfig": {

12
signals.d.ts vendored
View File

@ -1,4 +1,12 @@
export type Signal<V, A>= (set?: V)=> V & A; export interface Signal<V, A> {
/** The current value of the signal */
get(): V;
/** Set new value of the signal */
set(value: V): V;
toJSON(): V;
valueOf(): V;
}
type Action<V>= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof signal._ | void; type Action<V>= (this: { value: V, stopPropagation(): void }, ...a: any[])=> typeof signal._ | void;
//type SymbolSignal= Symbol; //type SymbolSignal= Symbol;
type SymbolOnclear= symbol; type SymbolOnclear= symbol;
@ -27,7 +35,7 @@ interface signal{
* ```js * ```js
* const name= S("Jan"); * const name= S("Jan");
* const surname= S("Andrle"); * const surname= S("Andrle");
* const fullname= S(()=> name()+" "+surname()); * const fullname= S(()=> name.get()+" "+surname.get());
* ``` * ```
* @param value Initial signal value. Or function computing value from other signals. * @param value Initial signal value. Or function computing value from other signals.
* @param actions Use to define actions on the signal. Such as add item to the array. * @param actions Use to define actions on the signal. Such as add item to the array.

View File

@ -1,4 +1,4 @@
export { signal, S, isSignal } from "./src/signals-lib.js"; export { signal, S, isSignal } from "./src/signals-lib/signals-lib.js";
import { signals_config } from "./src/signals-lib.js"; import { signals_config } from "./src/signals-lib/signals-lib.js";
import { registerReactivity } from "./src/signals-common.js"; import { registerReactivity } from "./src/signals-lib/common.js";
registerReactivity(signals_config); registerReactivity(signals_config);

View File

@ -1,6 +1,15 @@
import { keyLTE, evc, evd, eva } from "./dom-common.js"; import { keyLTE, evc, evd, eva } from "./dom-common.js";
import { scope } from "./dom.js"; import { scope } from "./dom.js";
import { c_ch_o } from "./events-observer.js"; import { c_ch_o } from "./events-observer.js";
/**
* Renders content into a custom element or shadow root
*
* @param {Element|ShadowRoot} target - The custom element or shadow root to render into
* @param {Function} render - The render function that returns content
* @param {Function|Object} [props=observedAttributes] - Props to pass to the render function
* @returns {Node} The rendered content
*/
export function customElementRender(target, render, props= observedAttributes){ export function customElementRender(target, render, props= observedAttributes){
const custom_element= target.host || target; const custom_element= target.host || target;
scope.push({ scope.push({
@ -17,6 +26,13 @@ export function customElementRender(target, render, props= observedAttributes){
scope.pop(); scope.pop();
return target.append(out); return target.append(out);
} }
/**
* Transforms custom element lifecycle callbacks into events
*
* @param {Function|Object} class_declaration - Custom element class or instance
* @returns {Function|Object} The modified class or instance
*/
export function lifecyclesToEvents(class_declaration){ export function lifecyclesToEvents(class_declaration){
wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail){ wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail){
target.apply(thisArg, detail); target.apply(thisArg, detail);
@ -38,12 +54,30 @@ export function lifecyclesToEvents(class_declaration){
class_declaration.prototype[keyLTE]= true; class_declaration.prototype[keyLTE]= true;
return class_declaration; return class_declaration;
} }
/** Public API */
export { lifecyclesToEvents as customElementWithDDE }; export { lifecyclesToEvents as customElementWithDDE };
/**
* Wraps a method with a proxy to intercept calls
*
* @param {Object} obj - Object containing the method
* @param {string} method - Method name to wrap
* @param {Function} apply - Function to execute when method is called
* @private
*/
function wrapMethod(obj, method, apply){ function wrapMethod(obj, method, apply){
obj[method]= new Proxy(obj[method] || (()=> {}), { apply }); obj[method]= new Proxy(obj[method] || (()=> {}), { apply });
} }
import { observedAttributes as oA } from "./helpers.js"; import { observedAttributes as oA } from "./helpers.js";
/**
* Gets observed attributes for a custom element
*
* @param {Element} instance - Custom element instance
* @returns {Object} Object mapping camelCase attribute names to their values
*/
export function observedAttributes(instance){ export function observedAttributes(instance){
return oA(instance, (i, n)=> i.getAttribute(n)); return oA(instance, (i, n)=> i.getAttribute(n));
} }

View File

@ -1,3 +1,15 @@
/**
* Environment configuration and globals for the library
* @typedef {Object} Environment
* @property {typeof setDeleteAttr} setDeleteAttr - Function to safely set or delete attributes
* @property {string} ssr - Server-side rendering flag
* @property {Document} D - Document global
* @property {typeof DocumentFragment} F - DocumentFragment constructor
* @property {typeof HTMLElement} H - HTMLElement constructor
* @property {typeof SVGElement} S - SVGElement constructor
* @property {typeof MutationObserver} M - MutationObserver constructor
* @property {Function} q - Promise wrapper for Promse queue feature
*/
export const enviroment= { export const enviroment= {
setDeleteAttr, setDeleteAttr,
ssr: "", ssr: "",
@ -8,26 +20,43 @@ export const enviroment= {
M: globalThis.MutationObserver, M: globalThis.MutationObserver,
q: p=> p || Promise.resolve(), q: p=> p || Promise.resolve(),
}; };
import { isUndef } from './helpers.js'; import { isInstance, isUndef } from './helpers.js';
function setDeleteAttr(obj, prop, val){
/* Issue /**
For some native attrs you can unset only to set empty string. * Handles attribute setting with special undefined handling
This can be confusing as it is seen in inspector `<… id=""`. *
Options: * @param {Object} obj - The object to set the property on
1. Leave it, as it is native behaviour * @param {string} prop - The property name
2. Sets as empty string and removes the corresponding attribute when also has empty string * @param {any} val - The value to set
3. (*) Sets as undefined and removes the corresponding attribute when "undefined" string discovered * @returns {void}
4. Point 2. with checks for coincidence (e.g. use special string) *
* Issue:
* For some native attrs you can unset only to set empty string.
* This can be confusing as it is seen in inspector `<… id=""`.
* Options:
* 1. Leave it, as it is native behaviour
* 2. Sets as empty string and removes the corresponding attribute when also has empty string
* 3. (*) Sets as undefined and removes the corresponding attribute when "undefined" string discovered
* 4. Point 2. with checks for coincidence (e.g. use special string)
*/ */
function setDeleteAttr(obj, prop, val){
Reflect.set(obj, prop, val); Reflect.set(obj, prop, val);
if(!isUndef(val)) return; if(!isUndef(val)) return;
Reflect.deleteProperty(obj, prop); Reflect.deleteProperty(obj, prop);
if(obj instanceof enviroment.H && obj.getAttribute(prop)==="undefined") if(isInstance(obj, enviroment.H) && obj.getAttribute(prop)==="undefined")
return obj.removeAttribute(prop); return obj.removeAttribute(prop);
if(Reflect.get(obj, prop)==="undefined") if(Reflect.get(obj, prop)==="undefined")
return Reflect.set(obj, prop, ""); return Reflect.set(obj, prop, "");
} }
/** Property key for tracking lifecycle events */
export const keyLTE= "__dde_lifecyclesToEvents"; //boolean export const keyLTE= "__dde_lifecyclesToEvents"; //boolean
/** Event name for connected lifecycle event */
export const evc= "dde:connected"; export const evc= "dde:connected";
/** Event name for disconnected lifecycle event */
export const evd= "dde:disconnected"; export const evd= "dde:disconnected";
/** Event name for attribute changed lifecycle event */
export const eva= "dde:attributeChanged"; export const eva= "dde:attributeChanged";

View File

@ -1,38 +1,104 @@
import { signals } from "./signals-common.js"; import { signals } from "./signals-lib/common.js";
import { enviroment as env } from './dom-common.js'; import { enviroment as env } from './dom-common.js';
import { isInstance, isUndef, oAssign } from "./helpers.js";
//TODO: add type, docs ≡ make it public /**
* Queues a promise, this is helpful for crossplatform components (on server side we can wait for all registered
* promises to be resolved before rendering).
* @param {Promise} promise - Promise to process
* @returns {Promise} Processed promise
*/
export function queue(promise){ return env.q(promise); } export function queue(promise){ return env.q(promise); }
/** @type {{ scope: object, prevent: boolean, host: function }[]} */
/**
* Array of scope contexts for tracking component hierarchies
* @type {{ scope: object, prevent: boolean, host: function }[]}
*/
const scopes= [ { const scopes= [ {
get scope(){ return env.D.body; }, get scope(){ return env.D.body; },
host: c=> c ? c(env.D.body) : env.D.body, host: c=> c ? c(env.D.body) : env.D.body,
prevent: true, prevent: true,
} ]; } ];
/**
* Scope management utility for tracking component hierarchies
*/
export const scope= { export const scope= {
/**
* Gets the current scope
* @returns {Object} Current scope context
*/
get current(){ return scopes[scopes.length-1]; }, get current(){ return scopes[scopes.length-1]; },
/**
* Gets the host element of the current scope
* @returns {Function} Host accessor function
*/
get host(){ return this.current.host; }, get host(){ return this.current.host; },
/**
* Prevents default behavior in the current scope
* @returns {Object} Current scope context
*/
preventDefault(){ preventDefault(){
const { current }= this; const { current }= this;
current.prevent= true; current.prevent= true;
return current; return current;
}, },
/**
* Gets a copy of the current scope stack
* @returns {Array} Copy of scope stack
*/
get state(){ return [ ...scopes ]; }, get state(){ return [ ...scopes ]; },
push(s= {}){ return scopes.push(Object.assign({}, this.current, { prevent: false }, s)); },
/**
* Pushes a new scope to the stack
* @param {Object} [s={}] - Scope object to push
* @returns {number} New length of the scope stack
*/
push(s= {}){ return scopes.push(oAssign({}, this.current, { prevent: false }, s)); },
/**
* Pushes the root scope to the stack
* @returns {number} New length of the scope stack
*/
pushRoot(){ return scopes.push(scopes[0]); }, pushRoot(){ return scopes.push(scopes[0]); },
/**
* Pops the current scope from the stack
* @returns {Object|undefined} Popped scope or undefined if only one scope remains
*/
pop(){ pop(){
if(scopes.length===1) return; if(scopes.length===1) return;
return scopes.pop(); return scopes.pop();
}, },
}; };
//NOTE: following chainableAppend implementation is OK as the ElementPrototype.append description already is { writable: true, enumerable: true, configurable: true } // editorconfig-checker-disable-line /**
* Chainable append function for elements
* @private
*/
function append(...els){ this.appendOriginal(...els); return this; } function append(...els){ this.appendOriginal(...els); return this; }
/**
* Makes an element's append method chainable. NOTE: following chainableAppend implementation is OK as the
* ElementPrototype.append description already is { writable: true, enumerable: true, configurable: true }
* @param {Element} el - Element to modify
* @returns {Element} Modified element
*/
export function chainableAppend(el){ export function chainableAppend(el){
if(el.append===append) return el; el.appendOriginal= el.append; el.append= append; return el; if(el.append===append) return el; el.appendOriginal= el.append; el.append= append; return el;
} }
/** Current namespace for element creation */
let namespace; let namespace;
/**
* Creates a DOM element with specified tag, attributes and addons
*
* @param {string|Function} tag - Element tag name or component function
* @param {Object|string|number} [attributes] - Element attributes
* @param {...Function} addons - Functions to call with the created element
* @returns {Element|DocumentFragment} Created element
*/
export function createElement(tag, attributes, ...addons){ export function createElement(tag, attributes, ...addons){
/* jshint maxcomplexity: 15 */ /* jshint maxcomplexity: 15 */
const s= signals(this); const s= signals(this);
@ -47,7 +113,7 @@ export function createElement(tag, attributes, ...addons){
(scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined); (scoped===1 ? addons.unshift(...c) : c.forEach(c=> c(el_host)), undefined);
scope.push({ scope: tag, host }); scope.push({ scope: tag, host });
el= tag(attributes || undefined); el= tag(attributes || undefined);
const is_fragment= el instanceof env.F; const is_fragment= isInstance(el, env.F);
if(el.nodeName==="#comment") break; if(el.nodeName==="#comment") break;
const el_mark= createElement.mark({ const el_mark= createElement.mark({
type: "component", type: "component",
@ -70,10 +136,15 @@ export function createElement(tag, attributes, ...addons){
scoped= 2; scoped= 2;
return el; return el;
} }
/** /**
* @param { { type: "component", name: string, host: "this" | "parentElement" } | { type: "reactive" | "later" } } attrs * Creates a marker comment for elements
* @param {boolean} [is_open=false] *
* */ * @param {{ type: "component"|"reactive"|"later", name?: string, host?: "this"|"parentElement" }} attrs - Marker
* attributes
* @param {boolean} [is_open=false] - Whether the marker is open-ended
* @returns {Comment} Comment node marker
*/
createElement.mark= function(attrs, is_open= false){ createElement.mark= function(attrs, is_open= false){
attrs= Object.entries(attrs).map(([ n, v ])=> n+`="${v}"`).join(" "); attrs= Object.entries(attrs).map(([ n, v ])=> n+`="${v}"`).join(" ");
const end= is_open ? "" : "/"; const end= is_open ? "" : "/";
@ -81,8 +152,16 @@ createElement.mark= function(attrs, is_open= false){
if(is_open) out.end= env.D.createComment("</dde:mark>"); if(is_open) out.end= env.D.createComment("</dde:mark>");
return out; return out;
}; };
/** Alias for createElement */
export { createElement as el }; export { createElement as el };
//TODO?: const namespaceHelper= ns=> ns==="svg" ? "http://www.w3.org/2000/svg" : ns; //TODO?: const namespaceHelper= ns=> ns==="svg" ? "http://www.w3.org/2000/svg" : ns;
/**
* Creates a namespaced element creation function
*
* @param {string} ns - Namespace URI
* @returns {Function} Element creation function for the namespace
*/
export function createElementNS(ns){ export function createElementNS(ns){
const _this= this; const _this= this;
return function createElementNSCurried(...rest){ return function createElementNSCurried(...rest){
@ -92,9 +171,17 @@ export function createElementNS(ns){
return el; return el;
}; };
} }
/** Alias for createElementNS */
export { createElementNS as elNS }; export { createElementNS as elNS };
/** @param {HTMLElement} element @param {HTMLElement} [root] */ /**
* Simulates slot functionality for elements
*
* @param {HTMLElement} element - Parent element
* @param {HTMLElement} [root=element] - Root element containing slots
* @returns {HTMLElement} The root element
*/
export function simulateSlots(element, root= element){ export function simulateSlots(element, root= element){
const mark_e= "¹⁰", mark_s= "✓"; //NOTE: Markers to identify slots processed by this function. Also “prevents” native behavior as it is unlikely to use these in names. // editorconfig-checker-disable-line const mark_e= "¹⁰", mark_s= "✓"; //NOTE: Markers to identify slots processed by this function. Also “prevents” native behavior as it is unlikely to use these in names. // editorconfig-checker-disable-line
const slots= Object.fromEntries( const slots= Object.fromEntries(
@ -128,17 +215,34 @@ export function simulateSlots(element, root= element){
return root; return root;
} }
/** Store for element assignment contexts */
const assign_context= new WeakMap(); const assign_context= new WeakMap();
const { setDeleteAttr }= env; const { setDeleteAttr }= env;
/**
* Assigns attributes to an element
*
* @param {Element} element - Element to assign attributes to
* @param {...Object} attributes - Attribute objects to assign
* @returns {Element} The element with attributes assigned
*/
export function assign(element, ...attributes){ export function assign(element, ...attributes){
if(!attributes.length) return element; if(!attributes.length) return element;
assign_context.set(element, assignContext(element, this)); assign_context.set(element, assignContext(element, this));
for(const [ key, value ] of Object.entries(Object.assign({}, ...attributes))) for(const [ key, value ] of Object.entries(oAssign({}, ...attributes)))
assignAttribute.call(this, element, key, value); assignAttribute.call(this, element, key, value);
assign_context.delete(element); assign_context.delete(element);
return element; return element;
} }
/**
* Assigns a single attribute to an element
*
* @param {Element} element - Element to assign attribute to
* @param {string} key - Attribute name
* @param {any} value - Attribute value
* @returns {any} Result of the attribute assignment
*/
export function assignAttribute(element, key, value){ export function assignAttribute(element, key, value){
const { setRemoveAttr, s }= assignContext(element, this); const { setRemoveAttr, s }= assignContext(element, this);
const _this= this; const _this= this;
@ -170,13 +274,28 @@ export function assignAttribute(element, key, value){
} }
return isPropSetter(element, key) ? setDeleteAttr(element, key, value) : setRemoveAttr(key, value); return isPropSetter(element, key) ? setDeleteAttr(element, key, value) : setRemoveAttr(key, value);
} }
/**
* Gets or creates assignment context for an element
*
* @param {Element} element - Element to get context for
* @param {Object} _this - Context object
* @returns {Object} Assignment context
* @private
*/
function assignContext(element, _this){ function assignContext(element, _this){
if(assign_context.has(element)) return assign_context.get(element); if(assign_context.has(element)) return assign_context.get(element);
const is_svg= element instanceof env.S; const is_svg= isInstance(element, env.S);
const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute"); const setRemoveAttr= (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
const s= signals(_this); const s= signals(_this);
return { setRemoveAttr, s }; return { setRemoveAttr, s };
} }
/**
* Applies a declarative classList object to an element
*
* @param {Element} element - Element to apply classes to
* @param {Object} toggle - Object with class names as keys and boolean values
* @returns {Element} The element with classes applied
*/
export function classListDeclarative(element, toggle){ export function classListDeclarative(element, toggle){
const s= signals(this); const s= signals(this);
forEachEntries(s, "classList", element, toggle, forEachEntries(s, "classList", element, toggle,
@ -184,18 +303,45 @@ export function classListDeclarative(element, toggle){
element.classList.toggle(class_name, val===-1 ? undefined : Boolean(val)) ); element.classList.toggle(class_name, val===-1 ? undefined : Boolean(val)) );
return element; return element;
} }
/**
* Generic element attribute manipulation
*
* @param {Element} element - Element to manipulate
* @param {string} op - Operation ("set" or "remove")
* @param {string} key - Attribute name
* @param {any} [value] - Attribute value
* @returns {void}
*/
export function elementAttribute(element, op, key, value){ export function elementAttribute(element, op, key, value){
if(element instanceof env.H) if(isInstance(element, env.H))
return element[op+"Attribute"](key, value); return element[op+"Attribute"](key, value);
return element[op+"AttributeNS"](null, key, value); return element[op+"AttributeNS"](null, key, value);
} }
import { isUndef } from "./helpers.js";
//TODO: add cache? `(Map/Set)<el.tagName+key,isUndef>` //TODO: add cache? `(Map/Set)<el.tagName+key,isUndef>`
/**
* Checks if a property can be set on an element
*
* @param {Element} el - Element to check
* @param {string} key - Property name
* @returns {boolean} Whether the property can be set
* @private
*/
function isPropSetter(el, key){ function isPropSetter(el, key){
if(!(key in el)) return false; if(!(key in el)) return false;
const des= getPropDescriptor(el, key); const des= getPropDescriptor(el, key);
return !isUndef(des.set); return !isUndef(des.set);
} }
/**
* Gets a property descriptor from a prototype chain
*
* @param {Object} p - Prototype object
* @param {string} key - Property name
* @returns {PropertyDescriptor} Property descriptor
* @private
*/
function getPropDescriptor(p, key){ function getPropDescriptor(p, key){
p= Object.getPrototypeOf(p); p= Object.getPrototypeOf(p);
if(!p) return {}; if(!p) return {};
@ -224,9 +370,44 @@ function forEachEntries(s, target, element, obj, cb){
}); });
} }
/**
* Sets or removes an attribute based on value
*
* @param {Element} obj - Element to modify
* @param {string} prop - Property suffix ("Attribute")
* @param {string} key - Attribute name
* @param {any} val - Attribute value
* @returns {void}
* @private
*/
function setRemove(obj, prop, key, val){ function setRemove(obj, prop, key, val){
return obj[ (isUndef(val) ? "remove" : "set") + prop ](key, val); } return obj[ (isUndef(val) ? "remove" : "set") + prop ](key, val);
}
/**
* Sets or removes a namespaced attribute based on value
*
* @param {Element} obj - Element to modify
* @param {string} prop - Property suffix ("Attribute")
* @param {string} key - Attribute name
* @param {any} val - Attribute value
* @param {string|null} [ns=null] - Namespace URI
* @returns {void}
* @private
*/
function setRemoveNS(obj, prop, key, val, ns= null){ function setRemoveNS(obj, prop, key, val, ns= null){
return obj[ (isUndef(val) ? "remove" : "set") + prop + "NS" ](ns, key, val); } return obj[ (isUndef(val) ? "remove" : "set") + prop + "NS" ](ns, key, val);
}
/**
* Sets or deletes a property based on value
*
* @param {Object} obj - Object to modify
* @param {string} key - Property name
* @param {any} val - Property value
* @returns {void}
* @private
*/
function setDelete(obj, key, val){ function setDelete(obj, key, val){
Reflect.set(obj, key, val); if(!isUndef(val)) return; return Reflect.deleteProperty(obj, key); } Reflect.set(obj, key, val); if(!isUndef(val)) return; return Reflect.deleteProperty(obj, key);
}

View File

@ -1,11 +1,27 @@
import { enviroment as env, evc, evd } from './dom-common.js'; import { enviroment as env, evc, evd } from './dom-common.js';
import { isInstance } from "./helpers.js";
/**
* Connection changes observer for tracking element connection/disconnection
* Falls back to a dummy implementation if MutationObserver is not available
*/
export const c_ch_o= env.M ? connectionsChangesObserverConstructor() : new Proxy({}, { export const c_ch_o= env.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
get(){ return ()=> {}; } get(){ return ()=> {}; }
}); });
/**
* Creates an observer that tracks elements being connected to and disconnected from the DOM
* @returns {Object} Observer with methods to register element listeners
*/
function connectionsChangesObserverConstructor(){ function connectionsChangesObserverConstructor(){
const store= new Map(); const store= new Map();
let is_observing= false; let is_observing= false;
/**
* Creates a mutation observer callback
* @param {Function} stop - Function to stop observation when no longer needed
* @returns {Function} MutationObserver callback
*/
const observerListener= stop=> function(mutations){ const observerListener= stop=> function(mutations){
for(const mutation of mutations){ for(const mutation of mutations){
if(mutation.type!=="childList") continue; if(mutation.type!=="childList") continue;
@ -17,13 +33,26 @@ function connectionsChangesObserverConstructor(){
stop(); stop();
} }
}; };
const observer= new env.M(observerListener(stop)); const observer= new env.M(observerListener(stop));
return { return {
/**
* Creates an observer for a specific element
* @param {Element} element - Element to observe
* @returns {Function} Cleanup function
*/
observe(element){ observe(element){
const o= new env.M(observerListener(()=> {})); const o= new env.M(observerListener(()=> {}));
o.observe(element, { childList: true, subtree: true }); o.observe(element, { childList: true, subtree: true });
return ()=> o.disconnect(); return ()=> o.disconnect();
}, },
/**
* Register a connection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for connection event
*/
onConnected(element, listener){ onConnected(element, listener){
start(); start();
const listeners= getElementStore(element); const listeners= getElementStore(element);
@ -31,6 +60,12 @@ function connectionsChangesObserverConstructor(){
listeners.connected.add(listener); listeners.connected.add(listener);
listeners.length_c+= 1; listeners.length_c+= 1;
}, },
/**
* Unregister a connection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offConnected(element, listener){ offConnected(element, listener){
if(!store.has(element)) return; if(!store.has(element)) return;
const ls= store.get(element); const ls= store.get(element);
@ -39,6 +74,12 @@ function connectionsChangesObserverConstructor(){
ls.length_c-= 1; ls.length_c-= 1;
cleanWhenOff(element, ls); cleanWhenOff(element, ls);
}, },
/**
* Register a disconnection listener for an element
* @param {Element} element - Element to watch
* @param {Function} listener - Callback for disconnection event
*/
onDisconnected(element, listener){ onDisconnected(element, listener){
start(); start();
const listeners= getElementStore(element); const listeners= getElementStore(element);
@ -46,6 +87,12 @@ function connectionsChangesObserverConstructor(){
listeners.disconnected.add(listener); listeners.disconnected.add(listener);
listeners.length_d+= 1; listeners.length_d+= 1;
}, },
/**
* Unregister a disconnection listener
* @param {Element} element - Element being watched
* @param {Function} listener - Callback to remove
*/
offDisconnected(element, listener){ offDisconnected(element, listener){
if(!store.has(element)) return; if(!store.has(element)) return;
const ls= store.get(element); const ls= store.get(element);
@ -55,12 +102,24 @@ function connectionsChangesObserverConstructor(){
cleanWhenOff(element, ls); cleanWhenOff(element, ls);
} }
}; };
/**
* Cleanup element tracking when all listeners are removed
* @param {Element} element - Element to potentially remove from tracking
* @param {Object} ls - Element's listener store
*/
function cleanWhenOff(element, ls){ function cleanWhenOff(element, ls){
if(ls.length_c || ls.length_d) if(ls.length_c || ls.length_d)
return; return;
store.delete(element); store.delete(element);
stop(); stop();
} }
/**
* Gets or creates a store for element listeners
* @param {Element} element - Element to get store for
* @returns {Object} Listener store for the element
*/
function getElementStore(element){ function getElementStore(element){
if(store.has(element)) return store.get(element); if(store.has(element)) return store.get(element);
const out= { const out= {
@ -72,32 +131,58 @@ function connectionsChangesObserverConstructor(){
store.set(element, out); store.set(element, out);
return out; return out;
} }
/**
* Start observing DOM changes
*/
function start(){ function start(){
if(is_observing) return; if(is_observing) return;
is_observing= true; is_observing= true;
observer.observe(env.D.body, { childList: true, subtree: true }); observer.observe(env.D.body, { childList: true, subtree: true });
} }
/**
* Stop observing DOM changes when no longer needed
*/
function stop(){ function stop(){
if(!is_observing || store.size) return; if(!is_observing || store.size) return;
is_observing= false; is_observing= false;
observer.disconnect(); observer.disconnect();
} }
//TODO: remount support? //TODO: remount support?
/**
* Schedule a task during browser idle time
* @returns {Promise<void>} Promise that resolves when browser is idle
*/
function requestIdle(){ return new Promise(function(resolve){ function requestIdle(){ return new Promise(function(resolve){
(requestIdleCallback || requestAnimationFrame)(resolve); (requestIdleCallback || requestAnimationFrame)(resolve);
}); } }); }
/**
* Collects child elements from the store that are contained by the given element
* @param {Element} element - Parent element
* @returns {Promise<Element[]>} Promise resolving to array of child elements
*/
async function collectChildren(element){ async function collectChildren(element){
if(store.size > 30)//TODO?: limit if(store.size > 30)//TODO?: limit
await requestIdle(); await requestIdle();
const out= []; const out= [];
if(!(element instanceof Node)) return out; if(!isInstance(element, Node)) return out;
for(const el of store.keys()){ for(const el of store.keys()){
if(el===element || !(el instanceof Node)) continue; if(el===element || !isInstance(el, Node)) continue;
if(element.contains(el)) if(element.contains(el))
out.push(el); out.push(el);
} }
return out; return out;
} }
/**
* Process nodes added to the DOM
* @param {NodeList} addedNodes - Nodes that were added
* @param {boolean} is_root - Whether these are root-level additions
* @returns {boolean} Whether any relevant elements were processed
*/
function observerAdded(addedNodes, is_root){ function observerAdded(addedNodes, is_root){
let out= false; let out= false;
for(const element of addedNodes){ for(const element of addedNodes){
@ -115,6 +200,13 @@ function connectionsChangesObserverConstructor(){
} }
return out; return out;
} }
/**
* Process nodes removed from the DOM
* @param {NodeList} removedNodes - Nodes that were removed
* @param {boolean} is_root - Whether these are root-level removals
* @returns {boolean} Whether any relevant elements were processed
*/
function observerRemoved(removedNodes, is_root){ function observerRemoved(removedNodes, is_root){
let out= false; let out= false;
for(const element of removedNodes){ for(const element of removedNodes){
@ -128,6 +220,12 @@ function connectionsChangesObserverConstructor(){
} }
return out; return out;
} }
/**
* Creates a function to dispatch the disconnect event
* @param {Element} element - Element that was removed
* @returns {Function} Function to dispatch event after confirming disconnection
*/
function dispatchRemove(element){ function dispatchRemove(element){
return ()=> { return ()=> {
if(element.isConnected) return; if(element.isConnected) return;

View File

@ -1,6 +1,15 @@
export { registerReactivity } from './signals-common.js'; export { registerReactivity } from './signals-lib/common.js';
import { enviroment as env, keyLTE, evc, evd, eva } from './dom-common.js'; import { enviroment as env, keyLTE, evc, evd, eva } from './dom-common.js';
import { oAssign, onAbort } from './helpers.js';
/**
* Creates a function to dispatch events on elements
*
* @param {string} name - Event name
* @param {Object} [options] - Event options
* @param {Element|Function} [host] - Host element or function returning host element
* @returns {Function} Function that dispatches the event
*/
export function dispatchEvent(name, options, host){ export function dispatchEvent(name, options, host){
if(!options) options= {}; if(!options) options= {};
return function dispatch(element, ...d){ return function dispatch(element, ...d){
@ -9,10 +18,19 @@ export function dispatchEvent(name, options, host){
element= typeof host==="function"? host() : host; element= typeof host==="function"? host() : host;
} }
//TODO: what about re-emmitting? //TODO: what about re-emmitting?
const event= d.length ? new CustomEvent(name, Object.assign({ detail: d[0] }, options)) : new Event(name, options); const event= d.length ? new CustomEvent(name, oAssign({ detail: d[0] }, options)) : new Event(name, options);
return element.dispatchEvent(event); return element.dispatchEvent(event);
}; };
} }
/**
* Creates a function to register event listeners on elements
*
* @param {string} event - Event name
* @param {Function} listener - Event handler
* @param {Object} [options] - Event listener options
* @returns {Function} Function that registers the listener
*/
export function on(event, listener, options){ export function on(event, listener, options){
return function registerElement(element){ return function registerElement(element){
element.addEventListener(event, listener, options); element.addEventListener(event, listener, options);
@ -21,10 +39,23 @@ export function on(event, listener, options){
} }
import { c_ch_o } from "./events-observer.js"; import { c_ch_o } from "./events-observer.js";
import { onAbort } from './helpers.js';
const lifeOptions= obj=> Object.assign({}, typeof obj==="object" ? obj : null, { once: true }); /**
* Prepares lifecycle event options with once:true default
* @private
*/
const lifeOptions= obj=> oAssign({}, typeof obj==="object" ? obj : null, { once: true });
//TODO: cleanUp when event before abort? //TODO: cleanUp when event before abort?
//TODO: docs (e.g.) https://nolanlawson.com/2024/01/13/web-component-gotcha-constructor-vs-connectedcallback/ //TODO: docs (e.g.) https://nolanlawson.com/2024/01/13/web-component-gotcha-constructor-vs-connectedcallback/
/**
* Creates a function to register connected lifecycle event listeners
*
* @param {Function} listener - Event handler
* @param {Object} [options] - Event listener options
* @returns {Function} Function that registers the connected listener
*/
on.connected= function(listener, options){ on.connected= function(listener, options){
options= lifeOptions(options); options= lifeOptions(options);
return function registerElement(element){ return function registerElement(element){
@ -37,6 +68,14 @@ on.connected= function(listener, options){
return element; return element;
}; };
}; };
/**
* Creates a function to register disconnected lifecycle event listeners
*
* @param {Function} listener - Event handler
* @param {Object} [options] - Event listener options
* @returns {Function} Function that registers the disconnected listener
*/
on.disconnected= function(listener, options){ on.disconnected= function(listener, options){
options= lifeOptions(options); options= lifeOptions(options);
return function registerElement(element){ return function registerElement(element){
@ -48,7 +87,16 @@ on.disconnected= function(listener, options){
return element; return element;
}; };
}; };
/** Store for disconnect abort controllers */
const store_abort= new WeakMap(); const store_abort= new WeakMap();
/**
* Creates an AbortController that triggers when the element disconnects
*
* @param {Element|Function} host - Host element or function taking an element
* @returns {AbortController} AbortController that aborts on disconnect
*/
on.disconnectedAsAbort= function(host){ on.disconnectedAsAbort= function(host){
if(store_abort.has(host)) return store_abort.get(host); if(store_abort.has(host)) return store_abort.get(host);
@ -57,7 +105,17 @@ on.disconnectedAsAbort= function(host){
host(on.disconnected(()=> a.abort())); host(on.disconnected(()=> a.abort()));
return a; return a;
}; };
/** Store for elements with attribute observers */
const els_attribute_store= new WeakSet(); const els_attribute_store= new WeakSet();
/**
* Creates a function to register attribute change event listeners
*
* @param {Function} listener - Event handler
* @param {Object} [options] - Event listener options
* @returns {Function} Function that registers the attribute change listener
*/
on.attributeChanged= function(listener, options){ on.attributeChanged= function(listener, options){
if(typeof options !== "object") if(typeof options !== "object")
options= {}; options= {};

View File

@ -1,13 +1,43 @@
/**
* Safe method to check if an object has a specific property
* @param {...any} a - Arguments to pass to Object.prototype.hasOwnProperty.call
* @returns {boolean} Result of hasOwnProperty check
*/
export const hasOwn= (...a)=> Object.prototype.hasOwnProperty.call(...a); export const hasOwn= (...a)=> Object.prototype.hasOwnProperty.call(...a);
/**
* Checks if a value is undefined
* @param {any} value - The value to check
* @returns {boolean} True if the value is undefined
*/
export function isUndef(value){ return typeof value==="undefined"; } export function isUndef(value){ return typeof value==="undefined"; }
/**
* Enhanced typeof that handles null and objects better
* @param {any} v - The value to check
* @returns {string} Type as a string
*/
export function typeOf(v){ export function typeOf(v){
const t= typeof v; const t= typeof v;
if(t!=="object") return t; if(t!=="object") return t;
if(v===null) return "null"; if(v===null) return "null";
return Object.prototype.toString.call(v); return Object.prototype.toString.call(v);
} }
export function isInstance(obj, cls){ return obj instanceof cls; }
/** @type {typeof Object.prototype.isPrototypeOf.call} */
export function isProtoFrom(obj, cls){ return Object.prototype.isPrototypeOf.call(cls, obj); }
export function oCreate(proto= null, p= {}){ return Object.create(proto, p); }
export function oAssign(...o){ return Object.assign(...o); }
/**
* Handles AbortSignal registration and cleanup
* @param {AbortSignal} signal - The AbortSignal to listen to
* @param {Function} listener - The abort event listener
* @returns {Function|undefined|boolean} Cleanup function or undefined if already aborted
*/
export function onAbort(signal, listener){ export function onAbort(signal, listener){
if(!signal || !(signal instanceof AbortSignal)) if(!signal || !isInstance(signal, AbortSignal))
return true; return true;
if(signal.aborted) if(signal.aborted)
return; return;
@ -16,6 +46,13 @@ export function onAbort(signal, listener){
signal.removeEventListener("abort", listener); signal.removeEventListener("abort", listener);
}; };
} }
/**
* Processes observed attributes for custom elements
* @param {object} instance - The custom element instance
* @param {Function} observedAttribute - Function to process each attribute
* @returns {object} Object with processed attributes
*/
export function observedAttributes(instance, observedAttribute){ export function observedAttributes(instance, observedAttribute){
const { observedAttributes= [] }= instance.constructor; const { observedAttributes= [] }= instance.constructor;
return observedAttributes return observedAttributes
@ -24,4 +61,28 @@ export function observedAttributes(instance, observedAttribute){
return out; return out;
}, {}); }, {});
} }
/**
* Converts kebab-case strings to camelCase
* @param {string} name - The kebab-case string
* @returns {string} The camelCase string
*/
function kebabToCamel(name){ return name.replace(/-./g, x=> x[1].toUpperCase()); } function kebabToCamel(name){ return name.replace(/-./g, x=> x[1].toUpperCase()); }
/**
* Error class for definition tracking
* Shows the correct stack trace for debugging (signal) creation
*/
export class Defined extends Error{
constructor(){
super();
const [ curr, ...rest ]= this.stack.split("\n");
const curr_file= curr.slice(curr.indexOf("@"), curr.indexOf(".js:")+4);
const curr_lib= curr_file.includes("src/helpers.js") ? "src/" : curr_file;
this.stack= rest.find(l=> !l.includes(curr_lib)) || curr;
}
get compact(){
const { stack }= this;
return stack.slice(0, stack.indexOf("@")+1)+"…"+stack.slice(stack.lastIndexOf("/"));
}
}

View File

@ -1,292 +0,0 @@
export const mark= "__dde_signal";
import { hasOwn } from "./helpers.js";
export function isSignal(candidate){
try{ return hasOwn(candidate, mark); }
catch(e){ return false; }
}
/** @type {function[]} */
const stack_watch= [];
/**
* ### `WeakMap<function, Set<ddeSignal<any, any>>>`
* The `Set` is in the form of `[ source, ...depended signals (DSs) ]`.
* When the DS is cleaned (`S.clear`) it is removed from DSs,
* if remains only one (`source`) it is cleared too.
* ### `WeakMap<object, function>`
* This is used for revesed deps, the `function` is also key for `deps`.
* @type {WeakMap<function|object,Set<ddeSignal<any, any>>|function>}
* */
const deps= new WeakMap();
export function signal(value, actions){
if(typeof value!=="function")
return create(false, value, actions);
if(isSignal(value)) return value;
const out= create(true);
const contextReWatch= function(){
const [ origin, ...deps_old ]= deps.get(contextReWatch);
deps.set(contextReWatch, new Set([ origin ]));
stack_watch.push(contextReWatch);
write(out, value());
stack_watch.pop();
if(!deps_old.length) return;
const deps_curr= deps.get(contextReWatch);
for (const dep_signal of deps_old){
if(deps_curr.has(dep_signal)) continue;
removeSignalListener(dep_signal, contextReWatch);
}
};
deps.set(out[mark], contextReWatch);
deps.set(contextReWatch, new Set([ out ]));
contextReWatch();
return out;
}
export { signal as S };
signal.action= function(s, name, ...a){
const M= s[mark], { actions }= M;
if(!actions || !(name in actions))
throw new Error(`'${s}' has no action with name '${name}'!`);
actions[name].apply(M, a);
if(M.skip) return (delete M.skip);
M.listeners.forEach(l=> l(M.value));
};
signal.on= function on(s, listener, options= {}){
const { signal: as }= options;
if(as && as.aborted) return;
if(Array.isArray(s)) return s.forEach(s=> on(s, listener, options));
addSignalListener(s, listener);
if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener));
//TODO: cleanup when signal removed
};
signal.symbols= {
//signal: mark,
onclear: Symbol.for("Signal.onclear")
};
signal.clear= function(...signals){
for(const s of signals){
const M= s[mark];
if(!M) continue;
delete s.toJSON;
M.onclear.forEach(f=> f.call(M));
clearListDeps(s, M);
delete s[mark];
}
function clearListDeps(s, o){
o.listeners.forEach(l=> {
o.listeners.delete(l);
if(!deps.has(l)) return;
const ls= deps.get(l);
ls.delete(s);
if(ls.size>1) return;
s.clear(...ls);
deps.delete(l);
});
}
};
const key_reactive= "__dde_reactive";
import { enviroment as env } from "./dom-common.js";
import { el } from "./dom.js";
import { scope } from "./dom.js";
// TODO: third argument for handle `cache_tmp` in re-render
signal.el= function(s, map){
const mark_start= el.mark({ type: "reactive" }, true);
const mark_end= mark_start.end;
const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current }= scope;
let cache= {};
const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeSignalListener(s, reRenderReactiveElement);
const cache_tmp= cache; // will be reused in the useCache or removed in the while loop on the end
cache= {};
scope.push(current);
let els= map(v, function useCache(key, fun){
let value;
if(hasOwn(cache_tmp, key)){
value= cache_tmp[key];
delete cache_tmp[key];
} else
value= fun();
cache[key]= value;
return value;
});
scope.pop();
if(!Array.isArray(els))
els= [ els ];
const el_start_rm= document.createComment("");
els.push(el_start_rm);
mark_start.after(...els);
let el_r;
while(( el_r= el_start_rm.nextSibling ) && el_r !== mark_end)
el_r.remove();
el_start_rm.remove();
if(mark_start.isConnected)
requestCleanUpReactives(current.host());
};
addSignalListener(s, reRenderReactiveElement);
removeSignalsFromElements(s, reRenderReactiveElement, mark_start, map);
reRenderReactiveElement(s());
return out;
};
function requestCleanUpReactives(host){
if(!host || !host[key_reactive]) return;
(requestIdleCallback || setTimeout)(function(){
host[key_reactive]= host[key_reactive]
.filter(([ s, el ])=> el.isConnected ? true : (removeSignalListener(...s), false));
});
}
import { on } from "./events.js";
import { observedAttributes } from "./helpers.js";
const observedAttributeActions= {
_set(value){ this.value= value; },
};
function observedAttribute(store){
return function(instance, name){
const varS= (...args)=> !args.length
? read(varS)
: instance.setAttribute(name, ...args);
const out= toSignal(varS, instance.getAttribute(name), observedAttributeActions);
store[name]= out;
return out;
};
}
const key_attributes= "__dde_attributes";
signal.observedAttributes= function(element){
const store= element[key_attributes]= {};
const attrs= observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToSignal({ detail }){
/*! This maps attributes to signals (`S.observedAttributes`).
* Investigate `__dde_attributes` key of the element.*/
const [ name, value ]= detail;
const curr= this[key_attributes][name];
if(curr) return signal.action(curr, "_set", value);
})(element);
on.disconnected(function(){
/*! This removes all signals mapped to attributes (`S.observedAttributes`).
* Investigate `__dde_attributes` key of the element.*/
signal.clear(...Object.values(this[key_attributes]));
})(element);
return attrs;
};
import { typeOf } from './helpers.js';
export const signals_config= {
isSignal,
processReactiveAttribute(element, key, attrs, set){
if(!isSignal(attrs)) return attrs;
const l= attr=> {
if(!element.isConnected)
return removeSignalListener(attrs, l);
set(key, attr);
};
addSignalListener(attrs, l);
removeSignalsFromElements(attrs, l, element, key);
return attrs();
}
};
function removeSignalsFromElements(s, listener, ...notes){
const { current }= scope;
if(current.prevent) return;
current.host(function(element){
if(!element[key_reactive]){
element[key_reactive]= [];
on.disconnected(()=>
/*!
* Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
* You can investigate the `__dde_reactive` key of the element.
* */
element[key_reactive].forEach(([ [ s, listener ] ])=>
removeSignalListener(s, listener, s[mark] && s[mark].host && s[mark].host() === element))
)(element);
}
element[key_reactive].push([ [ s, listener ], ...notes ]);
});
}
function create(is_readonly, value, actions){
const varS= is_readonly
? ()=> read(varS)
: (...value)=> value.length ? write(varS, ...value) : read(varS);
return toSignal(varS, value, actions, is_readonly);
}
const protoSigal= Object.assign(Object.create(null), {
stopPropagation(){
this.skip= true;
}
});
class SignalDefined extends Error{
constructor(){
super();
const [ curr, ...rest ]= this.stack.split("\n");
const curr_file= curr.slice(curr.indexOf("@"), curr.indexOf(".js:")+4);
this.stack= rest.find(l=> !l.includes(curr_file));
}
}
function toSignal(s, value, actions, readonly= false){
const onclear= [];
if(typeOf(actions)!=="[object Object]")
actions= {};
const { onclear: ocs }= signal.symbols;
if(actions[ocs]){
onclear.push(actions[ocs]);
delete actions[ocs];
}
const { host }= scope;
Reflect.defineProperty(s, mark, {
value: {
value, actions, onclear, host,
listeners: new Set(),
defined: (new SignalDefined()).stack,
readonly
},
enumerable: false,
writable: false,
configurable: true
});
s.toJSON= ()=> s();
s.valueOf= ()=> s[mark] && s[mark].value;
Object.setPrototypeOf(s[mark], protoSigal);
return s;
}
function currentContext(){
return stack_watch[stack_watch.length - 1];
}
function read(s){
if(!s[mark]) return;
const { value, listeners }= s[mark];
const context= currentContext();
if(context) listeners.add(context);
if(deps.has(context)) deps.get(context).add(s);
return value;
}
function write(s, value, force){
if(!s[mark]) return;
const M= s[mark];
if(!force && M.value===value) return;
M.value= value;
M.listeners.forEach(l=> l(value));
return value;
}
function addSignalListener(s, listener){
if(!s[mark]) return;
return s[mark].listeners.add(listener);
}
function removeSignalListener(s, listener, clear_when_empty){
const M= s[mark];
if(!M) return;
const out= M.listeners.delete(listener);
if(clear_when_empty && !M.listeners.size){
signal.clear(s);
if(!deps.has(M)) return out;
const c= deps.get(M);
if(!deps.has(c)) return out;
deps.get(c).forEach(sig=> removeSignalListener(sig, c, true));
}
return out;
}

View File

@ -1,13 +0,0 @@
export const signals_global= {
isSignal(attributes){ return false; },
processReactiveAttribute(obj, key, attr, set){ return attr; },
};
export function registerReactivity(def, global= true){
if(global) return Object.assign(signals_global, def);
Object.setPrototypeOf(def, signals_global);
return def;
}
/** @param {unknown} _this @returns {typeof signals_global} */
export function signals(_this){
return signals_global.isPrototypeOf(_this) && _this!==signals_global ? _this : signals_global;
}

View File

@ -1,293 +0,0 @@
export const mark= "__dde_signal";
import { hasOwn } from "./helpers.js";
export function isSignal(candidate){
try{ return hasOwn(candidate, mark); }
catch(e){ return false; }
}
/** @type {function[]} */
const stack_watch= [];
/**
* ### `WeakMap<function, Set<ddeSignal<any, any>>>`
* The `Set` is in the form of `[ source, ...depended signals (DSs) ]`.
* When the DS is cleaned (`S.clear`) it is removed from DSs,
* if remains only one (`source`) it is cleared too.
* ### `WeakMap<object, function>`
* This is used for revesed deps, the `function` is also key for `deps`.
* @type {WeakMap<function|object,Set<ddeSignal<any, any>>|function>}
* */
const deps= new WeakMap();
export function signal(value, actions){
if(typeof value!=="function")
return create(false, value, actions);
if(isSignal(value)) return value;
const out= create(true);
const contextReWatch= function(){
const [ origin, ...deps_old ]= deps.get(contextReWatch);
deps.set(contextReWatch, new Set([ origin ]));
stack_watch.push(contextReWatch);
write(out, value());
stack_watch.pop();
if(!deps_old.length) return;
const deps_curr= deps.get(contextReWatch);
for (const dep_signal of deps_old){
if(deps_curr.has(dep_signal)) continue;
removeSignalListener(dep_signal, contextReWatch);
}
};
deps.set(out[mark], contextReWatch);
deps.set(contextReWatch, new Set([ out ]));
contextReWatch();
return out;
}
export { signal as S };
signal.action= function(s, name, ...a){
const M= s[mark], { actions }= M;
if(!actions || !(name in actions))
throw new Error(`'${s}' has no action with name '${name}'!`);
actions[name].apply(M, a);
if(M.skip) return (delete M.skip);
M.listeners.forEach(l=> l(M.value));
};
signal.on= function on(s, listener, options= {}){
const { signal: as }= options;
if(as && as.aborted) return;
if(Array.isArray(s)) return s.forEach(s=> on(s, listener, options));
addSignalListener(s, listener);
if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener));
//TODO: cleanup when signal removed
};
signal.symbols= {
//signal: mark,
onclear: Symbol.for("Signal.onclear")
};
signal.clear= function(...signals){
for(const s of signals){
const M= s[mark];
if(!M) continue;
delete s.toJSON;
M.onclear.forEach(f=> f.call(M));
clearListDeps(s, M);
delete s[mark];
}
function clearListDeps(s, o){
o.listeners.forEach(l=> {
o.listeners.delete(l);
if(!deps.has(l)) return;
const ls= deps.get(l);
ls.delete(s);
if(ls.size>1) return;
s.clear(...ls);
deps.delete(l);
});
}
};
const key_reactive= "__dde_reactive";
import { enviroment as env } from "./dom-common.js";
import { el } from "./dom.js";
import { scope } from "./dom.js";
// TODO: third argument for handle `cache_tmp` in re-render
// TODO: clear cache on disconnect
// TODO: extract cache to separate (exportable) function
signal.el= function(s, map){
const mark_start= el.mark({ type: "reactive" }, true);
const mark_end= mark_start.end;
const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current }= scope;
let cache= {};
const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeSignalListener(s, reRenderReactiveElement);
const cache_tmp= cache; // will be reused in the useCache or removed in the while loop on the end
cache= {};
scope.push(current);
let els= map(v, function useCache(key, fun){
let value;
if(hasOwn(cache_tmp, key)){
value= cache_tmp[key];
delete cache_tmp[key];
} else
value= fun();
cache[key]= value;
return value;
});
scope.pop();
if(!Array.isArray(els))
els= [ els ];
const el_start_rm= document.createComment("");
els.push(el_start_rm);
mark_start.after(...els);
let el_r;
while(( el_r= el_start_rm.nextSibling ) && el_r !== mark_end)
el_r.remove();
el_start_rm.remove();
if(mark_start.isConnected)
requestCleanUpReactives(current.host());
};
addSignalListener(s, reRenderReactiveElement);
removeSignalsFromElements(s, reRenderReactiveElement, mark_start, map);
reRenderReactiveElement(s());
return out;
};
function requestCleanUpReactives(host){
if(!host || !host[key_reactive]) return;
(requestIdleCallback || setTimeout)(function(){
host[key_reactive]= host[key_reactive]
.filter(([ s, el ])=> el.isConnected ? true : (removeSignalListener(...s), false));
});
}
import { on } from "./events.js";
import { observedAttributes } from "./helpers.js";
const observedAttributeActions= {
_set(value){ this.value= value; },
};
function observedAttribute(store){
return function(instance, name){
const varS= (...args)=> !args.length
? read(varS)
: instance.setAttribute(name, ...args);
const out= toSignal(varS, instance.getAttribute(name), observedAttributeActions);
store[name]= out;
return out;
};
}
const key_attributes= "__dde_attributes";
signal.observedAttributes= function(element){
const store= element[key_attributes]= {};
const attrs= observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToSignal({ detail }){
/*! This maps attributes to signals (`S.observedAttributes`).
* Investigate `__dde_attributes` key of the element.*/
const [ name, value ]= detail;
const curr= this[key_attributes][name];
if(curr) return signal.action(curr, "_set", value);
})(element);
on.disconnected(function(){
/*! This removes all signals mapped to attributes (`S.observedAttributes`).
* Investigate `__dde_attributes` key of the element.*/
signal.clear(...Object.values(this[key_attributes]));
})(element);
return attrs;
};
import { typeOf } from './helpers.js';
export const signals_config= {
isSignal,
processReactiveAttribute(element, key, attrs, set){
if(!isSignal(attrs)) return attrs;
const l= attr=> {
if(!element.isConnected)
return removeSignalListener(attrs, l);
set(key, attr);
};
addSignalListener(attrs, l);
removeSignalsFromElements(attrs, l, element, key);
return attrs();
}
};
function removeSignalsFromElements(s, listener, ...notes){
const { current }= scope;
current.host(function(element){
if(element[key_reactive])
return element[key_reactive].push([ [ s, listener ], ...notes ]);
element[key_reactive]= [];
if(current.prevent) return; // typically document.body, doenst need auto-remove as it should happen on page leave
on.disconnected(()=>
/*!
* Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
* You can investigate the `__dde_reactive` key of the element.
* */
element[key_reactive].forEach(([ [ s, listener ] ])=>
removeSignalListener(s, listener, s[mark] && s[mark].host && s[mark].host() === element))
)(element);
});
}
function create(is_readonly, value, actions){
const varS= is_readonly
? ()=> read(varS)
: (...value)=> value.length ? write(varS, ...value) : read(varS);
return toSignal(varS, value, actions, is_readonly);
}
const protoSigal= Object.assign(Object.create(null), {
stopPropagation(){
this.skip= true;
}
});
class SignalDefined extends Error{
constructor(){
super();
const [ curr, ...rest ]= this.stack.split("\n");
const curr_file= curr.slice(curr.indexOf("@"), curr.indexOf(".js:")+4);
this.stack= rest.find(l=> !l.includes(curr_file));
}
}
function toSignal(s, value, actions, readonly= false){
const onclear= [];
if(typeOf(actions)!=="[object Object]")
actions= {};
const { onclear: ocs }= signal.symbols;
if(actions[ocs]){
onclear.push(actions[ocs]);
delete actions[ocs];
}
const { host }= scope;
Reflect.defineProperty(s, mark, {
value: {
value, actions, onclear, host,
listeners: new Set(),
defined: (new SignalDefined()).stack,
readonly
},
enumerable: false,
writable: false,
configurable: true
});
s.toJSON= ()=> s();
s.valueOf= ()=> s[mark] && s[mark].value;
Object.setPrototypeOf(s[mark], protoSigal);
return s;
}
function currentContext(){
return stack_watch[stack_watch.length - 1];
}
function read(s){
if(!s[mark]) return;
const { value, listeners }= s[mark];
const context= currentContext();
if(context) listeners.add(context);
if(deps.has(context)) deps.get(context).add(s);
return value;
}
function write(s, value, force){
if(!s[mark]) return;
const M= s[mark];
if(!force && M.value===value) return;
M.value= value;
M.listeners.forEach(l=> l(value));
return value;
}
function addSignalListener(s, listener){
if(!s[mark]) return;
return s[mark].listeners.add(listener);
}
function removeSignalListener(s, listener, clear_when_empty){
const M= s[mark];
if(!M) return;
const out= M.listeners.delete(listener);
if(clear_when_empty && !M.listeners.size){
signal.clear(s);
if(!deps.has(M)) return out;
const c= deps.get(M);
if(!deps.has(c)) return out;
deps.get(c).forEach(sig=> removeSignalListener(sig, c, true));
}
return out;
}

44
src/signals-lib/common.js Normal file
View File

@ -0,0 +1,44 @@
import { isProtoFrom, oAssign } from "../helpers.js";
/**
* Global signals object with default implementation
* @type {Object}
*/
export const signals_global= {
/**
* Checks if a value is a signal
* @param {any} attributes - Value to check
* @returns {boolean} Whether the value is a signal
*/
isSignal(attributes){ return false; },
/**
* Processes an attribute that might be reactive
* @param {Element} obj - Element that owns the attribute
* @param {string} key - Attribute name
* @param {any} attr - Attribute value
* @param {Function} set - Function to set the attribute
* @returns {any} Processed attribute value
*/
processReactiveAttribute(obj, key, attr, set){ return attr; },
};
/**
* Registers a reactivity implementation
* @param {Object} def - Reactivity implementation
* @param {boolean} [global=true] - Whether to set globally or create a new implementation
* @returns {Object} The registered reactivity implementation
*/
export function registerReactivity(def, global= true){
if(global) return oAssign(signals_global, def);
Object.setPrototypeOf(def, signals_global);
return def;
}
/**
* Gets the signals implementation from a context
* @param {unknown} _this - Context to check for signals implementation
* @returns {typeof signals_global} Signals implementation
*/
export function signals(_this){
return isProtoFrom(_this, signals_global) && _this!==signals_global ? _this : signals_global;
}

View File

@ -0,0 +1,39 @@
/**
* Symbol used to identify signals in objects
* @type {string}
*/
export const mark= "__dde_signal";
/**
* Batches signal updates to improve performance
* @type {Function}
*/
export const queueSignalWrite= (()=> {
let pendingSignals= new Set();
let scheduled= false;
/**
* Processes all pending signal updates
* @private
*/
function flushSignals() {
scheduled = false;
const todo= pendingSignals;
pendingSignals= new Set();
for(const signal of todo){
const M = signal[mark];
if(M) M.listeners.forEach(l => l(M.value));
}
}
/**
* Queues a signal for update
* @param {Object} s - Signal to queue
*/
return function(s){
pendingSignals.add(s);
if(scheduled) return;
scheduled = true;
queueMicrotask(flushSignals);
};
})();

View File

@ -1,4 +1,4 @@
import type { Action, Actions, signal as s, Signal, SymbolOnclear } from "../signals.d.ts"; import type { Action, Actions, signal as s, Signal, SymbolOnclear } from "../../signals.js";
export { Action, Actions, Signal, SymbolOnclear }; export { Action, Actions, Signal, SymbolOnclear };
export const S: s; export const S: s;
export const signal: s; export const signal: s;

View File

@ -0,0 +1,487 @@
import { queueSignalWrite, mark } from "./helpers.js";
export { mark };
import { hasOwn, Defined, oCreate, isProtoFrom, oAssign } from "../helpers.js";
const Signal = oCreate(null, {
get: { value(){ return read(this); } },
set: { value(...v){ return write(this, ...v); } },
toJSON: { value(){ return read(this); } },
valueOf: { value(){ return this[mark] && this[mark].value; } }
});
const SignalReadOnly= oCreate(Signal, {
set: { value(){ return; } },
});
/**
* Checks if a value is a signal
*
* @param {any} candidate - Value to check
* @returns {boolean} True if the value is a signal
*/
export function isSignal(candidate){
return isProtoFrom(candidate, Signal);
}
/**
* Stack for tracking nested signal computations
* @type {function[]}
*/
const stack_watch= [];
/**
* Dependencies tracking map for signals
*
* ### `WeakMap<function, Set<ddeSignal<any, any>>>`
* The `Set` is in the form of `[ source, ...depended signals (DSs) ]`.
* When the DS is cleaned (`S.clear`) it is removed from DSs,
* if remains only one (`source`) it is cleared too.
* ### `WeakMap<object, function>`
* This is used for revesed deps, the `function` is also key for `deps`.
* @type {WeakMap<function|object,Set<ddeSignal<any, any>>|function>}
*/
const deps= new WeakMap();
/**
* Creates a new signal or converts a function into a derived signal
*
* @param {any|function} value - Initial value or function that computes the value
* @param {Object} [actions] - Custom actions for the signal
* @returns {Object} Signal object with get() and set() methods
*/
export function signal(value, actions){
if(typeof value!=="function")
return create(false, value, actions);
if(isSignal(value)) return value;
const out= create(true);
/**
* Updates the derived signal when dependencies change
* @private
*/
function contextReWatch(){
const [ origin, ...deps_old ]= deps.get(contextReWatch);
deps.set(contextReWatch, new Set([ origin ]));
stack_watch.push(contextReWatch);
write(out, value());
stack_watch.pop();
if(!deps_old.length) return;
const deps_curr= deps.get(contextReWatch);
for (const dep_signal of deps_old){
if(deps_curr.has(dep_signal)) continue;
removeSignalListener(dep_signal, contextReWatch);
}
}
deps.set(out[mark], contextReWatch);
deps.set(contextReWatch, new Set([ out ]));
contextReWatch();
return out;
}
/** Alias for signal */
export { signal as S };
/**
* Calls a custom action on a signal
*
* @param {Object} s - Signal object to call action on
* @param {string} name - Action name
* @param {...any} a - Arguments to pass to the action
*/
signal.action= function(s, name, ...a){
const M= s[mark];
if(!M) return;
const { actions }= M;
if(!actions || !hasOwn(actions, name))
throw new Error(`Action "${name}" not defined. See ${mark}.actions.`);
actions[name].apply(M, a);
if(M.skip) return (delete M.skip);
queueSignalWrite(s);
};
/**
* Subscribes a listener to signal changes
*
* @param {Object|Object[]} s - Signal object or array of signal objects to subscribe to
* @param {function} listener - Callback function receiving signal value
* @param {Object} [options={}] - Subscription options
* @param {AbortSignal} [options.signal] - Signal to abort subscription
*/
signal.on= function on(s, listener, options= {}){
const { signal: as }= options;
if(as && as.aborted) return;
if(Array.isArray(s)) return s.forEach(s=> on(s, listener, options));
addSignalListener(s, listener);
if(as) as.addEventListener("abort", ()=> removeSignalListener(s, listener));
};
/**
* Symbol constants for signal internals
*/
signal.symbols= {
//signal: mark,
onclear: Symbol.for("Signal.onclear")
};
/**
* Cleans up signals and their dependencies
*
* @param {...Object} signals - Signal objects to clean up
*/
signal.clear= function(...signals){
for(const s of signals){
const M= s[mark];
if(!M) continue;
delete s.toJSON;
M.onclear.forEach(f=> f.call(M));
clearListDeps(s, M);
delete s[mark];
}
/**
* Cleans up signal dependencies
* @param {function} s - Signal being cleared
* @param {Object} o - Signal metadata
* @private
*/
function clearListDeps(s, o){
o.listeners.forEach(l=> {
o.listeners.delete(l);
if(!deps.has(l)) return;
const ls= deps.get(l);
ls.delete(s);
if(ls.size>1) return;
s.clear(...ls);
deps.delete(l);
});
}
};
/** Property key for tracking reactive elements */
const key_reactive= "__dde_reactive";
import { enviroment as env } from "../dom-common.js";
import { el } from "../dom.js";
import { scope } from "../dom.js";
import { on } from "../events.js";
export function cache(store= oCreate()){
return (key, fun)=> hasOwn(store, key) ? store[key] : (store[key]= fun());
}
/**
* Creates a reactive DOM element that re-renders when signal changes
*
* @TODO Third argument for handle `cache_tmp` in re-render
* @param {Object} s - Signal object to watch
* @param {Function} map - Function mapping signal value to DOM elements
* @returns {DocumentFragment} Fragment containing reactive elements
*/
signal.el= function(s, map){
const mark_start= el.mark({ type: "reactive", source: new Defined().compact }, true);
const mark_end= mark_start.end;
const out= env.D.createDocumentFragment();
out.append(mark_start, mark_end);
const { current }= scope;
let cache_shared= oCreate();
const reRenderReactiveElement= v=> {
if(!mark_start.parentNode || !mark_end.parentNode) // === `isConnected` or wasnt yet rendered
return removeSignalListener(s, reRenderReactiveElement);
const memo= cache(cache_shared);
cache_shared= oCreate();
scope.push(current);
let els= map(v, function useCache(key, fun){
return (cache_shared[key]= memo(key, fun));
});
scope.pop();
if(!Array.isArray(els))
els= [ els ];
const el_start_rm= document.createComment("");
els.push(el_start_rm);
mark_start.after(...els);
let el_r;
while(( el_r= el_start_rm.nextSibling ) && el_r !== mark_end)
el_r.remove();
el_start_rm.remove();
if(mark_start.isConnected)
requestCleanUpReactives(current.host());
};
addSignalListener(s, reRenderReactiveElement);
removeSignalsFromElements(s, reRenderReactiveElement, mark_start, map);
reRenderReactiveElement(s.get());
current.host(on.disconnected(()=>
/*! Clears cached elements for reactive element `S.el` */
cache_shared= {}
));
return out;
};
/**
* Cleans up reactive elements that are no longer connected
*
* @param {Element} host - Host element containing reactive elements
* @private
*/
function requestCleanUpReactives(host){
if(!host || !host[key_reactive]) return;
(requestIdleCallback || setTimeout)(function(){
host[key_reactive]= host[key_reactive]
.filter(([ s, el ])=> el.isConnected ? true : (removeSignalListener(...s), false));
});
}
import { observedAttributes } from "../helpers.js";
/**
* Actions for observed attribute signals
* @private
*/
const observedAttributeActions= {
_set(value){ this.value= value; },
};
/**
* Creates a function that returns signals for element attributes
*
* @param {Object} store - Storage object for attribute signals
* @returns {Function} Function creating attribute signals
* @private
*/
function observedAttribute(store){
return function(instance, name){
const varS= oCreate(Signal, {
set: { value(...v){ return instance.setAttribute(name, ...v); } }
});
const out= toSignal(varS, instance.getAttribute(name), observedAttributeActions);
store[name]= out;
return out;
};
}
/** Property key for storing attribute signals */
const key_attributes= "__dde_attributes";
/**
* Creates signals for observed attributes in custom elements
*
* @param {Element} element - Custom element instance
* @returns {Object} Object with attribute signals
*/
signal.observedAttributes= function(element){
const store= element[key_attributes]= {};
const attrs= observedAttributes(element, observedAttribute(store));
on.attributeChanged(function attributeChangeToSignal({ detail }){
/*! This maps attributes to signals (`S.observedAttributes`).
Investigate `__dde_attributes` key of the element. */
const [ name, value ]= detail;
const curr= this[key_attributes][name];
if(curr) return signal.action(curr, "_set", value);
})(element);
on.disconnected(function(){
/*! This removes all signals mapped to attributes (`S.observedAttributes`).
Investigate `__dde_attributes` key of the element. */
signal.clear(...Object.values(this[key_attributes]));
})(element);
return attrs;
};
import { typeOf } from '../helpers.js';
/**
* Signal configuration for the library
* Implements processReactiveAttribute to handle signal-based attributes
*/
export const signals_config= {
isSignal,
/**
* Processes attributes that might be signals
*
* @param {Element} element - Element with the attribute
* @param {string} key - Attribute name
* @param {any} attrs - Attribute value (possibly a signal)
* @param {Function} set - Function to set attribute value
* @returns {any} Processed attribute value
*/
processReactiveAttribute(element, key, attrs, set){
if(!isSignal(attrs)) return attrs;
const l= attr=> {
if(!element.isConnected)
return removeSignalListener(attrs, l);
set(key, attr);
};
addSignalListener(attrs, l);
removeSignalsFromElements(attrs, l, element, key);
return attrs.get();
}
};
/**
* Registers signal listener for cleanup when element is removed
*
* @param {Object} s - Signal object to track
* @param {Function} listener - Signal listener
* @param {...any} notes - Additional context information
* @private
*/
function removeSignalsFromElements(s, listener, ...notes){
const { current }= scope;
current.host(function(element){
if(element[key_reactive])
return element[key_reactive].push([ [ s, listener ], ...notes ]);
element[key_reactive]= [];
if(current.prevent) return; // typically document.body, doenst need auto-remove as it should happen on page leave
on.disconnected(()=>
/*! Clears all Signals listeners added in the current scope/host (`S.el`, `assign`, …?).
You can investigate the `__dde_reactive` key of the element. */
element[key_reactive].forEach(([ [ s, listener ] ])=>
removeSignalListener(s, listener, s[mark] && s[mark].host && s[mark].host() === element))
)(element);
});
}
/**
* Registry for cleaning up signals when they are garbage collected
* @type {FinalizationRegistry}
*/
const cleanUpRegistry = new FinalizationRegistry(function(s){
signal.clear({ [mark]: s });
});
/**
* Creates a new signal object
*
* @param {boolean} is_readonly - Whether the signal is readonly
* @param {any} value - Initial signal value
* @param {Object} actions - Custom actions for the signal
* @returns {Object} Signal object with get() and set() methods
* @private
*/
function create(is_readonly, value, actions){
const varS = oCreate(is_readonly ? SignalReadOnly : Signal);
const SI= toSignal(varS, value, actions, is_readonly);
cleanUpRegistry.register(SI, SI[mark]);
return SI;
}
/**
* Prototype for signal internal objects
* @private
*/
const protoSigal= oAssign(oCreate(), {
/**
* Prevents signal propagation
*/
stopPropagation(){
this.skip= true;
}
});
/**
* Transforms an object into a signal
*
* @param {Object} s - Object to transform
* @param {any} value - Initial value
* @param {Object} actions - Custom actions
* @param {boolean} [readonly=false] - Whether the signal is readonly
* @returns {Object} Signal object with get() and set() methods
* @private
*/
function toSignal(s, value, actions, readonly= false){
const onclear= [];
if(typeOf(actions)!=="[object Object]")
actions= {};
const { onclear: ocs }= signal.symbols;
if(actions[ocs]){
onclear.push(actions[ocs]);
delete actions[ocs];
}
const { host }= scope;
Reflect.defineProperty(s, mark, {
value: oAssign(oCreate(protoSigal), {
value, actions, onclear, host,
listeners: new Set(),
defined: new Defined().stack,
readonly
}),
enumerable: false,
writable: false,
configurable: true
});
return s;
}
/**
* Gets the current computation context
* @returns {function|undefined} Current context function
* @private
*/
function currentContext(){
return stack_watch[stack_watch.length - 1];
}
/**
* Reads a signal's value and tracks dependencies
*
* @param {Object} s - Signal object to read
* @returns {any} Signal value
* @private
*/
function read(s){
if(!s[mark]) return;
const { value, listeners }= s[mark];
const context= currentContext();
if(context) listeners.add(context);
if(deps.has(context)) deps.get(context).add(s);
return value;
}
/**
* Writes a new value to a signal
*
* @param {Object} s - Signal object to update
* @param {any} value - New value
* @param {boolean} [force=false] - Force update even if value is unchanged
* @returns {any} The new value
* @private
*/
function write(s, value, force){
const M= s[mark];
if(!M || (!force && M.value===value)) return;
M.value= value;
queueSignalWrite(s);
return value;
}
/**
* Adds a listener to a signal
*
* @param {Object} s - Signal object to listen to
* @param {Function} listener - Callback function
* @returns {Set} Listener set
* @private
*/
function addSignalListener(s, listener){
if(!s[mark]) return;
return s[mark].listeners.add(listener);
}
/**
* Removes a listener from a signal
*
* @param {Object} s - Signal object to modify
* @param {Function} listener - Listener to remove
* @param {boolean} [clear_when_empty] - Whether to clear the signal when no listeners remain
* @returns {boolean} Whether the listener was found and removed
* @private
*/
function removeSignalListener(s, listener, clear_when_empty){
const M= s[mark];
if(!M) return;
const { listeners: L }= M;
const out= L.delete(listener);
if(!out || !clear_when_empty || L.size) return out;
signal.clear(s);
const depList= deps.get(M);
if(!depList) return out;
const depSource= deps.get(depList);
if(!depSource) return out;
for(const sig of depSource) removeSignalListener(sig, depList, true);
return out;
}