mirror of
https://github.com/jaandrle/deka-dom-el
synced 2025-07-02 04:32:14 +02:00
Compare commits
12 Commits
v0.9.3-alp
...
c3a17e6dde
Author | SHA1 | Date | |
---|---|---|---|
c3a17e6dde | |||
bdb20ec298 | |||
7ec50e1660
|
|||
6c4ddd655f
|
|||
198f4a3777
|
|||
3435ea6cfe
|
|||
ed7e6c7963 | |||
3168f452ae
|
|||
b53f3926b3 | |||
8f2fd5a68c | |||
f53b97a89c | |||
f8a94ab9f8
|
17
README.md
17
README.md
@ -18,19 +18,19 @@ function HelloWorldComponent({ initial }){
|
||||
/** @param {HTMLOptionElement} el */
|
||||
const isSelected= el=> (el.selected= el.value===initial);
|
||||
// @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(
|
||||
el("p", {
|
||||
textContent: S(() => `Hello World ${emoji().repeat(clicks())}`),
|
||||
textContent: S(() => `Hello World ${emoji.get().repeat(clicks.get())}`),
|
||||
className: "example",
|
||||
ariaLive: "polite", //OR ariaset: { live: "polite" },
|
||||
dataset: { example: "Example" }, //OR dataExample: "Example",
|
||||
}),
|
||||
el("button",
|
||||
{ textContent: "Fire", type: "button" },
|
||||
on("click", ()=> clicks(clicks() + 1)),
|
||||
on("keyup", ()=> clicks(clicks() - 2)),
|
||||
on("click", ()=> clicks.set(clicks.get() + 1)),
|
||||
on("keyup", ()=> clicks.set(clicks.get() - 2)),
|
||||
),
|
||||
el("select", null, onChange).append(
|
||||
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/`…)
|
||||
|
||||
## Signals
|
||||
- [Signals — whats going on behind the scenes | by Ryan Hoffnan | ITNEXT](https://itnext.io/
|
||||
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)
|
||||
- there is also [tc39/proposal-signals: A proposal to add signals to JavaScript.](https://github.com/tc39/proposal-
|
||||
signals)
|
||||
- [Signals — whats going on behind the scenes \| by Ryan Hoffnan \| ITNEXT](https://itnext.io/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)
|
||||
- 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)
|
||||
|
1317
dist/dde-with-signals.js
vendored
1317
dist/dde-with-signals.js
vendored
File diff suppressed because it is too large
Load Diff
850
dist/dde.js
vendored
850
dist/dde.js
vendored
@ -1,456 +1,664 @@
|
||||
//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
|
||||
function m(t) {
|
||||
return typeof t > "u";
|
||||
function isUndef(value) {
|
||||
return typeof value === "undefined";
|
||||
}
|
||||
function L(t, e) {
|
||||
if (!t || !(t instanceof AbortSignal))
|
||||
return !0;
|
||||
if (!t.aborted)
|
||||
return t.addEventListener("abort", e), function() {
|
||||
t.removeEventListener("abort", e);
|
||||
};
|
||||
function isInstance(obj, cls) {
|
||||
return obj instanceof cls;
|
||||
}
|
||||
function q(t, e) {
|
||||
let { observedAttributes: r = [] } = t.constructor;
|
||||
return r.reduce(function(n, o) {
|
||||
return n[G(o)] = e(t, o), n;
|
||||
function isProtoFrom(obj, cls) {
|
||||
return Object.prototype.isPrototypeOf.call(cls, obj);
|
||||
}
|
||||
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 observedAttributes(instance, observedAttribute) {
|
||||
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
|
||||
return observedAttributes3.reduce(function(out, name) {
|
||||
out[kebabToCamel(name)] = observedAttribute(instance, name);
|
||||
return out;
|
||||
}, {});
|
||||
}
|
||||
function G(t) {
|
||||
return t.replace(/-./g, (e) => e[1].toUpperCase());
|
||||
function kebabToCamel(name) {
|
||||
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
|
||||
var a = {
|
||||
setDeleteAttr: V,
|
||||
var enviroment = {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
D: globalThis.document,
|
||||
F: globalThis.DocumentFragment,
|
||||
H: globalThis.HTMLElement,
|
||||
S: globalThis.SVGElement,
|
||||
M: globalThis.MutationObserver,
|
||||
q: (t) => t || Promise.resolve()
|
||||
q: (p) => p || Promise.resolve()
|
||||
};
|
||||
function V(t, e, r) {
|
||||
if (Reflect.set(t, e, r), !!m(r)) {
|
||||
if (Reflect.deleteProperty(t, e), t instanceof a.H && t.getAttribute(e) === "undefined")
|
||||
return t.removeAttribute(e);
|
||||
if (Reflect.get(t, e) === "undefined")
|
||||
return Reflect.set(t, e, "");
|
||||
}
|
||||
function setDeleteAttr(obj, prop, val) {
|
||||
Reflect.set(obj, prop, val);
|
||||
if (!isUndef(val)) return;
|
||||
Reflect.deleteProperty(obj, prop);
|
||||
if (isInstance(obj, enviroment.H) && obj.getAttribute(prop) === "undefined")
|
||||
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
|
||||
function dt(t) {
|
||||
return a.q(t);
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
}
|
||||
var g = [{
|
||||
var scopes = [{
|
||||
get scope() {
|
||||
return a.D.body;
|
||||
return enviroment.D.body;
|
||||
},
|
||||
host: (t) => t ? t(a.D.body) : a.D.body,
|
||||
prevent: !0
|
||||
}], O = {
|
||||
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
|
||||
prevent: true
|
||||
}];
|
||||
var scope = {
|
||||
/**
|
||||
* Gets the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
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() {
|
||||
return this.current.host;
|
||||
},
|
||||
/**
|
||||
* Prevents default behavior in the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
preventDefault() {
|
||||
let { current: t } = this;
|
||||
return t.prevent = !0, t;
|
||||
const { current } = this;
|
||||
current.prevent = true;
|
||||
return current;
|
||||
},
|
||||
/**
|
||||
* Gets a copy of the current scope stack
|
||||
* @returns {Array} Copy of scope stack
|
||||
*/
|
||||
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() {
|
||||
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() {
|
||||
if (g.length !== 1)
|
||||
return g.pop();
|
||||
if (scopes.length === 1) return;
|
||||
return scopes.pop();
|
||||
}
|
||||
};
|
||||
function k(...t) {
|
||||
return this.appendOriginal(...t), this;
|
||||
function append(...els) {
|
||||
this.appendOriginal(...els);
|
||||
return this;
|
||||
}
|
||||
function J(t) {
|
||||
return t.append === k || (t.appendOriginal = t.append, t.append = k), t;
|
||||
function chainableAppend(el) {
|
||||
if (el.append === append) return el;
|
||||
el.appendOriginal = el.append;
|
||||
el.append = append;
|
||||
return el;
|
||||
}
|
||||
var T;
|
||||
function P(t, e, ...r) {
|
||||
let n = S(this), o = 0, c, d;
|
||||
switch ((Object(e) !== e || n.isSignal(e)) && (e = { textContent: e }), !0) {
|
||||
case typeof t == "function": {
|
||||
o = 1;
|
||||
let f = (...l) => l.length ? (o === 1 ? r.unshift(...l) : l.forEach((E) => E(d)), void 0) : d;
|
||||
O.push({ scope: t, host: f }), c = t(e || void 0);
|
||||
let p = c instanceof a.F;
|
||||
if (c.nodeName === "#comment") break;
|
||||
let b = P.mark({
|
||||
var namespace;
|
||||
function createElement(tag, attributes, ...addons) {
|
||||
const s = signals(this);
|
||||
let scoped = 0;
|
||||
let el, el_host;
|
||||
if (Object(attributes) !== attributes || s.isSignal(attributes))
|
||||
attributes = { textContent: attributes };
|
||||
switch (true) {
|
||||
case typeof tag === "function": {
|
||||
scoped = 1;
|
||||
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",
|
||||
name: t.name,
|
||||
host: p ? "this" : "parentElement"
|
||||
name: tag.name,
|
||||
host: is_fragment ? "this" : "parentElement"
|
||||
});
|
||||
c.prepend(b), p && (d = b);
|
||||
el.prepend(el_mark);
|
||||
if (is_fragment) el_host = el_mark;
|
||||
break;
|
||||
}
|
||||
case t === "#text":
|
||||
c = R.call(this, a.D.createTextNode(""), e);
|
||||
case tag === "#text":
|
||||
el = assign.call(this, enviroment.D.createTextNode(""), attributes);
|
||||
break;
|
||||
case (t === "<>" || !t):
|
||||
c = R.call(this, a.D.createDocumentFragment(), e);
|
||||
case (tag === "<>" || !tag):
|
||||
el = assign.call(this, enviroment.D.createDocumentFragment(), attributes);
|
||||
break;
|
||||
case !!T:
|
||||
c = R.call(this, a.D.createElementNS(T, t), e);
|
||||
case Boolean(namespace):
|
||||
el = assign.call(this, enviroment.D.createElementNS(namespace, tag), attributes);
|
||||
break;
|
||||
case !c:
|
||||
c = R.call(this, a.D.createElement(t), e);
|
||||
case !el:
|
||||
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) {
|
||||
t = Object.entries(t).map(([o, c]) => o + `="${c}"`).join(" ");
|
||||
let r = e ? "" : "/", n = a.D.createComment(`<dde:mark ${t}${a.ssr}${r}>`);
|
||||
return e && (n.end = a.D.createComment("</dde:mark>")), n;
|
||||
createElement.mark = function(attrs, is_open = false) {
|
||||
attrs = Object.entries(attrs).map(([n, v]) => n + `="${v}"`).join(" ");
|
||||
const end = is_open ? "" : "/";
|
||||
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) {
|
||||
let e = this;
|
||||
return function(...n) {
|
||||
T = t;
|
||||
let o = P.call(e, ...n);
|
||||
return T = void 0, o;
|
||||
function createElementNS(ns) {
|
||||
const _this = this;
|
||||
return function createElementNSCurried(...rest) {
|
||||
namespace = ns;
|
||||
const el = createElement.call(_this, ...rest);
|
||||
namespace = void 0;
|
||||
return el;
|
||||
};
|
||||
}
|
||||
function lt(t, e = t) {
|
||||
let r = "\xB9\u2070", n = "\u2713", o = Object.fromEntries(
|
||||
Array.from(e.querySelectorAll("slot")).filter((c) => !c.name.endsWith(r)).map((c) => [c.name += r, c])
|
||||
function simulateSlots(element, root = element) {
|
||||
const mark_e = "\xB9\u2070", mark_s = "\u2713";
|
||||
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, {
|
||||
apply(c, d, f) {
|
||||
if (f[0] === e) return c.apply(t, f);
|
||||
for (let p of f) {
|
||||
let b = (p.slot || "") + r;
|
||||
element.append = new Proxy(element.append, {
|
||||
apply(orig, _, els) {
|
||||
if (els[0] === root) return orig.apply(element, els);
|
||||
for (const el of els) {
|
||||
const name = (el.slot || "") + mark_e;
|
||||
try {
|
||||
Q(p, "remove", "slot");
|
||||
} catch {
|
||||
elementAttribute(el, "remove", "slot");
|
||||
} catch (_error) {
|
||||
}
|
||||
let l = o[b];
|
||||
if (!l) return;
|
||||
l.name.startsWith(n) || (l.childNodes.forEach((E) => E.remove()), l.name = n + b), l.append(p);
|
||||
const slot = slots[name];
|
||||
if (!slot) return;
|
||||
if (!slot.name.startsWith(mark_s)) {
|
||||
slot.childNodes.forEach((c) => c.remove());
|
||||
slot.name = mark_s + name;
|
||||
}
|
||||
slot.append(el);
|
||||
}
|
||||
return t.append = c, t;
|
||||
element.append = orig;
|
||||
return element;
|
||||
}
|
||||
}), t !== e) {
|
||||
let c = Array.from(t.childNodes);
|
||||
t.append(...c);
|
||||
});
|
||||
if (element !== root) {
|
||||
const els = Array.from(element.childNodes);
|
||||
element.append(...els);
|
||||
}
|
||||
return e;
|
||||
return root;
|
||||
}
|
||||
var N = /* @__PURE__ */ new WeakMap(), { setDeleteAttr: $ } = a;
|
||||
function R(t, ...e) {
|
||||
if (!e.length) return t;
|
||||
N.set(t, H(t, this));
|
||||
for (let [r, n] of Object.entries(Object.assign({}, ...e)))
|
||||
U.call(this, t, r, n);
|
||||
return N.delete(t), t;
|
||||
var assign_context = /* @__PURE__ */ new WeakMap();
|
||||
var { setDeleteAttr: setDeleteAttr2 } = enviroment;
|
||||
function assign(element, ...attributes) {
|
||||
if (!attributes.length) return element;
|
||||
assign_context.set(element, assignContext(element, this));
|
||||
for (const [key, value] of Object.entries(oAssign({}, ...attributes)))
|
||||
assignAttribute.call(this, element, key, value);
|
||||
assign_context.delete(element);
|
||||
return element;
|
||||
}
|
||||
function U(t, e, r) {
|
||||
let { setRemoveAttr: n, s: o } = H(t, this), c = this;
|
||||
r = o.processReactiveAttribute(
|
||||
t,
|
||||
e,
|
||||
r,
|
||||
(f, p) => U.call(c, t, f, p)
|
||||
function assignAttribute(element, key, value) {
|
||||
const { setRemoveAttr, s } = assignContext(element, this);
|
||||
const _this = this;
|
||||
value = s.processReactiveAttribute(
|
||||
element,
|
||||
key,
|
||||
value,
|
||||
(key2, value2) => assignAttribute.call(_this, element, key2, value2)
|
||||
);
|
||||
let [d] = e;
|
||||
if (d === "=") return n(e.slice(1), r);
|
||||
if (d === ".") return F(t, e.slice(1), r);
|
||||
if (/(aria|data)([A-Z])/.test(e))
|
||||
return e = e.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(), n(e, r);
|
||||
switch (e === "className" && (e = "class"), e) {
|
||||
const [k] = key;
|
||||
if ("=" === k) return setRemoveAttr(key.slice(1), value);
|
||||
if ("." === k) return setDelete(element, key.slice(1), value);
|
||||
if (/(aria|data)([A-Z])/.test(key)) {
|
||||
key = key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
||||
return setRemoveAttr(key, value);
|
||||
}
|
||||
if ("className" === key) key = "class";
|
||||
switch (key) {
|
||||
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":
|
||||
return $(t, e, r);
|
||||
return setDeleteAttr2(element, key, value);
|
||||
case "style":
|
||||
if (typeof r != "object") break;
|
||||
if (typeof value !== "object") break;
|
||||
/* falls through */
|
||||
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":
|
||||
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":
|
||||
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) {
|
||||
if (N.has(t)) return N.get(t);
|
||||
let n = (t instanceof a.S ? tt : Y).bind(null, t, "Attribute"), o = S(e);
|
||||
return { setRemoveAttr: n, s: o };
|
||||
function assignContext(element, _this) {
|
||||
if (assign_context.has(element)) return assign_context.get(element);
|
||||
const is_svg = isInstance(element, enviroment.S);
|
||||
const setRemoveAttr = (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
|
||||
const s = signals(_this);
|
||||
return { setRemoveAttr, s };
|
||||
}
|
||||
function K(t, e) {
|
||||
let r = S(this);
|
||||
return M(
|
||||
r,
|
||||
function classListDeclarative(element, toggle) {
|
||||
const s = signals(this);
|
||||
forEachEntries(
|
||||
s,
|
||||
"classList",
|
||||
t,
|
||||
e,
|
||||
(n, o) => t.classList.toggle(n, o === -1 ? void 0 : !!o)
|
||||
), t;
|
||||
element,
|
||||
toggle,
|
||||
(class_name, val) => element.classList.toggle(class_name, val === -1 ? void 0 : Boolean(val))
|
||||
);
|
||||
return element;
|
||||
}
|
||||
function Q(t, e, r, n) {
|
||||
return t instanceof a.H ? t[e + "Attribute"](r, n) : t[e + "AttributeNS"](null, r, n);
|
||||
function elementAttribute(element, op, key, value) {
|
||||
if (isInstance(element, enviroment.H))
|
||||
return element[op + "Attribute"](key, value);
|
||||
return element[op + "AttributeNS"](null, key, value);
|
||||
}
|
||||
function X(t, e) {
|
||||
if (!(e in t)) return !1;
|
||||
let r = z(t, e);
|
||||
return !m(r.set);
|
||||
function isPropSetter(el, key) {
|
||||
if (!(key in el)) return false;
|
||||
const des = getPropDescriptor(el, key);
|
||||
return !isUndef(des.set);
|
||||
}
|
||||
function z(t, e) {
|
||||
if (t = Object.getPrototypeOf(t), !t) return {};
|
||||
let r = Object.getOwnPropertyDescriptor(t, e);
|
||||
return r || z(t, e);
|
||||
function getPropDescriptor(p, key) {
|
||||
p = Object.getPrototypeOf(p);
|
||||
if (!p) return {};
|
||||
const des = Object.getOwnPropertyDescriptor(p, key);
|
||||
if (!des) return getPropDescriptor(p, key);
|
||||
return des;
|
||||
}
|
||||
function M(t, e, r, n, o) {
|
||||
let c = String;
|
||||
if (!(typeof n != "object" || n === null))
|
||||
return Object.entries(n).forEach(function([f, p]) {
|
||||
f && (f = new c(f), f.target = e, p = t.processReactiveAttribute(r, f, p, o), o(f, p));
|
||||
});
|
||||
function forEachEntries(s, target, element, obj, cb) {
|
||||
const S = String;
|
||||
if (typeof obj !== "object" || obj === null) return;
|
||||
return Object.entries(obj).forEach(function process([key, val]) {
|
||||
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) {
|
||||
return t[(m(n) ? "remove" : "set") + e](r, n);
|
||||
function setRemove(obj, prop, key, val) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
|
||||
}
|
||||
function tt(t, e, r, n, o = null) {
|
||||
return t[(m(n) ? "remove" : "set") + e + "NS"](o, r, n);
|
||||
function setRemoveNS(obj, prop, key, val, ns = null) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
|
||||
}
|
||||
function F(t, e, r) {
|
||||
if (Reflect.set(t, e, r), !!m(r))
|
||||
return Reflect.deleteProperty(t, e);
|
||||
function setDelete(obj, key, val) {
|
||||
Reflect.set(obj, key, val);
|
||||
if (!isUndef(val)) return;
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
|
||||
// src/events-observer.js
|
||||
var _ = a.M ? et() : new Proxy({}, {
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
function et() {
|
||||
let t = /* @__PURE__ */ new Map(), e = !1, r = (s) => function(u) {
|
||||
for (let i of u)
|
||||
if (i.type === "childList") {
|
||||
if (l(i.addedNodes, !0)) {
|
||||
s();
|
||||
continue;
|
||||
}
|
||||
E(i.removedNodes, !0) && s();
|
||||
function connectionsChangesObserverConstructor() {
|
||||
const store = /* @__PURE__ */ new Map();
|
||||
let is_observing = false;
|
||||
const observerListener = (stop2) => function(mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
if (observerAdded(mutation.addedNodes, true)) {
|
||||
stop2();
|
||||
continue;
|
||||
}
|
||||
}, 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));
|
||||
if (observerRemoved(mutation.removedNodes, true))
|
||||
stop2();
|
||||
}
|
||||
};
|
||||
function o(s, u) {
|
||||
u.length_c || u.length_d || (t.delete(s), f());
|
||||
const observer = new enviroment.M(observerListener(stop));
|
||||
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 cleanWhenOff(element, ls) {
|
||||
if (ls.length_c || ls.length_d)
|
||||
return;
|
||||
store.delete(element);
|
||||
stop();
|
||||
}
|
||||
function c(s) {
|
||||
if (t.has(s)) return t.get(s);
|
||||
let u = {
|
||||
function getElementStore(element) {
|
||||
if (store.has(element)) return store.get(element);
|
||||
const out = {
|
||||
connected: /* @__PURE__ */ new WeakSet(),
|
||||
length_c: 0,
|
||||
disconnected: /* @__PURE__ */ new WeakSet(),
|
||||
length_d: 0
|
||||
};
|
||||
return t.set(s, u), u;
|
||||
store.set(element, out);
|
||||
return out;
|
||||
}
|
||||
function d() {
|
||||
e || (e = !0, n.observe(a.D.body, { childList: !0, subtree: !0 }));
|
||||
function start() {
|
||||
if (is_observing) return;
|
||||
is_observing = true;
|
||||
observer.observe(enviroment.D.body, { childList: true, subtree: true });
|
||||
}
|
||||
function f() {
|
||||
!e || t.size || (e = !1, n.disconnect());
|
||||
function stop() {
|
||||
if (!is_observing || store.size) return;
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function p() {
|
||||
return new Promise(function(s) {
|
||||
(requestIdleCallback || requestAnimationFrame)(s);
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function b(s) {
|
||||
t.size > 30 && await p();
|
||||
let u = [];
|
||||
if (!(s instanceof Node)) return u;
|
||||
for (let i of t.keys())
|
||||
i === s || !(i instanceof Node) || s.contains(i) && u.push(i);
|
||||
return u;
|
||||
}
|
||||
function l(s, u) {
|
||||
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);
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
const out = [];
|
||||
if (!isInstance(element, Node)) return out;
|
||||
for (const el of store.keys()) {
|
||||
if (el === element || !isInstance(el, Node)) continue;
|
||||
if (element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
return i;
|
||||
return out;
|
||||
}
|
||||
function E(s, u) {
|
||||
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 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;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
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 () => {
|
||||
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
|
||||
function wt(t, e, r = rt) {
|
||||
let n = t.host || t;
|
||||
O.push({
|
||||
scope: n,
|
||||
host: (...d) => d.length ? d.forEach((f) => f(n)) : n
|
||||
}), typeof r == "function" && (r = r.call(n, n));
|
||||
let o = n[x];
|
||||
o || nt(n);
|
||||
let c = e.call(n, r);
|
||||
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);
|
||||
function customElementRender(target, render, props = observedAttributes2) {
|
||||
const custom_element = target.host || target;
|
||||
scope.push({
|
||||
scope: custom_element,
|
||||
host: (...c) => c.length ? c.forEach((c2) => c2(custom_element)) : custom_element
|
||||
});
|
||||
if (typeof props === "function") props = props.call(custom_element, custom_element);
|
||||
const is_lte = custom_element[keyLTE];
|
||||
if (!is_lte) lifecyclesToEvents(custom_element);
|
||||
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) {
|
||||
return j(t.prototype, "connectedCallback", function(e, r, n) {
|
||||
e.apply(r, n), r.dispatchEvent(new Event(v));
|
||||
}), j(t.prototype, "disconnectedCallback", function(e, r, n) {
|
||||
e.apply(r, n), (globalThis.queueMicrotask || setTimeout)(
|
||||
() => !r.isConnected && r.dispatchEvent(new Event(w))
|
||||
function lifecyclesToEvents(class_declaration) {
|
||||
wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail) {
|
||||
target.apply(thisArg, detail);
|
||||
thisArg.dispatchEvent(new Event(evc));
|
||||
});
|
||||
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;
|
||||
r.dispatchEvent(new CustomEvent(y, {
|
||||
detail: [o, c]
|
||||
})), e.apply(r, n);
|
||||
}), t.prototype[x] = !0, t;
|
||||
});
|
||||
wrapMethod(class_declaration.prototype, "attributeChangedCallback", function(target, thisArg, detail) {
|
||||
const [attribute, , value] = detail;
|
||||
thisArg.dispatchEvent(new CustomEvent(eva, {
|
||||
detail: [attribute, value]
|
||||
}));
|
||||
target.apply(thisArg, detail);
|
||||
});
|
||||
class_declaration.prototype[keyLTE] = true;
|
||||
return class_declaration;
|
||||
}
|
||||
function j(t, e, r) {
|
||||
t[e] = new Proxy(t[e] || (() => {
|
||||
}), { apply: r });
|
||||
function wrapMethod(obj, method, apply) {
|
||||
obj[method] = new Proxy(obj[method] || (() => {
|
||||
}), { apply });
|
||||
}
|
||||
function rt(t) {
|
||||
return q(t, (e, r) => e.getAttribute(r));
|
||||
function observedAttributes2(instance) {
|
||||
return observedAttributes(instance, (i, n) => i.getAttribute(n));
|
||||
}
|
||||
|
||||
// src/events.js
|
||||
function yt(t, e, r) {
|
||||
return e || (e = {}), function(o, ...c) {
|
||||
r && (c.unshift(o), o = typeof r == "function" ? r() : r);
|
||||
let d = c.length ? new CustomEvent(t, Object.assign({ detail: c[0] }, e)) : new Event(t, e);
|
||||
return o.dispatchEvent(d);
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
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) {
|
||||
return function(o) {
|
||||
return o.addEventListener(t, e, r), o;
|
||||
function on(event, listener, options) {
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(event, listener, options);
|
||||
return element;
|
||||
};
|
||||
}
|
||||
var B = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 });
|
||||
D.connected = function(t, e) {
|
||||
return e = B(e), function(n) {
|
||||
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);
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
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) {
|
||||
return e = B(e), function(n) {
|
||||
return n.addEventListener(w, t, e), n[x] || L(e.signal, () => _.offDisconnected(n, t)) && _.onDisconnected(n, t), n;
|
||||
on.disconnected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
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();
|
||||
D.disconnectedAsAbort = function(t) {
|
||||
if (W.has(t)) return W.get(t);
|
||||
let e = new AbortController();
|
||||
return W.set(t, e), t(D.disconnected(() => e.abort())), e;
|
||||
var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
on.disconnectedAsAbort = function(host) {
|
||||
if (store_abort.has(host)) return store_abort.get(host);
|
||||
const a = new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(() => a.abort()));
|
||||
return a;
|
||||
};
|
||||
var ot = /* @__PURE__ */ new WeakSet();
|
||||
D.attributeChanged = function(t, e) {
|
||||
return typeof e != "object" && (e = {}), function(n) {
|
||||
if (n.addEventListener(y, t, e), n[x] || ot.has(n) || !a.M) return n;
|
||||
let o = new a.M(function(d) {
|
||||
for (let { attributeName: f, target: p } of d)
|
||||
p.dispatchEvent(
|
||||
new CustomEvent(y, { detail: [f, p.getAttribute(f)] })
|
||||
var els_attribute_store = /* @__PURE__ */ new WeakSet();
|
||||
on.attributeChanged = function(listener, options) {
|
||||
if (typeof options !== "object")
|
||||
options = {};
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(eva, listener, options);
|
||||
if (element[keyLTE] || els_attribute_store.has(element))
|
||||
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= {
|
||||
assign: R,
|
||||
assignAttribute: U,
|
||||
chainableAppend: J,
|
||||
classListDeclarative: K,
|
||||
createElement: P,
|
||||
createElementNS: pt,
|
||||
customElementRender: wt,
|
||||
customElementWithDDE: nt,
|
||||
dispatchEvent: yt,
|
||||
el: P,
|
||||
elNS: pt,
|
||||
elementAttribute: Q,
|
||||
lifecyclesToEvents: nt,
|
||||
observedAttributes: rt,
|
||||
on: D,
|
||||
queue: dt,
|
||||
registerReactivity: Z,
|
||||
scope: O,
|
||||
simulateSlots: lt
|
||||
assign,
|
||||
assignAttribute,
|
||||
chainableAppend,
|
||||
classListDeclarative,
|
||||
createElement,
|
||||
createElementNS,
|
||||
customElementRender,
|
||||
customElementWithDDE: lifecyclesToEvents,
|
||||
dispatchEvent,
|
||||
el: createElement,
|
||||
elNS: createElementNS,
|
||||
elementAttribute,
|
||||
lifecyclesToEvents,
|
||||
observedAttributes: observedAttributes2,
|
||||
on,
|
||||
queue,
|
||||
registerReactivity,
|
||||
scope,
|
||||
simulateSlots
|
||||
};
|
||||
|
||||
})();
|
20
dist/esm-with-signals.d.ts
vendored
20
dist/esm-with-signals.d.ts
vendored
@ -52,9 +52,12 @@ type IsReadonly<T, K extends keyof T> =
|
||||
* @private
|
||||
*/
|
||||
type ElementAttributes<T extends SupportedElement>= Partial<{
|
||||
[K in keyof _fromElsInterfaces<T>]: IsReadonly<_fromElsInterfaces<T>, K> extends false
|
||||
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
|
||||
: ddeStringable
|
||||
[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]>
|
||||
: ddeStringable)
|
||||
} & AttrsModified> & Record<string, any>;
|
||||
export function classListDeclarative<El extends SupportedElement>(
|
||||
element: El,
|
||||
@ -515,7 +518,14 @@ interface ddeSVGTSpanElement extends SVGTSpanElement{ append: ddeAppend<ddeSVGTS
|
||||
interface ddeSVGUseElement extends SVGUseElement{ append: ddeAppend<ddeSVGUseElement>; }
|
||||
interface ddeSVGViewElement extends SVGViewElement{ append: ddeAppend<ddeSVGViewElement>; }
|
||||
// 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 SymbolSignal= Symbol;
|
||||
type SymbolOnclear= symbol;
|
||||
@ -544,7 +554,7 @@ interface signal{
|
||||
* ```js
|
||||
* const name= S("Jan");
|
||||
* 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 actions Use to define actions on the signal. Such as add item to the array.
|
||||
|
1317
dist/esm-with-signals.js
vendored
1317
dist/esm-with-signals.js
vendored
File diff suppressed because it is too large
Load Diff
9
dist/esm.d.ts
vendored
9
dist/esm.d.ts
vendored
@ -52,9 +52,12 @@ type IsReadonly<T, K extends keyof T> =
|
||||
* @private
|
||||
*/
|
||||
type ElementAttributes<T extends SupportedElement>= Partial<{
|
||||
[K in keyof _fromElsInterfaces<T>]: IsReadonly<_fromElsInterfaces<T>, K> extends false
|
||||
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
|
||||
: ddeStringable
|
||||
[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]>
|
||||
: ddeStringable)
|
||||
} & AttrsModified> & Record<string, any>;
|
||||
export function classListDeclarative<El extends SupportedElement>(
|
||||
element: El,
|
||||
|
850
dist/esm.js
vendored
850
dist/esm.js
vendored
@ -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
|
||||
function m(t) {
|
||||
return typeof t > "u";
|
||||
function isUndef(value) {
|
||||
return typeof value === "undefined";
|
||||
}
|
||||
function L(t, e) {
|
||||
if (!t || !(t instanceof AbortSignal))
|
||||
return !0;
|
||||
if (!t.aborted)
|
||||
return t.addEventListener("abort", e), function() {
|
||||
t.removeEventListener("abort", e);
|
||||
};
|
||||
function isInstance(obj, cls) {
|
||||
return obj instanceof cls;
|
||||
}
|
||||
function q(t, e) {
|
||||
let { observedAttributes: r = [] } = t.constructor;
|
||||
return r.reduce(function(n, o) {
|
||||
return n[G(o)] = e(t, o), n;
|
||||
function isProtoFrom(obj, cls) {
|
||||
return Object.prototype.isPrototypeOf.call(cls, obj);
|
||||
}
|
||||
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 observedAttributes(instance, observedAttribute) {
|
||||
const { observedAttributes: observedAttributes3 = [] } = instance.constructor;
|
||||
return observedAttributes3.reduce(function(out, name) {
|
||||
out[kebabToCamel(name)] = observedAttribute(instance, name);
|
||||
return out;
|
||||
}, {});
|
||||
}
|
||||
function G(t) {
|
||||
return t.replace(/-./g, (e) => e[1].toUpperCase());
|
||||
function kebabToCamel(name) {
|
||||
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
|
||||
var a = {
|
||||
setDeleteAttr: V,
|
||||
var enviroment = {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
D: globalThis.document,
|
||||
F: globalThis.DocumentFragment,
|
||||
H: globalThis.HTMLElement,
|
||||
S: globalThis.SVGElement,
|
||||
M: globalThis.MutationObserver,
|
||||
q: (t) => t || Promise.resolve()
|
||||
q: (p) => p || Promise.resolve()
|
||||
};
|
||||
function V(t, e, r) {
|
||||
if (Reflect.set(t, e, r), !!m(r)) {
|
||||
if (Reflect.deleteProperty(t, e), t instanceof a.H && t.getAttribute(e) === "undefined")
|
||||
return t.removeAttribute(e);
|
||||
if (Reflect.get(t, e) === "undefined")
|
||||
return Reflect.set(t, e, "");
|
||||
}
|
||||
function setDeleteAttr(obj, prop, val) {
|
||||
Reflect.set(obj, prop, val);
|
||||
if (!isUndef(val)) return;
|
||||
Reflect.deleteProperty(obj, prop);
|
||||
if (isInstance(obj, enviroment.H) && obj.getAttribute(prop) === "undefined")
|
||||
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
|
||||
function dt(t) {
|
||||
return a.q(t);
|
||||
function queue(promise) {
|
||||
return enviroment.q(promise);
|
||||
}
|
||||
var g = [{
|
||||
var scopes = [{
|
||||
get scope() {
|
||||
return a.D.body;
|
||||
return enviroment.D.body;
|
||||
},
|
||||
host: (t) => t ? t(a.D.body) : a.D.body,
|
||||
prevent: !0
|
||||
}], O = {
|
||||
host: (c) => c ? c(enviroment.D.body) : enviroment.D.body,
|
||||
prevent: true
|
||||
}];
|
||||
var scope = {
|
||||
/**
|
||||
* Gets the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
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() {
|
||||
return this.current.host;
|
||||
},
|
||||
/**
|
||||
* Prevents default behavior in the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
preventDefault() {
|
||||
let { current: t } = this;
|
||||
return t.prevent = !0, t;
|
||||
const { current } = this;
|
||||
current.prevent = true;
|
||||
return current;
|
||||
},
|
||||
/**
|
||||
* Gets a copy of the current scope stack
|
||||
* @returns {Array} Copy of scope stack
|
||||
*/
|
||||
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() {
|
||||
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() {
|
||||
if (g.length !== 1)
|
||||
return g.pop();
|
||||
if (scopes.length === 1) return;
|
||||
return scopes.pop();
|
||||
}
|
||||
};
|
||||
function k(...t) {
|
||||
return this.appendOriginal(...t), this;
|
||||
function append(...els) {
|
||||
this.appendOriginal(...els);
|
||||
return this;
|
||||
}
|
||||
function J(t) {
|
||||
return t.append === k || (t.appendOriginal = t.append, t.append = k), t;
|
||||
function chainableAppend(el) {
|
||||
if (el.append === append) return el;
|
||||
el.appendOriginal = el.append;
|
||||
el.append = append;
|
||||
return el;
|
||||
}
|
||||
var T;
|
||||
function P(t, e, ...r) {
|
||||
let n = S(this), o = 0, c, d;
|
||||
switch ((Object(e) !== e || n.isSignal(e)) && (e = { textContent: e }), !0) {
|
||||
case typeof t == "function": {
|
||||
o = 1;
|
||||
let f = (...l) => l.length ? (o === 1 ? r.unshift(...l) : l.forEach((E) => E(d)), void 0) : d;
|
||||
O.push({ scope: t, host: f }), c = t(e || void 0);
|
||||
let p = c instanceof a.F;
|
||||
if (c.nodeName === "#comment") break;
|
||||
let b = P.mark({
|
||||
var namespace;
|
||||
function createElement(tag, attributes, ...addons) {
|
||||
const s = signals(this);
|
||||
let scoped = 0;
|
||||
let el, el_host;
|
||||
if (Object(attributes) !== attributes || s.isSignal(attributes))
|
||||
attributes = { textContent: attributes };
|
||||
switch (true) {
|
||||
case typeof tag === "function": {
|
||||
scoped = 1;
|
||||
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",
|
||||
name: t.name,
|
||||
host: p ? "this" : "parentElement"
|
||||
name: tag.name,
|
||||
host: is_fragment ? "this" : "parentElement"
|
||||
});
|
||||
c.prepend(b), p && (d = b);
|
||||
el.prepend(el_mark);
|
||||
if (is_fragment) el_host = el_mark;
|
||||
break;
|
||||
}
|
||||
case t === "#text":
|
||||
c = R.call(this, a.D.createTextNode(""), e);
|
||||
case tag === "#text":
|
||||
el = assign.call(this, enviroment.D.createTextNode(""), attributes);
|
||||
break;
|
||||
case (t === "<>" || !t):
|
||||
c = R.call(this, a.D.createDocumentFragment(), e);
|
||||
case (tag === "<>" || !tag):
|
||||
el = assign.call(this, enviroment.D.createDocumentFragment(), attributes);
|
||||
break;
|
||||
case !!T:
|
||||
c = R.call(this, a.D.createElementNS(T, t), e);
|
||||
case Boolean(namespace):
|
||||
el = assign.call(this, enviroment.D.createElementNS(namespace, tag), attributes);
|
||||
break;
|
||||
case !c:
|
||||
c = R.call(this, a.D.createElement(t), e);
|
||||
case !el:
|
||||
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) {
|
||||
t = Object.entries(t).map(([o, c]) => o + `="${c}"`).join(" ");
|
||||
let r = e ? "" : "/", n = a.D.createComment(`<dde:mark ${t}${a.ssr}${r}>`);
|
||||
return e && (n.end = a.D.createComment("</dde:mark>")), n;
|
||||
createElement.mark = function(attrs, is_open = false) {
|
||||
attrs = Object.entries(attrs).map(([n, v]) => n + `="${v}"`).join(" ");
|
||||
const end = is_open ? "" : "/";
|
||||
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) {
|
||||
let e = this;
|
||||
return function(...n) {
|
||||
T = t;
|
||||
let o = P.call(e, ...n);
|
||||
return T = void 0, o;
|
||||
function createElementNS(ns) {
|
||||
const _this = this;
|
||||
return function createElementNSCurried(...rest) {
|
||||
namespace = ns;
|
||||
const el = createElement.call(_this, ...rest);
|
||||
namespace = void 0;
|
||||
return el;
|
||||
};
|
||||
}
|
||||
function lt(t, e = t) {
|
||||
let r = "\xB9\u2070", n = "\u2713", o = Object.fromEntries(
|
||||
Array.from(e.querySelectorAll("slot")).filter((c) => !c.name.endsWith(r)).map((c) => [c.name += r, c])
|
||||
function simulateSlots(element, root = element) {
|
||||
const mark_e = "\xB9\u2070", mark_s = "\u2713";
|
||||
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, {
|
||||
apply(c, d, f) {
|
||||
if (f[0] === e) return c.apply(t, f);
|
||||
for (let p of f) {
|
||||
let b = (p.slot || "") + r;
|
||||
element.append = new Proxy(element.append, {
|
||||
apply(orig, _, els) {
|
||||
if (els[0] === root) return orig.apply(element, els);
|
||||
for (const el of els) {
|
||||
const name = (el.slot || "") + mark_e;
|
||||
try {
|
||||
Q(p, "remove", "slot");
|
||||
} catch {
|
||||
elementAttribute(el, "remove", "slot");
|
||||
} catch (_error) {
|
||||
}
|
||||
let l = o[b];
|
||||
if (!l) return;
|
||||
l.name.startsWith(n) || (l.childNodes.forEach((E) => E.remove()), l.name = n + b), l.append(p);
|
||||
const slot = slots[name];
|
||||
if (!slot) return;
|
||||
if (!slot.name.startsWith(mark_s)) {
|
||||
slot.childNodes.forEach((c) => c.remove());
|
||||
slot.name = mark_s + name;
|
||||
}
|
||||
slot.append(el);
|
||||
}
|
||||
return t.append = c, t;
|
||||
element.append = orig;
|
||||
return element;
|
||||
}
|
||||
}), t !== e) {
|
||||
let c = Array.from(t.childNodes);
|
||||
t.append(...c);
|
||||
});
|
||||
if (element !== root) {
|
||||
const els = Array.from(element.childNodes);
|
||||
element.append(...els);
|
||||
}
|
||||
return e;
|
||||
return root;
|
||||
}
|
||||
var N = /* @__PURE__ */ new WeakMap(), { setDeleteAttr: $ } = a;
|
||||
function R(t, ...e) {
|
||||
if (!e.length) return t;
|
||||
N.set(t, H(t, this));
|
||||
for (let [r, n] of Object.entries(Object.assign({}, ...e)))
|
||||
U.call(this, t, r, n);
|
||||
return N.delete(t), t;
|
||||
var assign_context = /* @__PURE__ */ new WeakMap();
|
||||
var { setDeleteAttr: setDeleteAttr2 } = enviroment;
|
||||
function assign(element, ...attributes) {
|
||||
if (!attributes.length) return element;
|
||||
assign_context.set(element, assignContext(element, this));
|
||||
for (const [key, value] of Object.entries(oAssign({}, ...attributes)))
|
||||
assignAttribute.call(this, element, key, value);
|
||||
assign_context.delete(element);
|
||||
return element;
|
||||
}
|
||||
function U(t, e, r) {
|
||||
let { setRemoveAttr: n, s: o } = H(t, this), c = this;
|
||||
r = o.processReactiveAttribute(
|
||||
t,
|
||||
e,
|
||||
r,
|
||||
(f, p) => U.call(c, t, f, p)
|
||||
function assignAttribute(element, key, value) {
|
||||
const { setRemoveAttr, s } = assignContext(element, this);
|
||||
const _this = this;
|
||||
value = s.processReactiveAttribute(
|
||||
element,
|
||||
key,
|
||||
value,
|
||||
(key2, value2) => assignAttribute.call(_this, element, key2, value2)
|
||||
);
|
||||
let [d] = e;
|
||||
if (d === "=") return n(e.slice(1), r);
|
||||
if (d === ".") return F(t, e.slice(1), r);
|
||||
if (/(aria|data)([A-Z])/.test(e))
|
||||
return e = e.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(), n(e, r);
|
||||
switch (e === "className" && (e = "class"), e) {
|
||||
const [k] = key;
|
||||
if ("=" === k) return setRemoveAttr(key.slice(1), value);
|
||||
if ("." === k) return setDelete(element, key.slice(1), value);
|
||||
if (/(aria|data)([A-Z])/.test(key)) {
|
||||
key = key.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
||||
return setRemoveAttr(key, value);
|
||||
}
|
||||
if ("className" === key) key = "class";
|
||||
switch (key) {
|
||||
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":
|
||||
return $(t, e, r);
|
||||
return setDeleteAttr2(element, key, value);
|
||||
case "style":
|
||||
if (typeof r != "object") break;
|
||||
if (typeof value !== "object") break;
|
||||
/* falls through */
|
||||
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":
|
||||
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":
|
||||
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) {
|
||||
if (N.has(t)) return N.get(t);
|
||||
let n = (t instanceof a.S ? tt : Y).bind(null, t, "Attribute"), o = S(e);
|
||||
return { setRemoveAttr: n, s: o };
|
||||
function assignContext(element, _this) {
|
||||
if (assign_context.has(element)) return assign_context.get(element);
|
||||
const is_svg = isInstance(element, enviroment.S);
|
||||
const setRemoveAttr = (is_svg ? setRemoveNS : setRemove).bind(null, element, "Attribute");
|
||||
const s = signals(_this);
|
||||
return { setRemoveAttr, s };
|
||||
}
|
||||
function K(t, e) {
|
||||
let r = S(this);
|
||||
return M(
|
||||
r,
|
||||
function classListDeclarative(element, toggle) {
|
||||
const s = signals(this);
|
||||
forEachEntries(
|
||||
s,
|
||||
"classList",
|
||||
t,
|
||||
e,
|
||||
(n, o) => t.classList.toggle(n, o === -1 ? void 0 : !!o)
|
||||
), t;
|
||||
element,
|
||||
toggle,
|
||||
(class_name, val) => element.classList.toggle(class_name, val === -1 ? void 0 : Boolean(val))
|
||||
);
|
||||
return element;
|
||||
}
|
||||
function Q(t, e, r, n) {
|
||||
return t instanceof a.H ? t[e + "Attribute"](r, n) : t[e + "AttributeNS"](null, r, n);
|
||||
function elementAttribute(element, op, key, value) {
|
||||
if (isInstance(element, enviroment.H))
|
||||
return element[op + "Attribute"](key, value);
|
||||
return element[op + "AttributeNS"](null, key, value);
|
||||
}
|
||||
function X(t, e) {
|
||||
if (!(e in t)) return !1;
|
||||
let r = z(t, e);
|
||||
return !m(r.set);
|
||||
function isPropSetter(el, key) {
|
||||
if (!(key in el)) return false;
|
||||
const des = getPropDescriptor(el, key);
|
||||
return !isUndef(des.set);
|
||||
}
|
||||
function z(t, e) {
|
||||
if (t = Object.getPrototypeOf(t), !t) return {};
|
||||
let r = Object.getOwnPropertyDescriptor(t, e);
|
||||
return r || z(t, e);
|
||||
function getPropDescriptor(p, key) {
|
||||
p = Object.getPrototypeOf(p);
|
||||
if (!p) return {};
|
||||
const des = Object.getOwnPropertyDescriptor(p, key);
|
||||
if (!des) return getPropDescriptor(p, key);
|
||||
return des;
|
||||
}
|
||||
function M(t, e, r, n, o) {
|
||||
let c = String;
|
||||
if (!(typeof n != "object" || n === null))
|
||||
return Object.entries(n).forEach(function([f, p]) {
|
||||
f && (f = new c(f), f.target = e, p = t.processReactiveAttribute(r, f, p, o), o(f, p));
|
||||
});
|
||||
function forEachEntries(s, target, element, obj, cb) {
|
||||
const S = String;
|
||||
if (typeof obj !== "object" || obj === null) return;
|
||||
return Object.entries(obj).forEach(function process([key, val]) {
|
||||
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) {
|
||||
return t[(m(n) ? "remove" : "set") + e](r, n);
|
||||
function setRemove(obj, prop, key, val) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop](key, val);
|
||||
}
|
||||
function tt(t, e, r, n, o = null) {
|
||||
return t[(m(n) ? "remove" : "set") + e + "NS"](o, r, n);
|
||||
function setRemoveNS(obj, prop, key, val, ns = null) {
|
||||
return obj[(isUndef(val) ? "remove" : "set") + prop + "NS"](ns, key, val);
|
||||
}
|
||||
function F(t, e, r) {
|
||||
if (Reflect.set(t, e, r), !!m(r))
|
||||
return Reflect.deleteProperty(t, e);
|
||||
function setDelete(obj, key, val) {
|
||||
Reflect.set(obj, key, val);
|
||||
if (!isUndef(val)) return;
|
||||
return Reflect.deleteProperty(obj, key);
|
||||
}
|
||||
|
||||
// src/events-observer.js
|
||||
var _ = a.M ? et() : new Proxy({}, {
|
||||
var c_ch_o = enviroment.M ? connectionsChangesObserverConstructor() : new Proxy({}, {
|
||||
get() {
|
||||
return () => {
|
||||
};
|
||||
}
|
||||
});
|
||||
function et() {
|
||||
let t = /* @__PURE__ */ new Map(), e = !1, r = (s) => function(u) {
|
||||
for (let i of u)
|
||||
if (i.type === "childList") {
|
||||
if (l(i.addedNodes, !0)) {
|
||||
s();
|
||||
continue;
|
||||
}
|
||||
E(i.removedNodes, !0) && s();
|
||||
function connectionsChangesObserverConstructor() {
|
||||
const store = /* @__PURE__ */ new Map();
|
||||
let is_observing = false;
|
||||
const observerListener = (stop2) => function(mutations) {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type !== "childList") continue;
|
||||
if (observerAdded(mutation.addedNodes, true)) {
|
||||
stop2();
|
||||
continue;
|
||||
}
|
||||
}, 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));
|
||||
if (observerRemoved(mutation.removedNodes, true))
|
||||
stop2();
|
||||
}
|
||||
};
|
||||
function o(s, u) {
|
||||
u.length_c || u.length_d || (t.delete(s), f());
|
||||
const observer = new enviroment.M(observerListener(stop));
|
||||
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 cleanWhenOff(element, ls) {
|
||||
if (ls.length_c || ls.length_d)
|
||||
return;
|
||||
store.delete(element);
|
||||
stop();
|
||||
}
|
||||
function c(s) {
|
||||
if (t.has(s)) return t.get(s);
|
||||
let u = {
|
||||
function getElementStore(element) {
|
||||
if (store.has(element)) return store.get(element);
|
||||
const out = {
|
||||
connected: /* @__PURE__ */ new WeakSet(),
|
||||
length_c: 0,
|
||||
disconnected: /* @__PURE__ */ new WeakSet(),
|
||||
length_d: 0
|
||||
};
|
||||
return t.set(s, u), u;
|
||||
store.set(element, out);
|
||||
return out;
|
||||
}
|
||||
function d() {
|
||||
e || (e = !0, n.observe(a.D.body, { childList: !0, subtree: !0 }));
|
||||
function start() {
|
||||
if (is_observing) return;
|
||||
is_observing = true;
|
||||
observer.observe(enviroment.D.body, { childList: true, subtree: true });
|
||||
}
|
||||
function f() {
|
||||
!e || t.size || (e = !1, n.disconnect());
|
||||
function stop() {
|
||||
if (!is_observing || store.size) return;
|
||||
is_observing = false;
|
||||
observer.disconnect();
|
||||
}
|
||||
function p() {
|
||||
return new Promise(function(s) {
|
||||
(requestIdleCallback || requestAnimationFrame)(s);
|
||||
function requestIdle() {
|
||||
return new Promise(function(resolve) {
|
||||
(requestIdleCallback || requestAnimationFrame)(resolve);
|
||||
});
|
||||
}
|
||||
async function b(s) {
|
||||
t.size > 30 && await p();
|
||||
let u = [];
|
||||
if (!(s instanceof Node)) return u;
|
||||
for (let i of t.keys())
|
||||
i === s || !(i instanceof Node) || s.contains(i) && u.push(i);
|
||||
return u;
|
||||
}
|
||||
function l(s, u) {
|
||||
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);
|
||||
async function collectChildren(element) {
|
||||
if (store.size > 30)
|
||||
await requestIdle();
|
||||
const out = [];
|
||||
if (!isInstance(element, Node)) return out;
|
||||
for (const el of store.keys()) {
|
||||
if (el === element || !isInstance(el, Node)) continue;
|
||||
if (element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
return i;
|
||||
return out;
|
||||
}
|
||||
function E(s, u) {
|
||||
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 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;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
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 () => {
|
||||
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
|
||||
function wt(t, e, r = rt) {
|
||||
let n = t.host || t;
|
||||
O.push({
|
||||
scope: n,
|
||||
host: (...d) => d.length ? d.forEach((f) => f(n)) : n
|
||||
}), typeof r == "function" && (r = r.call(n, n));
|
||||
let o = n[x];
|
||||
o || nt(n);
|
||||
let c = e.call(n, r);
|
||||
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);
|
||||
function customElementRender(target, render, props = observedAttributes2) {
|
||||
const custom_element = target.host || target;
|
||||
scope.push({
|
||||
scope: custom_element,
|
||||
host: (...c) => c.length ? c.forEach((c2) => c2(custom_element)) : custom_element
|
||||
});
|
||||
if (typeof props === "function") props = props.call(custom_element, custom_element);
|
||||
const is_lte = custom_element[keyLTE];
|
||||
if (!is_lte) lifecyclesToEvents(custom_element);
|
||||
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) {
|
||||
return j(t.prototype, "connectedCallback", function(e, r, n) {
|
||||
e.apply(r, n), r.dispatchEvent(new Event(v));
|
||||
}), j(t.prototype, "disconnectedCallback", function(e, r, n) {
|
||||
e.apply(r, n), (globalThis.queueMicrotask || setTimeout)(
|
||||
() => !r.isConnected && r.dispatchEvent(new Event(w))
|
||||
function lifecyclesToEvents(class_declaration) {
|
||||
wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail) {
|
||||
target.apply(thisArg, detail);
|
||||
thisArg.dispatchEvent(new Event(evc));
|
||||
});
|
||||
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;
|
||||
r.dispatchEvent(new CustomEvent(y, {
|
||||
detail: [o, c]
|
||||
})), e.apply(r, n);
|
||||
}), t.prototype[x] = !0, t;
|
||||
});
|
||||
wrapMethod(class_declaration.prototype, "attributeChangedCallback", function(target, thisArg, detail) {
|
||||
const [attribute, , value] = detail;
|
||||
thisArg.dispatchEvent(new CustomEvent(eva, {
|
||||
detail: [attribute, value]
|
||||
}));
|
||||
target.apply(thisArg, detail);
|
||||
});
|
||||
class_declaration.prototype[keyLTE] = true;
|
||||
return class_declaration;
|
||||
}
|
||||
function j(t, e, r) {
|
||||
t[e] = new Proxy(t[e] || (() => {
|
||||
}), { apply: r });
|
||||
function wrapMethod(obj, method, apply) {
|
||||
obj[method] = new Proxy(obj[method] || (() => {
|
||||
}), { apply });
|
||||
}
|
||||
function rt(t) {
|
||||
return q(t, (e, r) => e.getAttribute(r));
|
||||
function observedAttributes2(instance) {
|
||||
return observedAttributes(instance, (i, n) => i.getAttribute(n));
|
||||
}
|
||||
|
||||
// src/events.js
|
||||
function yt(t, e, r) {
|
||||
return e || (e = {}), function(o, ...c) {
|
||||
r && (c.unshift(o), o = typeof r == "function" ? r() : r);
|
||||
let d = c.length ? new CustomEvent(t, Object.assign({ detail: c[0] }, e)) : new Event(t, e);
|
||||
return o.dispatchEvent(d);
|
||||
function dispatchEvent(name, options, host) {
|
||||
if (!options) options = {};
|
||||
return function dispatch(element, ...d) {
|
||||
if (host) {
|
||||
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) {
|
||||
return function(o) {
|
||||
return o.addEventListener(t, e, r), o;
|
||||
function on(event, listener, options) {
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(event, listener, options);
|
||||
return element;
|
||||
};
|
||||
}
|
||||
var B = (t) => Object.assign({}, typeof t == "object" ? t : null, { once: !0 });
|
||||
D.connected = function(t, e) {
|
||||
return e = B(e), function(n) {
|
||||
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);
|
||||
var lifeOptions = (obj) => oAssign({}, typeof obj === "object" ? obj : null, { once: true });
|
||||
on.connected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
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) {
|
||||
return e = B(e), function(n) {
|
||||
return n.addEventListener(w, t, e), n[x] || L(e.signal, () => _.offDisconnected(n, t)) && _.onDisconnected(n, t), n;
|
||||
on.disconnected = function(listener, options) {
|
||||
options = lifeOptions(options);
|
||||
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();
|
||||
D.disconnectedAsAbort = function(t) {
|
||||
if (W.has(t)) return W.get(t);
|
||||
let e = new AbortController();
|
||||
return W.set(t, e), t(D.disconnected(() => e.abort())), e;
|
||||
var store_abort = /* @__PURE__ */ new WeakMap();
|
||||
on.disconnectedAsAbort = function(host) {
|
||||
if (store_abort.has(host)) return store_abort.get(host);
|
||||
const a = new AbortController();
|
||||
store_abort.set(host, a);
|
||||
host(on.disconnected(() => a.abort()));
|
||||
return a;
|
||||
};
|
||||
var ot = /* @__PURE__ */ new WeakSet();
|
||||
D.attributeChanged = function(t, e) {
|
||||
return typeof e != "object" && (e = {}), function(n) {
|
||||
if (n.addEventListener(y, t, e), n[x] || ot.has(n) || !a.M) return n;
|
||||
let o = new a.M(function(d) {
|
||||
for (let { attributeName: f, target: p } of d)
|
||||
p.dispatchEvent(
|
||||
new CustomEvent(y, { detail: [f, p.getAttribute(f)] })
|
||||
var els_attribute_store = /* @__PURE__ */ new WeakSet();
|
||||
on.attributeChanged = function(listener, options) {
|
||||
if (typeof options !== "object")
|
||||
options = {};
|
||||
return function registerElement(element) {
|
||||
element.addEventListener(eva, listener, options);
|
||||
if (element[keyLTE] || els_attribute_store.has(element))
|
||||
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 {
|
||||
R as assign,
|
||||
U as assignAttribute,
|
||||
J as chainableAppend,
|
||||
K as classListDeclarative,
|
||||
P as createElement,
|
||||
pt as createElementNS,
|
||||
wt as customElementRender,
|
||||
nt as customElementWithDDE,
|
||||
yt as dispatchEvent,
|
||||
P as el,
|
||||
pt as elNS,
|
||||
Q as elementAttribute,
|
||||
nt as lifecyclesToEvents,
|
||||
rt as observedAttributes,
|
||||
D as on,
|
||||
dt as queue,
|
||||
Z as registerReactivity,
|
||||
O as scope,
|
||||
lt as simulateSlots
|
||||
assign,
|
||||
assignAttribute,
|
||||
chainableAppend,
|
||||
classListDeclarative,
|
||||
createElement,
|
||||
createElementNS,
|
||||
customElementRender,
|
||||
lifecyclesToEvents as customElementWithDDE,
|
||||
dispatchEvent,
|
||||
createElement as el,
|
||||
createElementNS as elNS,
|
||||
elementAttribute,
|
||||
lifecyclesToEvents,
|
||||
observedAttributes2 as observedAttributes,
|
||||
on,
|
||||
queue,
|
||||
registerReactivity,
|
||||
scope,
|
||||
simulateSlots
|
||||
};
|
||||
|
@ -1,28 +1,179 @@
|
||||
import { registerClientFile, styles } from "../ssr.js";
|
||||
const host= "."+code.name;
|
||||
styles.css`
|
||||
${host}{
|
||||
--shiki-color-text: #e9eded;
|
||||
--shiki-color-background: #212121;
|
||||
/* Code block styling */
|
||||
${host} {
|
||||
/* 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-string: #c3e88d;
|
||||
--shiki-token-comment: #546e7a;
|
||||
--shiki-token-keyword: #c792ea;
|
||||
--shiki-token-parameter: #AA0000;
|
||||
--shiki-token-parameter: #fd971f;
|
||||
--shiki-token-function: #80cbae;
|
||||
--shiki-token-string-expression: #c3e88d;
|
||||
--shiki-token-punctuation: var(--code);
|
||||
--shiki-token-link: #EE0000;
|
||||
--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;
|
||||
|
||||
/* Basic styling */
|
||||
white-space: pre;
|
||||
tab-size: 2; /* TODO: allow custom tab size?! */
|
||||
overflow: scroll;
|
||||
tab-size: 2;
|
||||
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-radius: var(--standard-border-radius);
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 18.4px; /* to fix shift when → dataJS=done */
|
||||
padding: 1rem 1.4rem;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1rem;
|
||||
background-color: var(--code-bg);
|
||||
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";
|
||||
@ -52,12 +203,46 @@ let is_registered= {};
|
||||
function registerClientPart(page_id){
|
||||
if(is_registered[page_id]) return;
|
||||
|
||||
// Add Shiki with a more reliable loading method
|
||||
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(
|
||||
new URL("./code.js.js", import.meta.url),
|
||||
el("script", { type: "module" })
|
||||
scriptElement
|
||||
);
|
||||
|
||||
is_registered[page_id]= true;
|
||||
}
|
||||
|
@ -1,12 +1,61 @@
|
||||
const highlighter= await globalThis.shiki.getHighlighter({
|
||||
theme: "css-variables",
|
||||
langs: ["js", "ts", "css", "html", "shell"],
|
||||
});
|
||||
const codeBlocks= document.querySelectorAll('code[class*="language-"]');
|
||||
try {
|
||||
// Initialize Shiki with our custom theme
|
||||
const highlighter = await globalThis.shiki.getHighlighter({
|
||||
theme: "css-variables",
|
||||
langs: ["javascript", "typescript", "css", "html", "shell"],
|
||||
});
|
||||
|
||||
codeBlocks.forEach((block)=> {
|
||||
const lang= block.className.replace("language-", "");
|
||||
block.parentElement.dataset.js= "done";
|
||||
const html= highlighter.codeToHtml(block.textContent, { lang });
|
||||
block.innerHTML= html;
|
||||
});
|
||||
// Find all code blocks that need highlighting
|
||||
const codeBlocks = document.querySelectorAll('div[data-js="todo"] code[class*="language-"]');
|
||||
|
||||
// Process each code block
|
||||
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";
|
||||
});
|
||||
}
|
||||
|
@ -1,16 +1,103 @@
|
||||
import { styles } from "../ssr.js";
|
||||
const host= "."+example.name;
|
||||
styles.css`
|
||||
${host}{
|
||||
${host} {
|
||||
grid-column: full-main;
|
||||
width: 100%;
|
||||
max-width: calc(9/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 {
|
||||
background: #212121 !important;
|
||||
border: 1px solid white;
|
||||
background: var(--code-bg) !important;
|
||||
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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -26,7 +26,7 @@ function ddeComponent({ attr }){
|
||||
on.connected(e=> console.log(( /** @type {HTMLParagraphElement} */ (e.target)).outerHTML)),
|
||||
);
|
||||
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);
|
||||
|
@ -4,11 +4,11 @@ const threePS= ({ emoji= "🚀" })=> {
|
||||
const clicks= S(0); // A
|
||||
return el().append(
|
||||
el("p", S(()=>
|
||||
"Hello World "+emoji.repeat(clicks()) // B
|
||||
"Hello World "+emoji.repeat(clicks.get()) // B
|
||||
)),
|
||||
el("button", {
|
||||
type: "button",
|
||||
onclick: ()=> clicks(clicks()+1), // C
|
||||
onclick: ()=> clicks.set(clicks.get()+1), // C
|
||||
textContent: "Fire",
|
||||
})
|
||||
);
|
||||
|
@ -15,4 +15,4 @@ setTimeout(
|
||||
clearInterval,
|
||||
10*interval,
|
||||
setInterval(oninterval, interval)
|
||||
);
|
||||
);
|
@ -22,9 +22,9 @@ const onsubmit= on("submit", function(event){
|
||||
S.action(todos, "push", data.get("todo"));
|
||||
break;
|
||||
case "E"/*dit*/: {
|
||||
const last= todos().at(-1);
|
||||
const last= todos.get().at(-1);
|
||||
if(!last) break;
|
||||
last(data.get("todo"));
|
||||
last.set(data.get("todo"));
|
||||
break;
|
||||
}
|
||||
case "R"/*emove*/:
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { S } from "deka-dom-el/signals";
|
||||
const signal= S(0);
|
||||
// computation pattern
|
||||
const double= S(()=> 2*signal());
|
||||
const double= S(()=> 2*signal.get());
|
||||
|
||||
const ac= new AbortController();
|
||||
S.on(signal, v=> console.log("signal", 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 id= setInterval(()=> signal(signal()+1), interval);
|
||||
const id= setInterval(()=> signal.set(signal.get()+1), interval);
|
||||
ac.signal.addEventListener("abort",
|
||||
()=> setTimeout(()=> clearInterval(id), 2*interval));
|
||||
|
||||
setTimeout(()=> ac.abort(), 3*interval)
|
||||
setTimeout(()=> ac.abort(), 3*interval)
|
@ -3,8 +3,8 @@ const count= S(0);
|
||||
|
||||
import { el } from "deka-dom-el";
|
||||
document.body.append(
|
||||
el("p", S(()=> "Currently: "+count())),
|
||||
el("p", { classList: { red: S(()=> count()%2 === 0) }, dataset: { count }, textContent: "Attributes example" }),
|
||||
el("p", S(()=> "Currently: "+count.get())),
|
||||
el("p", { classList: { red: S(()=> count.get()%2 === 0) }, dataset: { count }, textContent: "Attributes example" }),
|
||||
);
|
||||
document.head.append(
|
||||
el("style", ".red { color: red; }")
|
||||
@ -12,4 +12,4 @@ document.head.append(
|
||||
|
||||
const interval= 5 * 1000;
|
||||
setTimeout(clearInterval, 10*interval,
|
||||
setInterval(()=> count(count()+1), interval));
|
||||
setInterval(()=> count.set(count.get()+1), interval));
|
@ -2,7 +2,7 @@ import { S } from "deka-dom-el/signals";
|
||||
const count= S(0, {
|
||||
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); }
|
||||
});
|
||||
|
||||
@ -22,5 +22,5 @@ document.body.append(
|
||||
const interval= 5*1000;
|
||||
setTimeout(clearInterval, 10*interval, setInterval(function(){
|
||||
S.action(count, "add");
|
||||
S.action(numbers, "push", count());
|
||||
}, interval));
|
||||
S.action(numbers, "push", count.get());
|
||||
}, interval));
|
@ -4,9 +4,9 @@ const signal= S(0);
|
||||
// β — just reacts on signal changes
|
||||
S.on(signal, console.log);
|
||||
// γ — just updates the value
|
||||
const update= ()=> signal(signal()+1);
|
||||
const update= ()=> signal.set(signal.get()+1);
|
||||
|
||||
update();
|
||||
const interval= 5*1000;
|
||||
setTimeout(clearInterval, 10*interval,
|
||||
setInterval(update, interval));
|
||||
setInterval(update, interval));
|
@ -1,18 +1,67 @@
|
||||
import { pages, styles } from "../ssr.js";
|
||||
const host= "."+prevNext.name;
|
||||
styles.css`
|
||||
${host}{
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
margin-top: 1rem;
|
||||
/* Previous/Next navigation */
|
||||
${host} {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 3rem;
|
||||
padding-top: 1.5rem;
|
||||
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;
|
||||
text-align: right;
|
||||
|
||||
${host} a:hover {
|
||||
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";
|
||||
@ -22,21 +71,28 @@ import { el } from "../../index.js";
|
||||
* @param {string} [attrs.id]
|
||||
* */
|
||||
export function h3({ textContent, id }){
|
||||
if(!id) id= "h-"+textContent.toLowerCase().replaceAll(/\s/g, "-").replaceAll(/[^a-z-]/g, "");
|
||||
return el("h3", { id }).append(
|
||||
el("a", { textContent: "#", href: "#"+id, tabIndex: -1 }),
|
||||
" ", textContent
|
||||
);
|
||||
if(!id) id= "h-"+textContent.toLowerCase().replaceAll(/\s/g, "-").replaceAll(/[^a-z-]/g, "");
|
||||
return el("h3", { id }).append(
|
||||
el("a", {
|
||||
className: "heading-anchor",
|
||||
href: "#"+id,
|
||||
textContent: "#",
|
||||
title: `Link to this section: ${textContent}`,
|
||||
"aria-label": `Link to section ${textContent}`
|
||||
}),
|
||||
" ",
|
||||
textContent,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {import("../types.d.ts").Info} page
|
||||
* */
|
||||
export function prevNext(page){
|
||||
const page_index= pages.indexOf(page);
|
||||
return el("div", { className: prevNext.name }).append(
|
||||
el(pageLink, { rel: "prev", page: pages[page_index-1] }),
|
||||
el(pageLink, { rel: "next", page: pages[page_index+1] })
|
||||
);
|
||||
const page_index= pages.indexOf(page);
|
||||
return el("div", { className: prevNext.name }).append(
|
||||
el(pageLink, { rel: "prev", page: pages[page_index-1] }),
|
||||
el(pageLink, { rel: "next", page: pages[page_index+1] })
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @param {Object} attrs
|
||||
@ -44,11 +100,22 @@ export function prevNext(page){
|
||||
* @param {import("../types.d.ts").Info} [attrs.page]
|
||||
* */
|
||||
function pageLink({ rel, page }){
|
||||
if(!page) return el();
|
||||
let { href, title, description }= page;
|
||||
return el("a", { rel, href, title: description }).append(
|
||||
rel==="next" ?"(next) " : "",
|
||||
title,
|
||||
rel==="prev" ? " (previous)" : "",
|
||||
);
|
||||
if(!page) return el();
|
||||
let { href, title, description }= page;
|
||||
|
||||
// Find the page index to show numbering
|
||||
const pageIndex = pages.findIndex(p => p === page);
|
||||
const pageNumber = pageIndex + 1;
|
||||
|
||||
const linkTitle = rel === "prev"
|
||||
? `Previous: ${pageNumber}. ${title}`
|
||||
: `Next: ${pageNumber}. ${title}`;
|
||||
|
||||
return el("a", {
|
||||
rel,
|
||||
href,
|
||||
title: description || linkTitle
|
||||
}).append(
|
||||
title
|
||||
);
|
||||
}
|
||||
|
@ -1,124 +1,426 @@
|
||||
import { styles } from "./ssr.js";
|
||||
styles.css`
|
||||
@import url(https://cdn.simplecss.org/simple.min.css);
|
||||
:root{
|
||||
--body-max-width: 45rem;
|
||||
--marked: #fb3779;
|
||||
--code: #0d47a1;
|
||||
--accent: #d81b60;
|
||||
:root {
|
||||
--primary: #b71c1c;
|
||||
--primary-light: #f05545;
|
||||
--primary-dark: #7f0000;
|
||||
--primary-rgb: 183, 28, 28;
|
||||
--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 {
|
||||
--accent: #f06292;
|
||||
--code: #62c1f0;
|
||||
--bg: #121212;
|
||||
--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%;
|
||||
grid-template-areas: "header" "sidebar" "content";
|
||||
}
|
||||
@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;
|
||||
|
||||
/* Base styling */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body > nav ol li,
|
||||
body > nav ul li {
|
||||
display:inline-block
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body > nav a,
|
||||
body > nav a:visited {
|
||||
margin: 0 .5rem 1rem .5rem;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: var(--standard-border-radius);
|
||||
color: var(--text-light);
|
||||
display: inline-block;
|
||||
padding: .1rem 1rem;
|
||||
|
||||
/* Accessibility improvements */
|
||||
:focus {
|
||||
outline: 3px solid rgba(63, 81, 181, 0.5);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
: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;
|
||||
cursor: pointer;
|
||||
transition: all .15s;
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
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);
|
||||
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);
|
||||
color: var(--accent);
|
||||
|
||||
::selection {
|
||||
background-color: var(--selection);
|
||||
}
|
||||
@media only screen and (max-width:720px) {
|
||||
body > nav{
|
||||
flex-flow: row wrap;
|
||||
padding-block: .5rem;
|
||||
}
|
||||
body > nav a {
|
||||
border:none;
|
||||
text-decoration:underline;
|
||||
margin-block: .1rem;
|
||||
padding-block:.1rem;
|
||||
line-height: 1rem;
|
||||
font-size: .9rem;
|
||||
|
||||
/* Typography */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
h1 {
|
||||
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;
|
||||
padding: 2rem;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
[full-main-start] 1fr
|
||||
[main-start] min(var(--body-max-width), 90%) [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;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
vertical-align: sub;
|
||||
padding-right: .25rem;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1.3em;
|
||||
margin-right: 0.2rem;
|
||||
height: 1em;
|
||||
vertical-align: -0.125em;
|
||||
stroke-width: 0;
|
||||
stroke: currentColor;
|
||||
fill: currentColor;
|
||||
}
|
||||
.note{
|
||||
font-size: .9rem;
|
||||
font-style: italic;
|
||||
|
||||
/* Information blocks */
|
||||
.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;
|
||||
}
|
||||
`;
|
||||
|
@ -1,38 +1,239 @@
|
||||
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 {import("../types.d.ts").Info} def.info
|
||||
* @param {import("../types.d.ts").Pkg} def.pkg Package information.
|
||||
* */
|
||||
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(
|
||||
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(
|
||||
el("header").append(
|
||||
el("h1", title),
|
||||
el("p", description)
|
||||
),
|
||||
el("nav").append(
|
||||
el("a", { href: pkg.homepage }).append(
|
||||
el(iconGitHub),
|
||||
"GitHub"
|
||||
// Header section with accessibility support
|
||||
el("header", { role: "banner", className: header.name }).append(
|
||||
el("div", { className: "header-title" }).append(
|
||||
el("a", {
|
||||
href: pkg.homepage,
|
||||
className: "github-link",
|
||||
"aria-label": "View on GitHub",
|
||||
target: "_blank",
|
||||
rel: "noopener noreferrer"
|
||||
}).append(
|
||||
el(iconGitHub),
|
||||
),
|
||||
el("h1").append(
|
||||
el("a", { href: pages[0].href, textContent: pkg.name, title: "Go to documentation homepage" }),
|
||||
),
|
||||
el("span", {
|
||||
className: "version-badge",
|
||||
"aria-label": "Version",
|
||||
textContent: pkg.version || ""
|
||||
})
|
||||
),
|
||||
...pages.map((p, i)=> el("a", {
|
||||
href: p.href==="index" ? "./" : p.href,
|
||||
textContent: (i+1) + ". " + p.title,
|
||||
title: p.description,
|
||||
classList: { current: p.href===href }
|
||||
}))
|
||||
)
|
||||
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 }){
|
||||
return el().append(
|
||||
el("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
|
||||
el("meta", { name: "description", content: description }),
|
||||
el("meta", { name: "theme-color", content: "#b71c1c" }),
|
||||
el("title", title),
|
||||
el(metaAuthor),
|
||||
el(metaTwitter, pkg),
|
||||
|
@ -7,9 +7,25 @@ import { prevNext } from "../components/pageUtils.html.js";
|
||||
/** @param {Pick<import("../types.d.ts").PageAttrs, "pkg" | "info">} attrs */
|
||||
export function simplePage({ pkg, info }){
|
||||
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("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"),
|
||||
|
||||
// Navigation between pages
|
||||
el(prevNext, info)
|
||||
)
|
||||
));
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { T, t } from "./utils/index.js";
|
||||
export const info= {
|
||||
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";
|
||||
@ -69,9 +69,9 @@ export function page({ pkg, info }){
|
||||
some manner independent and still connected to the same reactive entity.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
Signals are implemented in the library as functions. To see current value of signal, just call it without
|
||||
any arguments ${el("code", "console.log(signal())")}. To update the signal value, pass any argument
|
||||
${el("code", `signal('${t`a new value`}')`)}. For listenning the signal value changes, use
|
||||
Signals are implemented in the library as objects with methods. To see current value of signal,
|
||||
call the get method ${el("code", "console.log(signal.get())")}. To update the signal value, use the set method
|
||||
${el("code", `signal.set('${t`a new value`}')`)}. For listenning the signal value changes, use
|
||||
${el("code", "S.on(signal, console.log)")}.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
@ -100,10 +100,10 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
|
||||
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("li", t`to change some attribute(s) of existing element(s)`),
|
||||
el("li", t`to generate elements itself dynamically – this covers conditions and loops`)
|
||||
el("li", t`Reactive attributes: Update properties, attributes, and styles of existing elements`),
|
||||
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("p").append(...T`
|
||||
@ -114,7 +114,7 @@ export function page({ pkg, info }){
|
||||
`),
|
||||
el("p").append(...T`
|
||||
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.
|
||||
`),
|
||||
el("p").append(...T`
|
||||
|
@ -15,7 +15,7 @@ export function thirdParty(){
|
||||
}, {
|
||||
set(key, value){
|
||||
const p= this.value[key] || S();
|
||||
p(value);
|
||||
p.set(value);
|
||||
this.value[key]= p;
|
||||
}
|
||||
});
|
||||
@ -32,7 +32,7 @@ export function thirdParty(){
|
||||
})();
|
||||
return el("input", {
|
||||
className,
|
||||
value: store().value(),
|
||||
value: store.get().value.get(),
|
||||
type: "text",
|
||||
onchange: ev=> S.action(store, "set", "value", ev.target.value)
|
||||
});
|
||||
|
@ -9,24 +9,24 @@ export function fullNameComponent(){
|
||||
const labels= [ "Name", "Surname" ];
|
||||
const name= labels.map(_=> S(""));
|
||||
const full_name= S(()=>
|
||||
name.map(l=> l()).filter(Boolean).join(" ") || "-");
|
||||
name.map(l=> l.get()).filter(Boolean).join(" ") || "-");
|
||||
scope.host(
|
||||
on.connected(()=> console.log(fullNameComponent)),
|
||||
on.disconnected(()=> console.log(fullNameComponent))
|
||||
);
|
||||
|
||||
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 elSVG= elNS("http://www.w3.org/2000/svg");
|
||||
return el("div", { className }).append(
|
||||
el("h2", "Simple form:"),
|
||||
el("p", { textContent: S(()=> "Count: "+count()),
|
||||
dataset: { count }, classList: { count: S(() => count()%2 === 0) } }),
|
||||
el("p", { textContent: S(()=> "Count: "+count.get()),
|
||||
dataset: { count }, classList: { count: S(() => count.get()%2 === 0) } }),
|
||||
el("form", { onsubmit: ev=> ev.preventDefault() }).append(
|
||||
...name.map((v, i)=>
|
||||
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(
|
||||
|
@ -58,7 +58,7 @@ export function todosComponent({ todos= [ "Task A" ] }= {}){
|
||||
),
|
||||
el("div").append(
|
||||
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 onedited= on("change", ev=> {
|
||||
const el= /** @type {HTMLInputElement} */ (ev.target);
|
||||
textContent(el.value);
|
||||
is_editable(false);
|
||||
textContent.set(el.value);
|
||||
is_editable.set(false);
|
||||
});
|
||||
return el("li").append(
|
||||
S.el(is_editable, is=> is
|
||||
? el("input", { value: textContent(), type: "text" }, onedited)
|
||||
: el("span", { textContent, onclick: is_editable.bind(null, true) })
|
||||
? el("input", { value: textContent.get(), type: "text" }, onedited)
|
||||
: el("span", { textContent, onclick: ()=> is_editable.set(true) })
|
||||
),
|
||||
el("button", { type: "button", value, textContent: "-" }, onclick)
|
||||
);
|
||||
|
@ -34,7 +34,7 @@ export class CustomHTMLTestElement extends HTMLElement{
|
||||
text(test),
|
||||
text(name),
|
||||
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" }),
|
||||
);
|
||||
|
@ -20,7 +20,7 @@ document.body.append(
|
||||
el("span", { textContent: "test", slot: "test" }),
|
||||
),
|
||||
el(thirdParty),
|
||||
el(CustomSlottingHTMLElement.tagName, { onclick: ()=> toggle(!toggle()) }).append(
|
||||
el(CustomSlottingHTMLElement.tagName, { onclick: ()=> toggle.set(!toggle.get()) }).append(
|
||||
el("strong", { slot: "name", textContent: "Honzo" }),
|
||||
S.el(toggle, is=> is
|
||||
? el("span", "…default slot")
|
||||
|
9
index.d.ts
vendored
9
index.d.ts
vendored
@ -52,9 +52,12 @@ type IsReadonly<T, K extends keyof T> =
|
||||
* @private
|
||||
*/
|
||||
type ElementAttributes<T extends SupportedElement>= Partial<{
|
||||
[K in keyof _fromElsInterfaces<T>]: IsReadonly<_fromElsInterfaces<T>, K> extends false
|
||||
? _fromElsInterfaces<T>[K] | ddeSignal<_fromElsInterfaces<T>[K]>
|
||||
: ddeStringable
|
||||
[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]>
|
||||
: ddeStringable)
|
||||
} & AttrsModified> & Record<string, any>;
|
||||
export function classListDeclarative<El extends SupportedElement>(
|
||||
element: El,
|
||||
|
11
package.json
11
package.json
@ -29,8 +29,8 @@
|
||||
"types": "./jsdom.d.ts"
|
||||
},
|
||||
"./src/signals-lib": {
|
||||
"import": "./src/signals-lib.js",
|
||||
"types": "./src/signals-lib.d.ts"
|
||||
"import": "./src/signals-lib/signals-lib.js",
|
||||
"types": "./src/signals-lib/signals-lib.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
@ -53,7 +53,8 @@
|
||||
"globals": {
|
||||
"requestIdleCallback": false,
|
||||
"AbortController": false,
|
||||
"AbortSignal": false
|
||||
"AbortSignal": false,
|
||||
"FinalizationRegistry": false
|
||||
}
|
||||
},
|
||||
"size-limit": [
|
||||
@ -65,7 +66,7 @@
|
||||
},
|
||||
{
|
||||
"path": "./signals.js",
|
||||
"limit": "12 kB",
|
||||
"limit": "12.5 kB",
|
||||
"gzip": false,
|
||||
"brotli": false
|
||||
},
|
||||
@ -77,7 +78,7 @@
|
||||
},
|
||||
{
|
||||
"path": "./index-with-signals.js",
|
||||
"limit": "5.25 kB"
|
||||
"limit": "5.5 kB"
|
||||
}
|
||||
],
|
||||
"modifyEsbuildConfig": {
|
||||
|
12
signals.d.ts
vendored
12
signals.d.ts
vendored
@ -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 SymbolSignal= Symbol;
|
||||
type SymbolOnclear= symbol;
|
||||
@ -27,7 +35,7 @@ interface signal{
|
||||
* ```js
|
||||
* const name= S("Jan");
|
||||
* 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 actions Use to define actions on the signal. Such as add item to the array.
|
||||
|
@ -1,4 +1,4 @@
|
||||
export { signal, S, isSignal } from "./src/signals-lib.js";
|
||||
import { signals_config } from "./src/signals-lib.js";
|
||||
import { registerReactivity } from "./src/signals-common.js";
|
||||
export { signal, S, isSignal } from "./src/signals-lib/signals-lib.js";
|
||||
import { signals_config } from "./src/signals-lib/signals-lib.js";
|
||||
import { registerReactivity } from "./src/signals-lib/common.js";
|
||||
registerReactivity(signals_config);
|
||||
|
@ -1,6 +1,15 @@
|
||||
import { keyLTE, evc, evd, eva } from "./dom-common.js";
|
||||
import { scope } from "./dom.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){
|
||||
const custom_element= target.host || target;
|
||||
scope.push({
|
||||
@ -17,6 +26,13 @@ export function customElementRender(target, render, props= observedAttributes){
|
||||
scope.pop();
|
||||
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){
|
||||
wrapMethod(class_declaration.prototype, "connectedCallback", function(target, thisArg, detail){
|
||||
target.apply(thisArg, detail);
|
||||
@ -38,12 +54,30 @@ export function lifecyclesToEvents(class_declaration){
|
||||
class_declaration.prototype[keyLTE]= true;
|
||||
return class_declaration;
|
||||
}
|
||||
|
||||
/** Public API */
|
||||
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){
|
||||
obj[method]= new Proxy(obj[method] || (()=> {}), { apply });
|
||||
}
|
||||
|
||||
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){
|
||||
return oA(instance, (i, n)=> i.getAttribute(n));
|
||||
}
|
||||
|
@ -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= {
|
||||
setDeleteAttr,
|
||||
ssr: "",
|
||||
@ -8,26 +20,43 @@ export const enviroment= {
|
||||
M: globalThis.MutationObserver,
|
||||
q: p=> p || Promise.resolve(),
|
||||
};
|
||||
import { isUndef } from './helpers.js';
|
||||
import { isInstance, isUndef } from './helpers.js';
|
||||
|
||||
/**
|
||||
* Handles attribute setting with special undefined handling
|
||||
*
|
||||
* @param {Object} obj - The object to set the property on
|
||||
* @param {string} prop - The property name
|
||||
* @param {any} val - The value to set
|
||||
* @returns {void}
|
||||
*
|
||||
* 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){
|
||||
/* 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)
|
||||
*/
|
||||
Reflect.set(obj, prop, val);
|
||||
if(!isUndef(val)) return;
|
||||
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);
|
||||
if(Reflect.get(obj, prop)==="undefined")
|
||||
return Reflect.set(obj, prop, "");
|
||||
}
|
||||
|
||||
/** Property key for tracking lifecycle events */
|
||||
export const keyLTE= "__dde_lifecyclesToEvents"; //boolean
|
||||
|
||||
/** Event name for connected lifecycle event */
|
||||
export const evc= "dde:connected";
|
||||
|
||||
/** Event name for disconnected lifecycle event */
|
||||
export const evd= "dde:disconnected";
|
||||
|
||||
/** Event name for attribute changed lifecycle event */
|
||||
export const eva= "dde:attributeChanged";
|
||||
|
215
src/dom.js
215
src/dom.js
@ -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 { 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); }
|
||||
/** @type {{ scope: object, prevent: boolean, host: function }[]} */
|
||||
|
||||
/**
|
||||
* Array of scope contexts for tracking component hierarchies
|
||||
* @type {{ scope: object, prevent: boolean, host: function }[]}
|
||||
*/
|
||||
const scopes= [ {
|
||||
get scope(){ return env.D.body; },
|
||||
host: c=> c ? c(env.D.body) : env.D.body,
|
||||
prevent: true,
|
||||
} ];
|
||||
/**
|
||||
* Scope management utility for tracking component hierarchies
|
||||
*/
|
||||
export const scope= {
|
||||
/**
|
||||
* Gets the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
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; },
|
||||
|
||||
/**
|
||||
* Prevents default behavior in the current scope
|
||||
* @returns {Object} Current scope context
|
||||
*/
|
||||
preventDefault(){
|
||||
const { current }= this;
|
||||
current.prevent= true;
|
||||
return current;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a copy of the current scope stack
|
||||
* @returns {Array} Copy of scope stack
|
||||
*/
|
||||
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]); },
|
||||
|
||||
/**
|
||||
* Pops the current scope from the stack
|
||||
* @returns {Object|undefined} Popped scope or undefined if only one scope remains
|
||||
*/
|
||||
pop(){
|
||||
if(scopes.length===1) return;
|
||||
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; }
|
||||
|
||||
/**
|
||||
* 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){
|
||||
if(el.append===append) return el; el.appendOriginal= el.append; el.append= append; return el;
|
||||
}
|
||||
/** Current namespace for element creation */
|
||||
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){
|
||||
/* jshint maxcomplexity: 15 */
|
||||
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);
|
||||
scope.push({ scope: tag, host });
|
||||
el= tag(attributes || undefined);
|
||||
const is_fragment= el instanceof env.F;
|
||||
const is_fragment= isInstance(el, env.F);
|
||||
if(el.nodeName==="#comment") break;
|
||||
const el_mark= createElement.mark({
|
||||
type: "component",
|
||||
@ -70,10 +136,15 @@ export function createElement(tag, attributes, ...addons){
|
||||
scoped= 2;
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { { type: "component", name: string, host: "this" | "parentElement" } | { type: "reactive" | "later" } } attrs
|
||||
* @param {boolean} [is_open=false]
|
||||
* */
|
||||
* Creates a marker comment for elements
|
||||
*
|
||||
* @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){
|
||||
attrs= Object.entries(attrs).map(([ n, v ])=> n+`="${v}"`).join(" ");
|
||||
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>");
|
||||
return out;
|
||||
};
|
||||
/** Alias for createElement */
|
||||
export { createElement as el };
|
||||
|
||||
//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){
|
||||
const _this= this;
|
||||
return function createElementNSCurried(...rest){
|
||||
@ -92,9 +171,17 @@ export function createElementNS(ns){
|
||||
return el;
|
||||
};
|
||||
}
|
||||
|
||||
/** Alias for createElementNS */
|
||||
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){
|
||||
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(
|
||||
@ -128,17 +215,34 @@ export function simulateSlots(element, root= element){
|
||||
return root;
|
||||
}
|
||||
|
||||
/** Store for element assignment contexts */
|
||||
const assign_context= new WeakMap();
|
||||
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){
|
||||
if(!attributes.length) return element;
|
||||
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);
|
||||
assign_context.delete(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){
|
||||
const { setRemoveAttr, s }= assignContext(element, 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);
|
||||
}
|
||||
/**
|
||||
* 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){
|
||||
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 s= signals(_this);
|
||||
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){
|
||||
const s= signals(this);
|
||||
forEachEntries(s, "classList", element, toggle,
|
||||
@ -184,18 +303,45 @@ export function classListDeclarative(element, toggle){
|
||||
element.classList.toggle(class_name, val===-1 ? undefined : Boolean(val)) );
|
||||
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){
|
||||
if(element instanceof env.H)
|
||||
if(isInstance(element, env.H))
|
||||
return element[op+"Attribute"](key, value);
|
||||
return element[op+"AttributeNS"](null, key, value);
|
||||
}
|
||||
import { isUndef } from "./helpers.js";
|
||||
|
||||
//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){
|
||||
if(!(key in el)) return false;
|
||||
const des= getPropDescriptor(el, key);
|
||||
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){
|
||||
p= Object.getPrototypeOf(p);
|
||||
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){
|
||||
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){
|
||||
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){
|
||||
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);
|
||||
}
|
||||
|
@ -1,11 +1,27 @@
|
||||
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({}, {
|
||||
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(){
|
||||
const store= new Map();
|
||||
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){
|
||||
for(const mutation of mutations){
|
||||
if(mutation.type!=="childList") continue;
|
||||
@ -17,13 +33,26 @@ function connectionsChangesObserverConstructor(){
|
||||
stop();
|
||||
}
|
||||
};
|
||||
|
||||
const observer= new env.M(observerListener(stop));
|
||||
|
||||
return {
|
||||
/**
|
||||
* Creates an observer for a specific element
|
||||
* @param {Element} element - Element to observe
|
||||
* @returns {Function} Cleanup function
|
||||
*/
|
||||
observe(element){
|
||||
const o= new env.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);
|
||||
@ -31,6 +60,12 @@ function connectionsChangesObserverConstructor(){
|
||||
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);
|
||||
@ -39,6 +74,12 @@ function connectionsChangesObserverConstructor(){
|
||||
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);
|
||||
@ -46,6 +87,12 @@ function connectionsChangesObserverConstructor(){
|
||||
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);
|
||||
@ -55,12 +102,24 @@ function connectionsChangesObserverConstructor(){
|
||||
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){
|
||||
if(ls.length_c || ls.length_d)
|
||||
return;
|
||||
store.delete(element);
|
||||
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){
|
||||
if(store.has(element)) return store.get(element);
|
||||
const out= {
|
||||
@ -72,32 +131,58 @@ function connectionsChangesObserverConstructor(){
|
||||
store.set(element, out);
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start observing DOM changes
|
||||
*/
|
||||
function start(){
|
||||
if(is_observing) return;
|
||||
is_observing= true;
|
||||
observer.observe(env.D.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop observing DOM changes when no longer needed
|
||||
*/
|
||||
function stop(){
|
||||
if(!is_observing || store.size) return;
|
||||
is_observing= false;
|
||||
observer.disconnect();
|
||||
}
|
||||
|
||||
//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){
|
||||
(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){
|
||||
if(store.size > 30)//TODO?: limit
|
||||
await requestIdle();
|
||||
const out= [];
|
||||
if(!(element instanceof Node)) return out;
|
||||
if(!isInstance(element, Node)) return out;
|
||||
for(const el of store.keys()){
|
||||
if(el===element || !(el instanceof Node)) continue;
|
||||
if(el===element || !isInstance(el, Node)) continue;
|
||||
if(element.contains(el))
|
||||
out.push(el);
|
||||
}
|
||||
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){
|
||||
let out= false;
|
||||
for(const element of addedNodes){
|
||||
@ -115,6 +200,13 @@ function connectionsChangesObserverConstructor(){
|
||||
}
|
||||
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){
|
||||
let out= false;
|
||||
for(const element of removedNodes){
|
||||
@ -128,6 +220,12 @@ function connectionsChangesObserverConstructor(){
|
||||
}
|
||||
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){
|
||||
return ()=> {
|
||||
if(element.isConnected) return;
|
||||
|
@ -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 { 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){
|
||||
if(!options) options= {};
|
||||
return function dispatch(element, ...d){
|
||||
@ -9,10 +18,19 @@ export function dispatchEvent(name, options, host){
|
||||
element= typeof host==="function"? host() : host;
|
||||
}
|
||||
//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);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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){
|
||||
return function registerElement(element){
|
||||
element.addEventListener(event, listener, options);
|
||||
@ -21,10 +39,23 @@ export function on(event, listener, options){
|
||||
}
|
||||
|
||||
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: 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){
|
||||
options= lifeOptions(options);
|
||||
return function registerElement(element){
|
||||
@ -37,6 +68,14 @@ on.connected= function(listener, options){
|
||||
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){
|
||||
options= lifeOptions(options);
|
||||
return function registerElement(element){
|
||||
@ -48,7 +87,16 @@ on.disconnected= function(listener, options){
|
||||
return element;
|
||||
};
|
||||
};
|
||||
|
||||
/** Store for disconnect abort controllers */
|
||||
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){
|
||||
if(store_abort.has(host)) return store_abort.get(host);
|
||||
|
||||
@ -57,7 +105,17 @@ on.disconnectedAsAbort= function(host){
|
||||
host(on.disconnected(()=> a.abort()));
|
||||
return a;
|
||||
};
|
||||
|
||||
/** Store for elements with attribute observers */
|
||||
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){
|
||||
if(typeof options !== "object")
|
||||
options= {};
|
||||
|
@ -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);
|
||||
|
||||
/**
|
||||
* 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"; }
|
||||
|
||||
/**
|
||||
* 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){
|
||||
const t= typeof v;
|
||||
if(t!=="object") return t;
|
||||
if(v===null) return "null";
|
||||
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){
|
||||
if(!signal || !(signal instanceof AbortSignal))
|
||||
if(!signal || !isInstance(signal, AbortSignal))
|
||||
return true;
|
||||
if(signal.aborted)
|
||||
return;
|
||||
@ -16,6 +46,13 @@ export function onAbort(signal, 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){
|
||||
const { observedAttributes= [] }= instance.constructor;
|
||||
return observedAttributes
|
||||
@ -24,4 +61,28 @@ export function observedAttributes(instance, observedAttribute){
|
||||
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()); }
|
||||
|
||||
/**
|
||||
* 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("/"));
|
||||
}
|
||||
}
|
||||
|
@ -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 wasn’t 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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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 wasn’t 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
44
src/signals-lib/common.js
Normal 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;
|
||||
}
|
39
src/signals-lib/helpers.js
Normal file
39
src/signals-lib/helpers.js
Normal 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);
|
||||
};
|
||||
})();
|
@ -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 const S: s;
|
||||
export const signal: s;
|
487
src/signals-lib/signals-lib.js
Normal file
487
src/signals-lib/signals-lib.js
Normal 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 wasn’t 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;
|
||||
}
|
Reference in New Issue
Block a user