@miaskiewicz/turbo-dom 0.7.0 → 0.8.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.
- package/Cargo.lock +7 -7
- package/Cargo.toml +7 -5
- package/README.md +13 -8
- package/package.json +1 -1
- package/pkg/package.json +2 -2
- package/pkg/turbo_dom_parser_bg.wasm +0 -0
- package/pkg-web/package.json +2 -2
- package/pkg-web/turbo_dom_parser_bg.wasm +0 -0
- package/src/runtime/cascade.mjs +70 -54
- package/src/runtime/css-shorthand.mjs +1217 -734
- package/src/runtime/custom-elements.mjs +8 -2
- package/src/runtime/dom.mjs +148 -95
- package/src/runtime/events.mjs +106 -85
- package/src/runtime/html-serialize.mjs +28 -24
- package/src/runtime/selectors.mjs +353 -237
- package/src/runtime/stubs.mjs +9 -6
- package/turbo-dom-parser.darwin-arm64.node +0 -0
- package/turbo-dom-parser.linux-arm64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-gnu.node +0 -0
- package/turbo-dom-parser.linux-x64-musl.node +0 -0
- package/turbo-dom-parser.win32-x64-msvc.node +0 -0
|
@@ -270,6 +270,13 @@ export function constructCustomElement(newTarget) {
|
|
|
270
270
|
return el;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
// Is the constructor result `el` a fresh <localName> of `doc` (no parent, children or
|
|
274
|
+
// attributes)?
|
|
275
|
+
function isFreshElement(el, doc, localName) {
|
|
276
|
+
return !(!el || el.nodeType !== 1 || el.localName !== localName || el.ownerDocument !== doc || el.parentNode
|
|
277
|
+
|| el.__children().length !== 0 || (el.__attrs !== undefined && el.__attrs.length !== 0));
|
|
278
|
+
}
|
|
279
|
+
|
|
273
280
|
// HTML "create an element" for an HTML element of `doc` (createElement, cloneNode):
|
|
274
281
|
// `synchronous` runs an autonomous definition's constructor directly (createElement),
|
|
275
282
|
// else the element is created "undefined" and upgraded (clones).
|
|
@@ -279,8 +286,7 @@ export function createCustomElement(doc, localName, is, synchronous) {
|
|
|
279
286
|
if (def && def.localName === def.name && synchronous) {
|
|
280
287
|
try {
|
|
281
288
|
const el = Reflect.construct(def.ctor, []);
|
|
282
|
-
if (!el
|
|
283
|
-
|| el.__children().length !== 0 || (el.__attrs !== undefined && el.__attrs.length !== 0)) {
|
|
289
|
+
if (!isFreshElement(el, doc, localName)) {
|
|
284
290
|
throw new TypeError(`Failed to execute 'createElement' on 'Document': The result must be a fresh <${localName}> element`);
|
|
285
291
|
}
|
|
286
292
|
return el;
|
package/src/runtime/dom.mjs
CHANGED
|
@@ -109,6 +109,42 @@ function assertNotAncestor(parent, node) {
|
|
|
109
109
|
if (n === node) throw domError('HierarchyRequestError', 'The new child element contains the parent.');
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
// Does inserting `node` into `parent` run inside a [CEReactions] scope (a custom element
|
|
113
|
+
// registry on either document, and not already in one)?
|
|
114
|
+
function insertNeedsReactions(parent, node) {
|
|
115
|
+
return (parent.ownerDocument.__ceRegistry !== undefined || node.ownerDocument.__ceRegistry !== undefined) && reactionDepth === 0;
|
|
116
|
+
}
|
|
117
|
+
function hasCeRegistry(doc) {
|
|
118
|
+
return doc !== null && doc.__ceRegistry !== undefined;
|
|
119
|
+
}
|
|
120
|
+
// The previous sibling of a node just inserted into `kids` (at `i` before `ref`, or last).
|
|
121
|
+
function insertedPrevious(kids, ref, i) {
|
|
122
|
+
return ref ? kids[i - 1] ?? null : kids[kids.length - 2] ?? null;
|
|
123
|
+
}
|
|
124
|
+
// Range extract / clone helpers. The character data [from, to) of `node` goes to `frag`
|
|
125
|
+
// (removed from `node` when `move`).
|
|
126
|
+
function cutData(frag, node, from, to, move) {
|
|
127
|
+
const copy = node.cloneNode(false);
|
|
128
|
+
copy.data = node.substringData(from, to - from);
|
|
129
|
+
frag.appendChild(copy);
|
|
130
|
+
if (move) node.replaceData(from, to - from, '');
|
|
131
|
+
}
|
|
132
|
+
// The child of `common` that is an inclusive ancestor of `n`.
|
|
133
|
+
function childOfAncestor(n, common) {
|
|
134
|
+
while (n.parentNode !== common) n = n.parentNode;
|
|
135
|
+
return n;
|
|
136
|
+
}
|
|
137
|
+
// A partially contained child `node` of the range's common ancestor: its data within
|
|
138
|
+
// [fromOff, toOff) when it is character data (then from = to = node), else a shallow clone
|
|
139
|
+
// holding the extract of the sub-range (from, fromOff)..(to, toOff).
|
|
140
|
+
function extractPartial(doc, frag, move, node, from, fromOff, to, toOff) {
|
|
141
|
+
if (isCharData(node)) { cutData(frag, node, fromOff, toOff, move); return; }
|
|
142
|
+
const copy = node.cloneNode(false);
|
|
143
|
+
frag.appendChild(copy);
|
|
144
|
+
const sub = new Range(doc);
|
|
145
|
+
sub.startContainer = from; sub.startOffset = fromOff; sub.endContainer = to; sub.endOffset = toOff;
|
|
146
|
+
copy.appendChild(sub.__extract(move));
|
|
147
|
+
}
|
|
112
148
|
// DOM "valid element local name" (createElement / createElementNS, Chrome 156): an ASCII
|
|
113
149
|
// letter followed by anything but ASCII whitespace, NULL, '/' and '>'; or ':', '_' or a
|
|
114
150
|
// non-ASCII code point followed by ASCII alphanumerics, '-', '.', ':', '_' and non-ASCII.
|
|
@@ -116,22 +152,25 @@ function validLocalName(name) {
|
|
|
116
152
|
const n = name.length;
|
|
117
153
|
if (n === 0) return false;
|
|
118
154
|
const c0 = name.charCodeAt(0) | 0x20;
|
|
119
|
-
if (c0 >= 97 && c0 <= 122)
|
|
120
|
-
for (let i = 1; i < n; i++) {
|
|
121
|
-
const c = name.charCodeAt(i);
|
|
122
|
-
if (c <= 32 ? c === 32 || c === 9 || c === 10 || c === 12 || c === 13 || c === 0 : c === 47 || c === 62) return false;
|
|
123
|
-
}
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
155
|
+
if (c0 >= 97 && c0 <= 122) return lettersNameRestValid(name);
|
|
126
156
|
const first = name.charCodeAt(0);
|
|
127
157
|
if (first !== 58 && first !== 95 && first < 0x80) return false;
|
|
128
|
-
for (let i = 1; i < n; i++)
|
|
158
|
+
for (let i = 1; i < n; i++) if (!isLocalNameCodeUnit(name.charCodeAt(i))) return false;
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
// After a leading ASCII letter: anything but ASCII whitespace, NULL, '/' and '>'.
|
|
162
|
+
function lettersNameRestValid(name) {
|
|
163
|
+
for (let i = 1; i < name.length; i++) {
|
|
129
164
|
const c = name.charCodeAt(i);
|
|
130
|
-
if (c
|
|
131
|
-
return false;
|
|
165
|
+
if (c <= 32 ? c === 32 || c === 9 || c === 10 || c === 12 || c === 13 || c === 0 : c === 47 || c === 62) return false;
|
|
132
166
|
}
|
|
133
167
|
return true;
|
|
134
168
|
}
|
|
169
|
+
// After a leading ':', '_' or non-ASCII code point: ASCII alphanumerics, '-', '.', ':',
|
|
170
|
+
// '_' and non-ASCII.
|
|
171
|
+
function isLocalNameCodeUnit(c) {
|
|
172
|
+
return c >= 0x80 || c === 45 || c === 46 || c === 58 || c === 95 || (c >= 48 && c <= 57) || ((c | 0x20) >= 97 && (c | 0x20) <= 122);
|
|
173
|
+
}
|
|
135
174
|
// DOM "valid namespace prefix": non-empty, no ASCII whitespace, NULL, '/' or '>'.
|
|
136
175
|
function validPrefix(prefix) {
|
|
137
176
|
if (prefix.length === 0) return false;
|
|
@@ -141,6 +180,25 @@ function validPrefix(prefix) {
|
|
|
141
180
|
}
|
|
142
181
|
return true;
|
|
143
182
|
}
|
|
183
|
+
// createElementNS "validate and extract": { prefix (null without a colon), local }; throws
|
|
184
|
+
// InvalidCharacterError on an invalid prefix or local name.
|
|
185
|
+
function splitQualifiedName(qualified) {
|
|
186
|
+
let prefix = null, local = qualified;
|
|
187
|
+
const colon = qualified.indexOf(':');
|
|
188
|
+
if (colon !== -1) {
|
|
189
|
+
prefix = qualified.slice(0, colon);
|
|
190
|
+
const end = qualified.indexOf(':', colon + 1);
|
|
191
|
+
local = end === -1 ? qualified.slice(colon + 1) : qualified.slice(colon + 1, end);
|
|
192
|
+
if (!validPrefix(prefix)) throw invalidName(qualified);
|
|
193
|
+
}
|
|
194
|
+
if (!validLocalName(local)) throw invalidName(qualified);
|
|
195
|
+
return { prefix, local };
|
|
196
|
+
}
|
|
197
|
+
// createElementNS's NamespaceError conditions.
|
|
198
|
+
function namespaceMismatch(ns, qualified, prefix) {
|
|
199
|
+
return (prefix !== null && ns === null) || (prefix === 'xml' && ns !== XML_NS)
|
|
200
|
+
|| ((qualified === 'xmlns' || prefix === 'xmlns') !== (ns === XMLNS_NS));
|
|
201
|
+
}
|
|
144
202
|
const invalidName = (name) => domError('InvalidCharacterError', `The qualified name provided ('${name}') contains the invalid name-start character or invalid character.`);
|
|
145
203
|
|
|
146
204
|
// ------------------------------------------------------------------ Node ----
|
|
@@ -212,7 +270,7 @@ export class Node extends EventTarget {
|
|
|
212
270
|
appendChild(node) { return this.insertBefore(node, null); }
|
|
213
271
|
|
|
214
272
|
insertBefore(node, ref) {
|
|
215
|
-
if ((this
|
|
273
|
+
if (insertNeedsReactions(this, node)) return withReactions(METHOD.insertBefore, this, [node, ref]);
|
|
216
274
|
// DOM "pre-insert": check the reference child before touching the tree, and a node
|
|
217
275
|
// inserted before itself goes before its next sibling (miaskiewicz/turbo-dom#34)
|
|
218
276
|
assertNotAncestor(this, node);
|
|
@@ -227,12 +285,11 @@ export class Node extends EventTarget {
|
|
|
227
285
|
if (liveRanges.length !== 0) liveInsert(this, ref ? i : kids.length, 1);
|
|
228
286
|
if (ref) kids.splice(i, 0, node); else kids.push(node);
|
|
229
287
|
node.parentNode = this;
|
|
230
|
-
if (moQuiet === 0) notifyMutation(this, 'childList', node, null, ref || null, null, null, ref
|
|
288
|
+
if (moQuiet === 0) notifyMutation(this, 'childList', node, null, ref || null, null, null, insertedPrevious(kids, ref, i));
|
|
231
289
|
else this.__touch();
|
|
232
290
|
// custom elements (custom-elements.mjs): upgrades + connectedCallback, only once a
|
|
233
291
|
// registry exists — one predicted-false read otherwise
|
|
234
|
-
|
|
235
|
-
if (doc !== null && doc.__ceRegistry !== undefined) insertedNode(node);
|
|
292
|
+
if (hasCeRegistry(this.ownerDocument)) insertedNode(node);
|
|
236
293
|
return node;
|
|
237
294
|
}
|
|
238
295
|
|
|
@@ -1351,8 +1408,33 @@ const observed = (node) => { const d = node.ownerDocument; return d !== null &&
|
|
|
1351
1408
|
// DOM "insert a fragment" / "replace all" for the multi-node APIs: the nodes leave their old
|
|
1352
1409
|
// parents (recorded there), \`replaced\` children of \`parent\` go, and the nodes go in before
|
|
1353
1410
|
// \`ref\` (after it when \`afterRef\`; null = at the start) — ONE childList record, as in Chrome.
|
|
1354
|
-
|
|
1355
|
-
|
|
1411
|
+
// Document.__inflateNested helpers: an element (attributes, `is`, template contents) and a
|
|
1412
|
+
// container's children (the template `content` fragment is not a child).
|
|
1413
|
+
const isContentFragment = (c) => c.nodeType === DOCUMENT_FRAGMENT_NODE && c.name === 'content';
|
|
1414
|
+
function inflateNestedElement(doc, raw) {
|
|
1415
|
+
const node = newElement(doc, raw.name, raw.namespace || '');
|
|
1416
|
+
node.__attrs = raw.attrs.map((a) => ({ name: a.name, value: a.value, prefix: a.prefix || '' }));
|
|
1417
|
+
// the fragment parser reads `is` at creation, like the initial parse (custom-elements.mjs)
|
|
1418
|
+
if (node.__ns === '' && raw.attrs.length !== 0) parsedIsAttribute(node);
|
|
1419
|
+
if (raw.name === 'template') {
|
|
1420
|
+
const contentRaw = raw.children.find(isContentFragment);
|
|
1421
|
+
if (contentRaw) node.__content = doc.__templateDocument().__inflateNested(contentRaw);
|
|
1422
|
+
}
|
|
1423
|
+
return node;
|
|
1424
|
+
}
|
|
1425
|
+
function inflateNestedKids(doc, raw, node) {
|
|
1426
|
+
const kids = [];
|
|
1427
|
+
for (const rc of raw.children) {
|
|
1428
|
+
if (isContentFragment(rc)) continue;
|
|
1429
|
+
const child = doc.__inflateNested(rc);
|
|
1430
|
+
child.parentNode = node;
|
|
1431
|
+
kids.push(child);
|
|
1432
|
+
}
|
|
1433
|
+
return kids;
|
|
1434
|
+
}
|
|
1435
|
+
// The nodes to insert, in order: a fragment's children (emptied, ONE record on the
|
|
1436
|
+
// fragment) or the node itself (removed from its old parent).
|
|
1437
|
+
function detachForInsert(nodes) {
|
|
1356
1438
|
const flat = [];
|
|
1357
1439
|
for (const n of nodes) {
|
|
1358
1440
|
if (n.nodeType === DOCUMENT_FRAGMENT_NODE) {
|
|
@@ -1366,6 +1448,11 @@ function insertBatch(parent, nodes, ref, replaced, afterRef = false) {
|
|
|
1366
1448
|
flat.push(n);
|
|
1367
1449
|
}
|
|
1368
1450
|
}
|
|
1451
|
+
return flat;
|
|
1452
|
+
}
|
|
1453
|
+
function insertBatch(parent, nodes, ref, replaced, afterRef = false) {
|
|
1454
|
+
for (const n of nodes) assertNotAncestor(parent, n);
|
|
1455
|
+
const flat = detachForInsert(nodes);
|
|
1369
1456
|
const before = afterRef ? (ref === null ? parent.firstChild : ref.nextSibling) : ref;
|
|
1370
1457
|
const gone = replaced.filter((c) => c.parentNode === parent);
|
|
1371
1458
|
let previous;
|
|
@@ -1644,36 +1731,16 @@ class Range {
|
|
|
1644
1731
|
const frag = this.__doc.createDocumentFragment();
|
|
1645
1732
|
if (this.collapsed) return frag;
|
|
1646
1733
|
const sc = this.startContainer, so = this.startOffset, ec = this.endContainer, eo = this.endOffset;
|
|
1647
|
-
|
|
1648
|
-
const copy = node.cloneNode(false);
|
|
1649
|
-
copy.data = node.substringData(from, to - from);
|
|
1650
|
-
frag.appendChild(copy);
|
|
1651
|
-
if (move) node.replaceData(from, to - from, '');
|
|
1652
|
-
};
|
|
1653
|
-
if (sc === ec && isCharData(sc)) { cutData(sc, so, eo); if (move) this.collapse(true); return frag; }
|
|
1734
|
+
if (sc === ec && isCharData(sc)) { cutData(frag, sc, so, eo, move); if (move) this.collapse(true); return frag; }
|
|
1654
1735
|
const common = this.commonAncestorContainer;
|
|
1655
|
-
const
|
|
1656
|
-
const
|
|
1657
|
-
const lastPartial = isInclusiveAncestor(ec, sc) ? null : childOf(ec);
|
|
1736
|
+
const firstPartial = isInclusiveAncestor(sc, ec) ? null : childOfAncestor(sc, common);
|
|
1737
|
+
const lastPartial = isInclusiveAncestor(ec, sc) ? null : childOfAncestor(ec, common);
|
|
1658
1738
|
const contained = common.__children().filter((c) => this.__contains(c));
|
|
1659
1739
|
if (contained.some((c) => c.nodeType === DOCUMENT_TYPE_NODE)) throw domError('HierarchyRequestError', 'A doctype cannot be extracted.');
|
|
1660
1740
|
const [newNode, newOffset] = move ? this.__collapsePoint() : [null, 0];
|
|
1661
|
-
|
|
1662
|
-
const copy = node.cloneNode(false);
|
|
1663
|
-
frag.appendChild(copy);
|
|
1664
|
-
const sub = new Range(this.__doc);
|
|
1665
|
-
sub.startContainer = from; sub.startOffset = fromOff; sub.endContainer = to; sub.endOffset = toOff;
|
|
1666
|
-
copy.appendChild(sub.__extract(move));
|
|
1667
|
-
};
|
|
1668
|
-
if (firstPartial) {
|
|
1669
|
-
if (isCharData(firstPartial)) cutData(firstPartial, so, nodeLength(firstPartial));
|
|
1670
|
-
else partial(firstPartial, sc, so, firstPartial, nodeLength(firstPartial));
|
|
1671
|
-
}
|
|
1741
|
+
if (firstPartial) extractPartial(this.__doc, frag, move, firstPartial, sc, so, firstPartial, nodeLength(firstPartial));
|
|
1672
1742
|
for (const c of contained) frag.appendChild(move ? c : c.cloneNode(true));
|
|
1673
|
-
if (lastPartial)
|
|
1674
|
-
if (isCharData(lastPartial)) cutData(lastPartial, 0, eo);
|
|
1675
|
-
else partial(lastPartial, lastPartial, 0, ec, eo);
|
|
1676
|
-
}
|
|
1743
|
+
if (lastPartial) extractPartial(this.__doc, frag, move, lastPartial, lastPartial, 0, ec, eo);
|
|
1677
1744
|
if (move) { this.startContainer = this.endContainer = newNode; this.startOffset = this.endOffset = newOffset; }
|
|
1678
1745
|
return frag;
|
|
1679
1746
|
}
|
|
@@ -2031,19 +2098,22 @@ function declarationEntries(prop, text) {
|
|
|
2031
2098
|
// WHATWG value-sanitization per input type. Returns the accepted value, or null
|
|
2032
2099
|
// if invalid (caller leaves the current value unchanged — like a real browser,
|
|
2033
2100
|
// which rejects partial/garbage input rather than storing it).
|
|
2101
|
+
// The value syntax of the types whose invalid values sanitize to null.
|
|
2102
|
+
const INPUT_VALUE_SYNTAX = new Map([
|
|
2103
|
+
['date', /^\d{4}-\d{2}-\d{2}$/],
|
|
2104
|
+
['month', /^\d{4}-\d{2}$/],
|
|
2105
|
+
['week', /^\d{4}-W\d{2}$/],
|
|
2106
|
+
['time', /^\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?$/],
|
|
2107
|
+
['datetime-local', /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/],
|
|
2108
|
+
['number', /^-?\d*\.?\d+(e[-+]?\d+)?$/i],
|
|
2109
|
+
]);
|
|
2034
2110
|
function sanitizeInputValue(type, v) {
|
|
2035
2111
|
if (v === '') return '';
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
case 'datetime-local': return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/.test(v) ? v : null;
|
|
2042
|
-
case 'number': return /^-?\d*\.?\d+(e[-+]?\d+)?$/i.test(v) ? v : null;
|
|
2043
|
-
case 'range': return /^-?\d*\.?\d+$/.test(v) ? v : '50';
|
|
2044
|
-
case 'color': return /^#[0-9a-fA-F]{6}$/.test(v) ? v.toLowerCase() : null;
|
|
2045
|
-
default: return v; // text, email, password, search, url, tel, hidden, etc.
|
|
2046
|
-
}
|
|
2112
|
+
if (type === 'range') return /^-?\d*\.?\d+$/.test(v) ? v : '50';
|
|
2113
|
+
if (type === 'color') return /^#[0-9a-fA-F]{6}$/.test(v) ? v.toLowerCase() : null;
|
|
2114
|
+
const syntax = INPUT_VALUE_SYNTAX.get(type);
|
|
2115
|
+
if (syntax === undefined) return v; // text, email, password, search, url, tel, hidden, etc.
|
|
2116
|
+
return syntax.test(v) ? v : null;
|
|
2047
2117
|
}
|
|
2048
2118
|
|
|
2049
2119
|
// element.dataset — camelCase <-> data-* attribute mapping.
|
|
@@ -2134,26 +2204,36 @@ function notifyMutation(target, type, added, removed, nextSibling, attributeName
|
|
|
2134
2204
|
// attributeChangedCallback: queued after the mutation record, like Chrome
|
|
2135
2205
|
if (type === 'attributes' && doc.__ceRegistry !== undefined) attributeChanged(target, attributeName, oldValue);
|
|
2136
2206
|
}
|
|
2207
|
+
// One node, or the node list of a batched change (insertBatch, innerHTML, textContent).
|
|
2208
|
+
function recordNodes(nodes) {
|
|
2209
|
+
return Array.isArray(nodes) ? nodes : nodes ? [nodes] : EMPTY_NODES;
|
|
2210
|
+
}
|
|
2211
|
+
// Does a registration observing `obsTarget` with `options` see a `type` change of `target`?
|
|
2212
|
+
function observes(options, obsTarget, target, type, attributeName) {
|
|
2213
|
+
const onTarget = target === obsTarget;
|
|
2214
|
+
const inSubtree = options.subtree && isDescendant(target, obsTarget);
|
|
2215
|
+
if (!onTarget && !inSubtree) return false;
|
|
2216
|
+
if (type === 'childList') return options.childList;
|
|
2217
|
+
if (type === 'attributes') return options.attributes && (!options.attributeFilter || options.attributeFilter.includes(attributeName));
|
|
2218
|
+
return type !== 'characterData' || options.characterData;
|
|
2219
|
+
}
|
|
2220
|
+
function recordOldValue(options, type, oldValue) {
|
|
2221
|
+
const wanted = (type === 'attributes' && options.attributeOldValue) ||
|
|
2222
|
+
(type === 'characterData' && options.characterDataOldValue);
|
|
2223
|
+
return wanted ? (oldValue ?? null) : null;
|
|
2224
|
+
}
|
|
2137
2225
|
function queueRecords(mo, doc, target, type, added, removed, nextSibling, attributeName, oldValue, previousSibling) {
|
|
2138
|
-
|
|
2139
|
-
const
|
|
2140
|
-
const removedNodes = Array.isArray(removed) ? removed : removed ? [removed] : EMPTY_NODES;
|
|
2226
|
+
const addedNodes = recordNodes(added);
|
|
2227
|
+
const removedNodes = recordNodes(removed);
|
|
2141
2228
|
for (const reg of mo) {
|
|
2142
2229
|
const { obs, target: obsTarget, options } = reg;
|
|
2143
|
-
|
|
2144
|
-
const inSubtree = options.subtree && isDescendant(target, obsTarget);
|
|
2145
|
-
if (!onTarget && !inSubtree) continue;
|
|
2146
|
-
if (type === 'childList' && !options.childList) continue;
|
|
2147
|
-
if (type === 'attributes' && !options.attributes) continue;
|
|
2148
|
-
if (type === 'attributes' && options.attributeFilter && !options.attributeFilter.includes(attributeName)) continue;
|
|
2149
|
-
if (type === 'characterData' && !options.characterData) continue;
|
|
2230
|
+
if (!observes(options, obsTarget, target, type, attributeName)) continue;
|
|
2150
2231
|
const rec = {
|
|
2151
2232
|
type, target,
|
|
2152
2233
|
addedNodes, removedNodes,
|
|
2153
2234
|
previousSibling: previousSibling || null, nextSibling: nextSibling || null,
|
|
2154
2235
|
attributeName: attributeName || null, attributeNamespace: null,
|
|
2155
|
-
oldValue: (type
|
|
2156
|
-
(type === 'characterData' && options.characterDataOldValue) ? (oldValue ?? null) : null,
|
|
2236
|
+
oldValue: recordOldValue(options, type, oldValue),
|
|
2157
2237
|
};
|
|
2158
2238
|
obs.__enqueue(rec);
|
|
2159
2239
|
doc.__scheduleMO(obs);
|
|
@@ -2281,32 +2361,14 @@ export class Document extends Node {
|
|
|
2281
2361
|
__inflateNested(raw) {
|
|
2282
2362
|
let node;
|
|
2283
2363
|
switch (raw.nodeType) {
|
|
2284
|
-
case ELEMENT_NODE:
|
|
2285
|
-
node = newElement(this, raw.name, raw.namespace || '');
|
|
2286
|
-
node.__attrs = raw.attrs.map((a) => ({ name: a.name, value: a.value, prefix: a.prefix || '' }));
|
|
2287
|
-
// the fragment parser reads `is` at creation, like the initial parse (custom-elements.mjs)
|
|
2288
|
-
if (node.__ns === '' && raw.attrs.length !== 0) parsedIsAttribute(node);
|
|
2289
|
-
if (raw.name === 'template') {
|
|
2290
|
-
const contentRaw = raw.children.find((c) => c.nodeType === DOCUMENT_FRAGMENT_NODE && c.name === 'content');
|
|
2291
|
-
if (contentRaw) node.__content = this.__templateDocument().__inflateNested(contentRaw);
|
|
2292
|
-
}
|
|
2293
|
-
break;
|
|
2364
|
+
case ELEMENT_NODE: node = inflateNestedElement(this, raw); break;
|
|
2294
2365
|
case TEXT_NODE: node = new Text(this, raw.value); break;
|
|
2295
2366
|
case COMMENT_NODE: node = new Comment(this, raw.value); break;
|
|
2296
2367
|
case DOCUMENT_TYPE_NODE: node = new DocumentType(this, raw.name, raw.publicId, raw.systemId); break;
|
|
2297
2368
|
case DOCUMENT_FRAGMENT_NODE: node = new DocumentFragment(this); break;
|
|
2298
2369
|
default: node = new Comment(this, ''); break;
|
|
2299
2370
|
}
|
|
2300
|
-
if (raw.nodeType !== TEXT_NODE && raw.nodeType !== COMMENT_NODE && raw.nodeType !== DOCUMENT_TYPE_NODE)
|
|
2301
|
-
const kids = [];
|
|
2302
|
-
for (const rc of raw.children) {
|
|
2303
|
-
if (rc.nodeType === DOCUMENT_FRAGMENT_NODE && rc.name === 'content') continue;
|
|
2304
|
-
const child = this.__inflateNested(rc);
|
|
2305
|
-
child.parentNode = node;
|
|
2306
|
-
kids.push(child);
|
|
2307
|
-
}
|
|
2308
|
-
node.__kids = kids;
|
|
2309
|
-
}
|
|
2371
|
+
if (raw.nodeType !== TEXT_NODE && raw.nodeType !== COMMENT_NODE && raw.nodeType !== DOCUMENT_TYPE_NODE) node.__kids = inflateNestedKids(this, raw, node);
|
|
2310
2372
|
return node;
|
|
2311
2373
|
}
|
|
2312
2374
|
|
|
@@ -2358,17 +2420,8 @@ export class Document extends Node {
|
|
|
2358
2420
|
createElementNS(namespace, qualifiedName, options) {
|
|
2359
2421
|
const ns = namespace === null || namespace === undefined || namespace === '' ? null : String(namespace);
|
|
2360
2422
|
const qualified = String(qualifiedName);
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
if (colon !== -1) {
|
|
2364
|
-
prefix = qualified.slice(0, colon);
|
|
2365
|
-
const end = qualified.indexOf(':', colon + 1);
|
|
2366
|
-
local = end === -1 ? qualified.slice(colon + 1) : qualified.slice(colon + 1, end);
|
|
2367
|
-
if (!validPrefix(prefix)) throw invalidName(qualified);
|
|
2368
|
-
}
|
|
2369
|
-
if (!validLocalName(local)) throw invalidName(qualified);
|
|
2370
|
-
if ((prefix !== null && ns === null) || (prefix === 'xml' && ns !== XML_NS)
|
|
2371
|
-
|| ((qualified === 'xmlns' || prefix === 'xmlns') !== (ns === XMLNS_NS))) {
|
|
2423
|
+
const { prefix, local } = splitQualifiedName(qualified);
|
|
2424
|
+
if (namespaceMismatch(ns, qualified, prefix)) {
|
|
2372
2425
|
throw domError('NamespaceError', `The namespace and qualified name ('${qualified}') do not match.`);
|
|
2373
2426
|
}
|
|
2374
2427
|
const el = ns === HTML_NS && (this.__ceRegistry !== undefined || options !== undefined)
|
package/src/runtime/events.mjs
CHANGED
|
@@ -186,36 +186,13 @@ export class EventTarget {
|
|
|
186
186
|
// Single ancestor walk: build the path AND note whether any node on it has a
|
|
187
187
|
// listener for this type. React fires thousands of events with zero matching
|
|
188
188
|
// listeners on the path — those skip the capture/target/bubble invoke loops.
|
|
189
|
-
const type = event.type;
|
|
190
189
|
const path = [];
|
|
191
|
-
let hasListener = false;
|
|
192
190
|
// Pay-for-what-you-use: the shadow-aware walk + per-invoke retargeting only
|
|
193
191
|
// runs when a shadow root exists in this document. Otherwise the original
|
|
194
|
-
// flat walk runs
|
|
192
|
+
// flat walk runs (one boolean read, predicted false).
|
|
195
193
|
const doc = this.ownerDocument || this;
|
|
196
194
|
const useShadow = !!(doc && doc.__hasShadow);
|
|
197
|
-
|
|
198
|
-
let node = this;
|
|
199
|
-
while (node) {
|
|
200
|
-
path.push(node);
|
|
201
|
-
if (!hasListener) { const l = node.__listeners && node.__listeners.get(type); if (l && l.length) hasListener = true; }
|
|
202
|
-
node = node.parentNode || node.__owner || null;
|
|
203
|
-
}
|
|
204
|
-
} else {
|
|
205
|
-
const targetRoot = treeRootOf(target);
|
|
206
|
-
let node = this;
|
|
207
|
-
while (node) {
|
|
208
|
-
path.push(node);
|
|
209
|
-
if (!hasListener) { const l = node.__listeners && node.__listeners.get(type); if (l && l.length) hasListener = true; }
|
|
210
|
-
if (node.__isShadowRoot) {
|
|
211
|
-
// a non-composed event stops at the shadow boundary enclosing its
|
|
212
|
-
// target; a composed event continues up through the host.
|
|
213
|
-
node = (!event.composed && node === targetRoot) ? null : (node.host || null);
|
|
214
|
-
} else {
|
|
215
|
-
node = node.parentNode || node.__owner || null;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
195
|
+
const hasListener = useShadow ? shadowPath(this, event, path) : flatPath(this, event.type, path);
|
|
219
196
|
event._path = path;
|
|
220
197
|
|
|
221
198
|
// pre-click activation (WHATWG): checkbox/radio toggle BEFORE click listeners
|
|
@@ -228,58 +205,8 @@ export class EventTarget {
|
|
|
228
205
|
// relatedTarget (focus/mouseenter/mouseleave) is retargeted per listener
|
|
229
206
|
// exactly like target. Only relevant under shadow with a non-null value.
|
|
230
207
|
const relatedTarget = useShadow ? event.relatedTarget : null;
|
|
231
|
-
const invoke = (node, phase) => {
|
|
232
|
-
const list = node.__listeners && node.__listeners.get(event.type);
|
|
233
|
-
if (!list || list.length === 0) return;
|
|
234
|
-
event.currentTarget = node;
|
|
235
|
-
event.eventPhase = phase;
|
|
236
|
-
// present `target`/`relatedTarget` retargeted to the listener's tree
|
|
237
|
-
if (useShadow) {
|
|
238
|
-
event.target = retarget(target, node);
|
|
239
|
-
if (relatedTarget != null) event.relatedTarget = retarget(relatedTarget, node);
|
|
240
|
-
}
|
|
241
|
-
// snapshot — listeners added during dispatch don't fire this round. Skip the
|
|
242
|
-
// slice alloc for the single-listener case (common React-delegated one): a
|
|
243
|
-
// 1-element, length-captured iteration can't be disturbed by an add/remove in
|
|
244
|
-
// that one call. Multi-listener keeps the slice (concurrent-mutation guard).
|
|
245
|
-
const snap = list.length === 1 ? list : list.slice();
|
|
246
|
-
for (let k = 0, len = snap.length; k < len; k++) {
|
|
247
|
-
const l = snap[k];
|
|
248
|
-
if (phase === PHASE_CAPTURING && !l.capture) continue;
|
|
249
|
-
if (phase === PHASE_BUBBLING && l.capture) continue;
|
|
250
|
-
if (l.once) {
|
|
251
|
-
const cur = node.__listeners.get(event.type);
|
|
252
|
-
const idx = cur ? cur.indexOf(l) : -1;
|
|
253
|
-
if (idx !== -1) cur.splice(idx, 1);
|
|
254
|
-
}
|
|
255
|
-
event._passiveListener = l.passive;
|
|
256
|
-
const handler = typeof l.callback === 'function' ? l.callback : l.callback.handleEvent;
|
|
257
|
-
try {
|
|
258
|
-
handler.call(node, event);
|
|
259
|
-
} finally {
|
|
260
|
-
event._passiveListener = false;
|
|
261
|
-
}
|
|
262
|
-
if (event._stopImmediate) return;
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
|
|
266
208
|
// no listener anywhere on the path → skip all three propagation phases
|
|
267
|
-
if (hasListener)
|
|
268
|
-
// capturing: root -> just before target
|
|
269
|
-
for (let i = path.length - 1; i >= 1; i--) {
|
|
270
|
-
if (event._stopPropagation) break;
|
|
271
|
-
invoke(path[i], PHASE_CAPTURING);
|
|
272
|
-
}
|
|
273
|
-
// at target
|
|
274
|
-
if (!event._stopPropagation) invoke(path[0], PHASE_AT_TARGET);
|
|
275
|
-
// bubbling: target's parent -> root
|
|
276
|
-
if (event.bubbles) {
|
|
277
|
-
for (let i = 1; i < path.length; i++) {
|
|
278
|
-
if (event._stopPropagation) break;
|
|
279
|
-
invoke(path[i], PHASE_BUBBLING);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
209
|
+
if (hasListener) propagate(event, path, useShadow ? target : null, relatedTarget);
|
|
283
210
|
|
|
284
211
|
event.eventPhase = PHASE_NONE;
|
|
285
212
|
event.currentTarget = null;
|
|
@@ -287,15 +214,7 @@ export class EventTarget {
|
|
|
287
214
|
event.target = target;
|
|
288
215
|
if (relatedTarget != null) event.relatedTarget = relatedTarget;
|
|
289
216
|
}
|
|
290
|
-
|
|
291
|
-
// canceled activation: undo the pre-click toggle if default was prevented.
|
|
292
|
-
// Otherwise a checkbox/radio toggle fires input then change (activation
|
|
293
|
-
// default action) — user-event/React rely on these to see the new value.
|
|
294
|
-
if (activation && event.defaultPrevented) activation.undo();
|
|
295
|
-
else if (activation && activation.fireChange) {
|
|
296
|
-
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
297
|
-
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
298
|
-
}
|
|
217
|
+
if (activation) finishActivation(this, event, activation);
|
|
299
218
|
|
|
300
219
|
// remaining default actions (label→control, form submit) unless prevented
|
|
301
220
|
if (!event.defaultPrevented && typeof this.__runDefaultAction === 'function') {
|
|
@@ -305,6 +224,108 @@ export class EventTarget {
|
|
|
305
224
|
}
|
|
306
225
|
}
|
|
307
226
|
|
|
227
|
+
// dispatchEvent helpers (plain functions: no per-dispatch closure).
|
|
228
|
+
function hasListenerFor(node, type) {
|
|
229
|
+
const l = node.__listeners && node.__listeners.get(type);
|
|
230
|
+
return !!(l && l.length);
|
|
231
|
+
}
|
|
232
|
+
// The flat (shadow-free) event path from `start` into `path`; whether a node on it has a
|
|
233
|
+
// `type` listener.
|
|
234
|
+
function flatPath(start, type, path) {
|
|
235
|
+
let hasListener = false;
|
|
236
|
+
let node = start;
|
|
237
|
+
while (node) {
|
|
238
|
+
path.push(node);
|
|
239
|
+
if (!hasListener) hasListener = hasListenerFor(node, type);
|
|
240
|
+
node = node.parentNode || node.__owner || null;
|
|
241
|
+
}
|
|
242
|
+
return hasListener;
|
|
243
|
+
}
|
|
244
|
+
// The shadow-aware event path: a non-composed event stops at the shadow boundary enclosing
|
|
245
|
+
// its target; a composed event continues up through the host.
|
|
246
|
+
function shadowPath(start, event, path) {
|
|
247
|
+
const type = event.type;
|
|
248
|
+
const targetRoot = treeRootOf(start);
|
|
249
|
+
let hasListener = false;
|
|
250
|
+
let node = start;
|
|
251
|
+
while (node) {
|
|
252
|
+
path.push(node);
|
|
253
|
+
if (!hasListener) hasListener = hasListenerFor(node, type);
|
|
254
|
+
if (node.__isShadowRoot) node = (!event.composed && node === targetRoot) ? null : (node.host || null);
|
|
255
|
+
else node = node.parentNode || node.__owner || null;
|
|
256
|
+
}
|
|
257
|
+
return hasListener;
|
|
258
|
+
}
|
|
259
|
+
// The capture, target and bubble phases over `path`. `shadowTarget`: the original target
|
|
260
|
+
// when listeners see a retargeted `target` / `relatedTarget` (shadow), else null.
|
|
261
|
+
function propagate(event, path, shadowTarget, relatedTarget) {
|
|
262
|
+
// capturing: root -> just before target
|
|
263
|
+
for (let i = path.length - 1; i >= 1; i--) {
|
|
264
|
+
if (event._stopPropagation) break;
|
|
265
|
+
invoke(event, path[i], PHASE_CAPTURING, shadowTarget, relatedTarget);
|
|
266
|
+
}
|
|
267
|
+
// at target
|
|
268
|
+
if (!event._stopPropagation) invoke(event, path[0], PHASE_AT_TARGET, shadowTarget, relatedTarget);
|
|
269
|
+
// bubbling: target's parent -> root
|
|
270
|
+
if (!event.bubbles) return;
|
|
271
|
+
for (let i = 1; i < path.length; i++) {
|
|
272
|
+
if (event._stopPropagation) break;
|
|
273
|
+
invoke(event, path[i], PHASE_BUBBLING, shadowTarget, relatedTarget);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Does listener `l` skip `phase` (capture listeners skip bubbling and vice versa)?
|
|
277
|
+
function skipsPhase(l, phase) {
|
|
278
|
+
return phase === PHASE_CAPTURING ? !l.capture : phase === PHASE_BUBBLING && l.capture;
|
|
279
|
+
}
|
|
280
|
+
function removeListener(node, type, l) {
|
|
281
|
+
const cur = node.__listeners.get(type);
|
|
282
|
+
const idx = cur ? cur.indexOf(l) : -1;
|
|
283
|
+
if (idx !== -1) cur.splice(idx, 1);
|
|
284
|
+
}
|
|
285
|
+
function callListener(node, event, l) {
|
|
286
|
+
event._passiveListener = l.passive;
|
|
287
|
+
const handler = typeof l.callback === 'function' ? l.callback : l.callback.handleEvent;
|
|
288
|
+
try {
|
|
289
|
+
handler.call(node, event);
|
|
290
|
+
} finally {
|
|
291
|
+
event._passiveListener = false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Run `node`'s listeners for `event` in `phase`.
|
|
295
|
+
function invoke(event, node, phase, shadowTarget, relatedTarget) {
|
|
296
|
+
const list = node.__listeners && node.__listeners.get(event.type);
|
|
297
|
+
if (!list || list.length === 0) return;
|
|
298
|
+
event.currentTarget = node;
|
|
299
|
+
event.eventPhase = phase;
|
|
300
|
+
// present `target`/`relatedTarget` retargeted to the listener's tree
|
|
301
|
+
if (shadowTarget !== null) {
|
|
302
|
+
event.target = retarget(shadowTarget, node);
|
|
303
|
+
if (relatedTarget != null) event.relatedTarget = retarget(relatedTarget, node);
|
|
304
|
+
}
|
|
305
|
+
// snapshot — listeners added during dispatch don't fire this round. Skip the
|
|
306
|
+
// slice alloc for the single-listener case (common React-delegated one): a
|
|
307
|
+
// 1-element, length-captured iteration can't be disturbed by an add/remove in
|
|
308
|
+
// that one call. Multi-listener keeps the slice (concurrent-mutation guard).
|
|
309
|
+
const snap = list.length === 1 ? list : list.slice();
|
|
310
|
+
for (let k = 0, len = snap.length; k < len; k++) {
|
|
311
|
+
const l = snap[k];
|
|
312
|
+
if (skipsPhase(l, phase)) continue;
|
|
313
|
+
if (l.once) removeListener(node, event.type, l);
|
|
314
|
+
callListener(node, event, l);
|
|
315
|
+
if (event._stopImmediate) return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// canceled activation: undo the pre-click toggle if default was prevented.
|
|
319
|
+
// Otherwise a checkbox/radio toggle fires input then change (activation
|
|
320
|
+
// default action) — user-event/React rely on these to see the new value.
|
|
321
|
+
function finishActivation(target, event, activation) {
|
|
322
|
+
if (event.defaultPrevented) activation.undo();
|
|
323
|
+
else if (activation.fireChange) {
|
|
324
|
+
target.dispatchEvent(new Event('input', { bubbles: true }));
|
|
325
|
+
target.dispatchEvent(new Event('change', { bubbles: true }));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
308
329
|
// on<event> handler properties (onclick, oninput, …). Defining these makes
|
|
309
330
|
// `'oninput' in document` true, so React skips its legacy attachEvent polyfill,
|
|
310
331
|
// and lets libraries assign el.onX = fn directly.
|