@miaskiewicz/turbo-dom 0.5.7 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -63,11 +63,10 @@ export class CSSStyleSheet {
63
63
  get rules() { return this.cssRules; }
64
64
 
65
65
  insertRule(rule, index = 0) {
66
- const i = index === undefined ? 0 : index;
67
- if (i < 0 || i > this.cssRules.length) throw new RangeError('insertRule: index out of bounds');
68
- this.cssRules.splice(i, 0, new CSSStyleRule(rule));
66
+ if (index < 0 || index > this.cssRules.length) throw new RangeError('insertRule: index out of bounds');
67
+ this.cssRules.splice(index, 0, new CSSStyleRule(rule));
69
68
  this.#invalidate();
70
- return i;
69
+ return index;
71
70
  }
72
71
 
73
72
  deleteRule(index) {
@@ -0,0 +1,398 @@
1
+ // HTML custom elements for the JS runtime (miaskiewicz/turbo-dom#26 item 2): each element's
2
+ // custom element state (what `:defined` reads), the window's CustomElementRegistry, element
3
+ // upgrade / construction, and the lifecycle callbacks. Kept in lockstep with the Rust engine,
4
+ // whose tree records the same state (crates/turbo-dom/src/rtdom/custom_element_state.rs) and
5
+ // whose V8 binding upgrades like Chrome — both checked against the Chrome 156 control
6
+ // crates/turbo-dom-browser-env/src/custom_elements_defined_chrome156.json
7
+ // (test/custom-elements-chrome.test.mjs here).
8
+ //
9
+ // State. An element's state is fixed when it is created and changes only through upgrade
10
+ // or construction. Without either, it is the creation-time state: an HTML element whose
11
+ // local name is a valid custom element name, or that was created with an `is` value (the
12
+ // parser's `is` attribute, `createElement(name, {is})`, a clone of one), is "undefined";
13
+ // every other element is "uncustomized". A later `is` attribute edit never changes it.
14
+ // `el.__ce` holds an explicit state (unset = the creation-time one) and `el.__ceIs` an `is`
15
+ // value the buffer cannot tell (created, cloned and innerHTML-parsed elements); both live
16
+ // only on the rare elements that need them, so plain elements carry nothing.
17
+ //
18
+ // Pay-for-what-you-use: the DOM's hooks (dom.mjs) run only once the document has a
19
+ // registry with a definition (`Document.__ceRegistry`, set when `window.customElements` is
20
+ // first touched); until then every mutation path is unchanged.
21
+ //
22
+ // Reactions are queued per element and run when the outermost DOM call returns, element by
23
+ // element, like Chrome's [CEReactions] (see "reactions" below; miaskiewicz/turbo-dom#29 item
24
+ // 4): upgrades (constructor, then attributeChangedCallback for observed attributes and
25
+ // connectedCallback), disconnectedCallback on removal, adoptedCallback on adoption.
26
+ // Exceptions from constructors and callbacks are reported (`window.reportError`), never
27
+ // thrown into the DOM call, like Chrome; a throwing constructor leaves the element "failed".
28
+
29
+ export const UNDEFINED = 'undefined';
30
+ export const FAILED = 'failed';
31
+ export const UNCUSTOMIZED = 'uncustomized';
32
+ export const PRECUSTOMIZED = 'precustomized';
33
+ export const CUSTOM = 'custom';
34
+ const STATES = new Set([UNDEFINED, FAILED, UNCUSTOMIZED, PRECUSTOMIZED, CUSTOM]);
35
+
36
+ // HTML's "valid custom element name" for the names the parser and createElement produce.
37
+ const RESERVED = new Set(['annotation-xml', 'color-profile', 'font-face', 'font-face-src', 'font-face-uri',
38
+ 'font-face-format', 'font-face-name', 'missing-glyph']);
39
+ export function isCustomName(name) {
40
+ return /^[a-z][^A-Z]*-[^A-Z]*$/.test(name) && !RESERVED.has(name);
41
+ }
42
+
43
+ // The `is` value the parser saw (the immutable buffer's attribute, whatever later edits did).
44
+ function parsedIs(el) {
45
+ const doc = el.ownerDocument, buf = doc && doc.__buf;
46
+ return el.__attrIdx >= 0 && buf ? buf.attrGet(el.__attrIdx, 'is') : null;
47
+ }
48
+
49
+ // Keep the parser's `is` value when the element loses its buffer link (adoption).
50
+ export function freezeParsedIs(el) {
51
+ const is = parsedIs(el);
52
+ if (is === null) return;
53
+ el.__ceIs = is;
54
+ if (el.__ce === undefined) el.__ce = UNDEFINED;
55
+ }
56
+
57
+ // A fragment-parsed (innerHTML) element: its `is` attribute is its `is` value.
58
+ export function parsedIsAttribute(el) {
59
+ const attrs = el.__attrs;
60
+ for (let i = 0; i < attrs.length; i++) {
61
+ if (attrs[i].name === 'is' && attrs[i].prefix === '') { el.__ceIs = attrs[i].value; el.__ce = UNDEFINED; return; }
62
+ }
63
+ }
64
+
65
+ // The element's `is` value (HTML: read once, at creation).
66
+ export function isValue(el) {
67
+ return el.__ceIs !== undefined ? el.__ceIs : parsedIs(el);
68
+ }
69
+
70
+ // The element's custom element state: the recorded one, else the creation-time state.
71
+ export function customElementState(el) {
72
+ if (!el || el.nodeType !== 1 || el.__ns !== '') return UNCUSTOMIZED;
73
+ const state = el.__ce;
74
+ if (state !== undefined) return state;
75
+ const name = el.localName;
76
+ return (name.indexOf('-') !== -1 && isCustomName(name)) || parsedIs(el) !== null ? UNDEFINED : UNCUSTOMIZED;
77
+ }
78
+
79
+ // :defined — uncustomized or custom.
80
+ export function isDefinedElement(el) {
81
+ const state = customElementState(el);
82
+ return state === UNCUSTOMIZED || state === CUSTOM;
83
+ }
84
+
85
+ // Record an element's state (an embedder's upgrade report, or this module's own). Returns
86
+ // whether the state changed; a change bumps the DOM version so cached queries and
87
+ // computed styles see it. Non-elements and non-HTML elements are ignored.
88
+ export function setCustomElementState(el, state) {
89
+ if (!STATES.has(state)) throw new TypeError(`unknown custom element state: ${state}`);
90
+ if (!el || el.nodeType !== 1 || el.__ns !== '') return false;
91
+ const before = customElementState(el);
92
+ el.__ce = state;
93
+ if (before === state) return false;
94
+ const doc = el.ownerDocument;
95
+ if (doc) doc.__touch();
96
+ return true;
97
+ }
98
+
99
+ // The state a copy (clone / import) starts in: "undefined" when the source is a custom
100
+ // element or was created with an `is` value, else uncustomized; the `is` value travels.
101
+ export function copyCustomElementState(source, copy) {
102
+ const is = isValue(source);
103
+ if (is !== null) copy.__ceIs = is;
104
+ if (customElementState(source) !== UNCUSTOMIZED) copy.__ce = UNDEFINED;
105
+ }
106
+
107
+ // ---------------------------------------------------------------- registry ----
108
+
109
+ const definitions = new WeakMap(); // constructor -> definition (its registry's)
110
+ const ALREADY_CONSTRUCTED = {};
111
+
112
+ function report(doc, error) {
113
+ const win = doc && doc.defaultView;
114
+ const reportError = win && win.reportError;
115
+ if (typeof reportError === 'function') reportError(error);
116
+ }
117
+
118
+ // The live registry state of a document, when it has at least one definition.
119
+ function active(doc) {
120
+ const reg = doc && doc.__ceRegistry;
121
+ return reg !== undefined && reg !== null && reg.defs.size !== 0 ? reg : null;
122
+ }
123
+
124
+ function lookup(reg, localName, is) {
125
+ const autonomous = reg.defs.get(localName);
126
+ if (autonomous !== undefined && autonomous.localName === localName) return autonomous;
127
+ if (is === null) return null;
128
+ const customized = reg.defs.get(is);
129
+ return customized !== undefined && customized.localName === localName ? customized : null;
130
+ }
131
+
132
+ function definitionOf(el) {
133
+ if (customElementState(el) !== UNDEFINED) return null;
134
+ const reg = active(el.ownerDocument);
135
+ return reg && lookup(reg, el.localName, isValue(el));
136
+ }
137
+
138
+ // ------------------------------------------------------------- reactions ----
139
+ // HTML [CEReactions]: upgrades and lifecycle callbacks are queued on the element's own
140
+ // reaction queue (`el.__ceReactions`) and the element on the open scope's element queue;
141
+ // the outermost DOM call of the current script level runs them when it returns, element by
142
+ // element (each element's whole queue, then the next element). dom.mjs opens a scope
143
+ // (`withReactions`) around the DOM calls that can queue reactions, only when a document
144
+ // involved has custom element hooks armed (`__ceRegistry !== undefined`). Constructors and
145
+ // callbacks run with no scope open (`reactionDepth === 0`), so a DOM call they make opens
146
+ // its own scope and runs its reactions before returning to them. A reaction queued with no
147
+ // scope open (a single-step DOM call such as setAttribute) runs right away — the same
148
+ // moment as "when the call returns".
149
+
150
+ export let reactionDepth = 0;
151
+ let elementQueue = null;
152
+
153
+ // Run `fn.apply(self, args)` as one [CEReactions] scope.
154
+ export function withReactions(fn, self, args) {
155
+ if (reactionDepth++ === 0) elementQueue = [];
156
+ try {
157
+ return fn.apply(self, args);
158
+ } finally {
159
+ if (--reactionDepth === 0) {
160
+ const queue = elementQueue;
161
+ elementQueue = null;
162
+ for (let i = 0; i < queue.length; i++) drain(queue[i]);
163
+ }
164
+ }
165
+ }
166
+
167
+ // Invoke the element's queued reactions in order (later ones queued meanwhile included).
168
+ function drain(el) {
169
+ const reactions = el.__ceReactions;
170
+ while (reactions.length !== 0) {
171
+ const reaction = reactions.shift();
172
+ if (reaction.fn === undefined) upgrade(el, reaction.def);
173
+ // reported in the definition's window (the element may have moved to another document)
174
+ else try { reaction.fn.apply(el, reaction.args); } catch (error) { report(reaction.def.registry.document, error); }
175
+ }
176
+ }
177
+
178
+ function enqueue(el, reaction) {
179
+ const reactions = el.__ceReactions;
180
+ if (reactions === undefined) el.__ceReactions = [reaction];
181
+ else reactions.push(reaction);
182
+ if (elementQueue !== null) elementQueue.push(el);
183
+ else drain(el);
184
+ }
185
+
186
+ // HTML "enqueue a custom element callback reaction" (a no-op without that callback).
187
+ function enqueueCallback(el, def, name, args) {
188
+ const fn = def[name];
189
+ if (fn !== undefined) enqueue(el, { def, fn, args });
190
+ }
191
+
192
+ // HTML "enqueue a custom element upgrade reaction".
193
+ function enqueueUpgrade(el, def) {
194
+ enqueue(el, { def, fn: undefined, args: undefined });
195
+ }
196
+
197
+ // A callback queued while the element's own reactions are running (upgrade's initial
198
+ // attributeChanged / connected): picked up by the running drain, no element queue entry.
199
+ function enqueueOwn(el, def, name, args) {
200
+ const fn = def[name];
201
+ if (fn !== undefined) el.__ceReactions.push({ def, fn, args });
202
+ }
203
+
204
+ // HTML "upgrade an element": run the definition's constructor on the existing element.
205
+ function upgrade(el, def) {
206
+ if (customElementState(el) !== UNDEFINED) return;
207
+ setCustomElementState(el, PRECUSTOMIZED);
208
+ def.stack.push(el);
209
+ let result, error, threw = false;
210
+ try { result = Reflect.construct(def.ctor, []); } catch (e) { threw = true; error = e; }
211
+ def.stack.pop();
212
+ if (!threw && result !== el) {
213
+ threw = true;
214
+ error = new TypeError('Custom element constructors must call super() first and must not return a different object');
215
+ }
216
+ if (threw) {
217
+ setCustomElementState(el, FAILED);
218
+ report(el.ownerDocument, error);
219
+ return;
220
+ }
221
+ el.__ceDef = def;
222
+ setCustomElementState(el, CUSTOM);
223
+ if (el.__ceReactions === undefined) el.__ceReactions = [];
224
+ if (def.attributeChangedCallback !== undefined) {
225
+ for (const name of def.observed) {
226
+ const value = el.getAttribute(name);
227
+ if (value !== null) enqueueOwn(el, def, 'attributeChangedCallback', [name, null, value, null]);
228
+ }
229
+ }
230
+ if (el.isConnected) enqueueOwn(el, def, 'connectedCallback', []);
231
+ }
232
+
233
+ // HTML "try to upgrade an element": queue an upgrade when its definition exists.
234
+ function tryUpgrade(el) {
235
+ const def = definitionOf(el);
236
+ if (def) enqueueUpgrade(el, def);
237
+ }
238
+
239
+ // Shadow-including inclusive descendants in shadow-including tree order (elements only).
240
+ function eachElement(root, visit) {
241
+ if (root.nodeType === 1) {
242
+ visit(root);
243
+ const shadow = root.__shadow;
244
+ if (shadow) eachElement(shadow, visit);
245
+ }
246
+ const kids = root.__children();
247
+ for (let i = 0; i < kids.length; i++) if (kids[i].nodeType === 1 || kids[i].nodeType === 11) eachElement(kids[i], visit);
248
+ }
249
+
250
+ // The HTML element constructors (HTMLElement and the tag interfaces): `new C()` for a
251
+ // defined C makes a new custom element; during an upgrade it returns the element being
252
+ // upgraded. Returns null for a constructor that is not defined (the caller's fallback).
253
+ export function constructCustomElement(newTarget) {
254
+ const def = definitions.get(newTarget);
255
+ if (def === undefined || def.registry.document.__ceRegistry !== def.registry) return null;
256
+ const proto = newTarget.prototype;
257
+ const stack = def.stack;
258
+ if (stack.length === 0) {
259
+ const el = def.registry.document.__newHtmlElement(def.localName);
260
+ if (def.localName !== def.name) el.__ceIs = def.name;
261
+ Object.setPrototypeOf(el, proto);
262
+ el.__ceDef = def;
263
+ el.__ce = CUSTOM;
264
+ return el;
265
+ }
266
+ const el = stack[stack.length - 1];
267
+ if (el === ALREADY_CONSTRUCTED) throw new TypeError("Failed to construct 'HTMLElement': This instance is already constructed");
268
+ Object.setPrototypeOf(el, proto);
269
+ stack[stack.length - 1] = ALREADY_CONSTRUCTED;
270
+ return el;
271
+ }
272
+
273
+ // HTML "create an element" for an HTML element of `doc` (createElement, cloneNode):
274
+ // `synchronous` runs an autonomous definition's constructor directly (createElement),
275
+ // else the element is created "undefined" and upgraded (clones).
276
+ export function createCustomElement(doc, localName, is, synchronous) {
277
+ const reg = active(doc);
278
+ const def = reg && lookup(reg, localName, is);
279
+ if (def && def.localName === def.name && synchronous) {
280
+ try {
281
+ const el = Reflect.construct(def.ctor, []);
282
+ if (!el || el.nodeType !== 1 || el.localName !== localName || el.ownerDocument !== doc || el.parentNode
283
+ || el.__children().length !== 0 || (el.__attrs !== undefined && el.__attrs.length !== 0)) {
284
+ throw new TypeError(`Failed to execute 'createElement' on 'Document': The result must be a fresh <${localName}> element`);
285
+ }
286
+ return el;
287
+ } catch (error) {
288
+ report(doc, error);
289
+ const el = doc.__newHtmlElement(localName);
290
+ el.__ce = FAILED;
291
+ return el;
292
+ }
293
+ }
294
+ const el = doc.__newHtmlElement(localName);
295
+ if (is !== null) { el.__ceIs = is; el.__ce = UNDEFINED; }
296
+ // a customized built-in upgrades synchronously; a fresh element queues no reactions
297
+ if (def) upgrade(el, def);
298
+ return el;
299
+ }
300
+
301
+ // ------------------------------------------------------------- DOM hooks ----
302
+ // Called by dom.mjs only when `doc.__ceRegistry` is set (one predicted-false read otherwise).
303
+ // They queue reactions; the enclosing withReactions scope runs them.
304
+
305
+ // A clone (or import) of `source`: copy the state, queue an upgrade when a definition exists.
306
+ export function clonedElement(source, copy) {
307
+ copyCustomElementState(source, copy);
308
+ if (active(copy.ownerDocument)) tryUpgrade(copy);
309
+ }
310
+
311
+ // `node` was inserted: connectedCallback for custom elements (in any document — one adopted
312
+ // into a document without a registry stays custom), try to upgrade the others.
313
+ export function insertedNode(node) {
314
+ if (!node.isConnected) return;
315
+ const upgrades = active(node.ownerDocument) !== null;
316
+ eachElement(node, (el) => {
317
+ if (el.__ce === CUSTOM) { if (el.__ceDef) enqueueCallback(el, el.__ceDef, 'connectedCallback', []); }
318
+ else if (upgrades) tryUpgrade(el);
319
+ });
320
+ }
321
+
322
+ // Elements created by an HTML fragment parse (innerHTML, insertAdjacentHTML): upgrades
323
+ // queued at creation when their definition exists, connected or not (Chrome creates them
324
+ // in the context element's document) — before the old children are removed.
325
+ export function parsedFragment(nodes) {
326
+ if (nodes.length === 0 || !active(nodes[0].ownerDocument)) return;
327
+ for (const node of nodes) eachElement(node, tryUpgrade);
328
+ }
329
+
330
+ // `node` was removed from a connected tree: disconnectedCallback for custom elements.
331
+ export function removedNode(node) {
332
+ eachElement(node, (el) => { if (el.__ce === CUSTOM && el.__ceDef) enqueueCallback(el, el.__ceDef, 'disconnectedCallback', []); });
333
+ }
334
+
335
+ // An attribute of `el` changed (`oldValue` null when it was added).
336
+ export function attributeChanged(el, name, oldValue) {
337
+ const def = el.__ce === CUSTOM ? el.__ceDef : undefined;
338
+ if (def === undefined || def.attributeChangedCallback === undefined || !def.observed.includes(name)) return;
339
+ enqueueCallback(el, def, 'attributeChangedCallback', [name, oldValue, el.getAttribute(name), null]);
340
+ }
341
+
342
+ // `node` was adopted from `oldDocument`: adoptedCallback for custom elements.
343
+ export function adoptedNode(node, oldDocument, newDocument) {
344
+ eachElement(node, (el) => {
345
+ if (el.__ce === CUSTOM && el.__ceDef) enqueueCallback(el, el.__ceDef, 'adoptedCallback', [oldDocument, newDocument]);
346
+ });
347
+ }
348
+
349
+ // --------------------------------------------------- CustomElementRegistry ----
350
+
351
+ // The window's registry over `document`: define / get / getName / whenDefined / upgrade.
352
+ export function makeCustomElements(document) {
353
+ const reg = { document, defs: new Map() };
354
+ const waiters = new Map();
355
+ if (document) document.__ceRegistry = reg;
356
+ return {
357
+ define(name, ctor, options) {
358
+ name = String(name);
359
+ if (typeof ctor !== 'function') throw new TypeError("Failed to execute 'define' on 'CustomElementRegistry': The provided value is not a constructor");
360
+ if (reg.defs.has(name)) throw new Error(`'${name}' already defined`);
361
+ const localName = options && options.extends != null ? String(options.extends) : name;
362
+ const proto = ctor.prototype;
363
+ const def = { registry: reg, name, localName, ctor, stack: [], observed: [] };
364
+ for (const key of ['connectedCallback', 'disconnectedCallback', 'adoptedCallback', 'attributeChangedCallback']) {
365
+ const fn = proto[key];
366
+ def[key] = typeof fn === 'function' ? fn : undefined;
367
+ }
368
+ if (def.attributeChangedCallback !== undefined) {
369
+ const observed = ctor.observedAttributes;
370
+ if (observed != null) def.observed = Array.from(observed, String);
371
+ }
372
+ reg.defs.set(name, def);
373
+ definitions.set(ctor, def);
374
+ // upgrade the document's candidates in shadow-including tree order
375
+ if (document && document.__ceRegistry === reg) withReactions(eachElement, null, [document, (el) => {
376
+ if (el.localName === localName && customElementState(el) === UNDEFINED && lookup(reg, localName, isValue(el)) === def) enqueueUpgrade(el, def);
377
+ }]);
378
+ const w = waiters.get(name);
379
+ if (w) { w.forEach((resolve) => resolve(ctor)); waiters.delete(name); }
380
+ },
381
+ get(name) { const def = reg.defs.get(String(name)); return def ? def.ctor : undefined; },
382
+ getName(ctor) { for (const [n, def] of reg.defs) if (def.ctor === ctor) return n; return null; },
383
+ whenDefined(name) {
384
+ name = String(name);
385
+ const def = reg.defs.get(name);
386
+ if (def) return Promise.resolve(def.ctor);
387
+ return new Promise((resolve) => { const a = waiters.get(name) || []; a.push(resolve); waiters.set(name, a); });
388
+ },
389
+ upgrade(root) {
390
+ if (!root || typeof root.__children !== 'function') return;
391
+ withReactions(eachElement, null, [root, (el) => {
392
+ const def = el.ownerDocument && el.ownerDocument.__ceRegistry === reg && customElementState(el) === UNDEFINED
393
+ ? lookup(reg, el.localName, isValue(el)) : null;
394
+ if (def) enqueueUpgrade(el, def);
395
+ }]);
396
+ },
397
+ };
398
+ }