Skip to content

Commit 71b0af0

Browse files
committed
Implement changes from whatwg/dom#819
The DOM spec is currently broken because of changes around the adoption of nodes. There is an open PR which reverts some of these changes. This implements the same changes as in that PR to fix issues where some mutations would otherwise generate invalid mutation records.
1 parent b06bb4c commit 71b0af0

File tree

3 files changed

+63
-54
lines changed

3 files changed

+63
-54
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ This library is currently aimed at providing a lightweight and consistent experi
5858

5959
Do not rely on the behavior or presence of any methods and properties not specified in the DOM standard. For example, do not use JavaScript array methods exposed on properties that should expose a NodeList and do not use Element as a constructor. This behavior is _not_ considered public API and may change without warning in a future release.
6060

61+
This library implements the changes from [whatwg/dom#819][dom-adopt-pr], as the specification as currently described has known bugs around adoption.
62+
6163
### Parsing
6264

6365
This library does not implement the `DOMParser` interface, nor `insertAdjacentHTML` on `Element`, nor `createContextualFragment` on `Range`. The `innerHTML` and `outerHTML` properties are read-only,
@@ -80,7 +82,7 @@ Emulating a full browser environment is not the goal of this library. Consider u
8082

8183
This implementation offers no special treatment of HTML documents, which means there are no implementations of `HTMLElement` and its subclasses. This also affects HTML-specific casing behavior for attributes and tagNames. The `id` / `className` / `classList` properties on `Element` and `compatMode` / `contentType` on `Document` have not been implemented. HTML-specific query methods (`getElementById` for interface `NonElementParentNode`, `getElementsByClassName` on `Document`) are also missing.
8284

83-
This library also does not currently implement events, including the `Event` / `EventTarget` interfaces. It also currently does not contain an implementation of `AbortController` / `AbortSignal`. As these may have wider applications than browser-specific use cases, please file an issue if you have a use for these in your application and would like support for them to be added.
85+
This library does not currently implement events, including the `Event` / `EventTarget` interfaces. It also currently does not contain an implementation of `AbortController` / `AbortSignal`. As these may have wider applications than browser-specific use cases, please file an issue if you have a use for these in your application and would like support for them to be added.
8486

8587
There is currently no support for shadow DOM, so no `Slottable` / `ShadowRoot` interfaces and no `slot` / `attachShadow` / `shadowRoot` on `Element`. Slimdom also does not support the APIs for custom elements using the `is` option on `createElement` / `createElementNS`.
8688

@@ -97,6 +99,7 @@ The following features are missing simply because I have not yet had a need for
9799
- `attributeFilter` for mutation observers.
98100
- `isConnected` / `getRootNode` / `isEqualNode` / `isSameNode` / `compareDocumentPosition` on `Node`
99101

102+
[dom-adopt-pr]: https://github.com/whatwg/dom/pull/819
100103
[slimdom-sax-parser]: https://github.com/wvbe/slimdom-sax-parser
101104
[fontoxpath]: https://github.com/FontoXML/fontoxpath/
102105
[parse5-example]: https://github.com/bwrrp/slimdom.js/tree/main/test/examples/parse5

src/Document.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,9 @@ export default class Document extends Node implements NonElementParentNode, Pare
430430
}
431431

432432
// 2. If node is a shadow root, then throw a HierarchyRequestError.
433-
// 3. If node is a DocumentFragment whose host is non-null, then return.
434-
// (shadow dom not implemented)
433+
// 3. If node is a DocumentFragment node and its host is non-null, then return node.
434+
// Note: unfortunately this does not throw for web compatibility.
435+
// (shadow dom and HTML templates not implemented)
435436

436437
// 4. Adopt node into this.
437438
adoptNode(node, this);

src/util/mutationAlgorithms.ts

+56-51
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import { throwHierarchyRequestError, throwNotFoundError } from './errorHelpers';
22
import { NodeType, isNodeOfType } from './NodeType';
3-
import {
4-
determineLengthOfNode,
5-
getNodeDocument,
6-
getNodeIndex,
7-
forEachInclusiveDescendant,
8-
} from './treeHelpers';
3+
import { getNodeDocument, getNodeIndex, forEachInclusiveDescendant } from './treeHelpers';
94
import { insertIntoChildren, removeFromChildren } from './treeMutations';
105
import Document from '../Document';
116
import DocumentFragment from '../DocumentFragment';
@@ -179,10 +174,13 @@ export function preInsertNode<TNode extends Node>(
179174
referenceChild = node.nextSibling;
180175
}
181176

182-
// 4. Insert node into parent before referenceChild.
177+
// 4. Adopt node into parent's node document.
178+
adoptNode(node, getNodeDocument(parent));
179+
180+
// 5. Insert node into parent before referenceChild.
183181
insertNode(node, parent, referenceChild);
184182

185-
// 5. Return node.
183+
// 6. Return node.
186184
return node;
187185
}
188186

@@ -247,40 +245,30 @@ export function insertNode(
247245
// 6. Let previousSibling be child’s previous sibling or parent’s last child if child is null.
248246
let previousSibling = child === null ? parent.lastChild : child.previousSibling;
249247

250-
// Non-standard: it appears the standard as of 27 January 2021 does not account for
251-
// previousSibling now possibly being node, which can happen, for instance, when doing
252-
// parent.insertBefore(child, child);
253-
if (previousSibling === node) {
254-
previousSibling = node.previousSibling;
255-
}
256-
257248
// 7. For each node in nodes, in tree order:
258249
nodes.forEach((node) => {
259-
// 7.1. Adopt node into parent's node document.
260-
adoptNode(node, getNodeDocument(parent));
261-
262-
// 7.2. If child is null, then append node to parent’s children.
263-
// 7.3. Otherwise, insert node into parent’s children before child’s index.
250+
// 7.1. If child is null, then append node to parent’s children.
251+
// 7.2. Otherwise, insert node into parent’s children before child’s index.
264252
insertIntoChildren(node, parent, child);
265253

266-
// 7.4. If parent is a shadow host and node is a slottable, then assign a slot for node.
254+
// 7.3. If parent is a shadow host and node is a slottable, then assign a slot for node.
267255
// (shadow dom not implemented)
268256

269-
// 7.5. If parent's root is a shadow root, and parent is a slot whose assigned nodes is the
257+
// 7.4. If parent's root is a shadow root, and parent is a slot whose assigned nodes is the
270258
// empty list, then run signal a slot change for parent.
271-
// 7.6. Run assign slottables for a tree with node’s tree.
259+
// 7.5. Run assign slottables for a tree with node’s tree.
272260
// (shadow dom not implemented)
273261

274-
// 7.7. For each shadow-including inclusive descendant inclusiveDescendant of node, in
262+
// 7.6. For each shadow-including inclusive descendant inclusiveDescendant of node, in
275263
// shadow-including tree order:
276-
// 7.7.1. Run the insertion steps with inclusiveDescendant.
264+
// 7.6.1. Run the insertion steps with inclusiveDescendant.
277265
// (insertion steps not implemented)
278266

279-
// 7.7.2. If inclusiveDescendant is connected, then:
280-
// 7.7.2.1. If inclusiveDescendant is custom, then enqueue a custom element callback
267+
// 7.6.2. If inclusiveDescendant is connected, then:
268+
// 7.6.2.1. If inclusiveDescendant is custom, then enqueue a custom element callback
281269
// reaction with inclusiveDescendant, callback name "connectedCallback", and an empty
282270
// argument list.
283-
// 7.7.2.2. Otherwise, try to upgrade inclusiveDescendant. If this successfully upgrades
271+
// 7.6.2.2. Otherwise, try to upgrade inclusiveDescendant. If this successfully upgrades
284272
// inclusiveDescendant, its connectedCallback will be enqueued automatically during the
285273
// upgrade an element algorithm.
286274
// (custom elements not implemented)
@@ -465,11 +453,13 @@ export function replaceChildWithNode<TChild extends Node>(
465453
// 9. Let previousSibling be child’s previous sibling.
466454
const previousSibling = child.previousSibling;
467455

468-
// 10. Let removedNodes be the empty set.
456+
// 10. Adopt node into parent's node document
457+
adoptNode(node, getNodeDocument(parent));
458+
459+
// 11. Let removedNodes be the empty set.
469460
let removedNodes: Node[] = [];
470461

471-
// 11. If child’s parent is non-null, then:
472-
/* istanbul ignore else */
462+
// 12. If child’s parent is non-null, then:
473463
if (child.parentNode !== null) {
474464
// 11.1. Set removedNodes to « child ».
475465
removedNodes.push(child);
@@ -478,17 +468,16 @@ export function replaceChildWithNode<TChild extends Node>(
478468
removeNode(child, true);
479469
}
480470
// The above can only be false if child is node.
481-
// (TODO: this is no longer the case, at least until whatwg/dom#819 is merged)
482471

483-
// 12. Let nodes be node’s children if node is a DocumentFragment node; otherwise « node ».
472+
// 13. Let nodes be node’s children if node is a DocumentFragment node; otherwise « node ».
484473
const nodes = isNodeOfType(node, NodeType.DOCUMENT_FRAGMENT_NODE)
485474
? Array.from(node.childNodes)
486475
: [node];
487476

488-
// 13. Insert node into parent before referenceChild with the suppress observers flag set.
477+
// 14. Insert node into parent before referenceChild with the suppress observers flag set.
489478
insertNode(node, parent, referenceChild, true);
490479

491-
// 14. Queue a tree mutation record for parent with nodes, removedNodes, previousSibling and
480+
// 15. Queue a tree mutation record for parent with nodes, removedNodes, previousSibling and
492481
// referenceChild.
493482
queueMutationRecord('childList', parent, {
494483
addedNodes: nodes,
@@ -497,7 +486,7 @@ export function replaceChildWithNode<TChild extends Node>(
497486
previousSibling: previousSibling,
498487
});
499488

500-
// 15. Return child.
489+
// 16. Return child.
501490
return child;
502491
}
503492

@@ -508,36 +497,41 @@ export function replaceChildWithNode<TChild extends Node>(
508497
* @param parent Parent to replace under
509498
*/
510499
function replaceAllWithNode(node: Node | null, parent: Node): void {
511-
// 1. Let removedNodes be parent’s children.
500+
// 1. If node is non-null, then adopt node into parent's node document
501+
if (node !== null) {
502+
adoptNode(node, getNodeDocument(parent));
503+
}
504+
505+
// 2. Let removedNodes be parent’s children.
512506
const removedNodes = Array.from(parent.childNodes);
513507

514-
// 2. Let addedNodes be the empty set.
508+
// 3. Let addedNodes be the empty set.
515509
let addedNodes: Node[] = [];
516510

517511
if (node !== null) {
518-
// 3. If node is a DocumentFragment node, then set addedNodes to node's children.
512+
// 4. If node is a DocumentFragment node, then set addedNodes to node's children.
519513
if (isNodeOfType(node, NodeType.DOCUMENT_FRAGMENT_NODE)) {
520514
node.childNodes.forEach((child) => {
521515
addedNodes.push(child);
522516
});
523517
} else {
524-
// 4. Otherwise, if node is non-null, set addedNodes to « node ».
518+
// 5. Otherwise, if node is non-null, set addedNodes to « node ».
525519
addedNodes.push(node);
526520
}
527521
}
528522

529-
// 5. Remove all parent’s children, in tree order, with the suppress observers flag set.
523+
// 6. Remove all parent’s children, in tree order, with the suppress observers flag set.
530524
removedNodes.forEach((child) => {
531525
removeNode(child, true);
532526
});
533527

534-
// 6. If node is non-null, then insert node into parent before null with the suppress observers
528+
// 7. If node is non-null, then insert node into parent before null with the suppress observers
535529
// flag set.
536530
if (node !== null) {
537531
insertNode(node, parent, null, true);
538532
}
539533

540-
// 7. If either addedNodes or removedNodes is not empty, then queue a tree mutation record for
534+
// 8. If either addedNodes or removedNodes is not empty, then queue a tree mutation record for
541535
// parent with addedNodes, removedNodes, null, and null.
542536
if (addedNodes.length > 0 || removedNodes.length > 0) {
543537
queueMutationRecord('childList', parent, {
@@ -688,13 +682,18 @@ export function removeNode(node: Node, suppressObservers: boolean = false): void
688682
/**
689683
* 3.5. Interface Document
690684
*
691-
* To adopt a node into a document, run these steps:
685+
* To adopt a node into a document, with an optional forceDocumentFragmentAdoption, run these steps:
686+
*
687+
* (forceDocumentFragmentAdoption is only set to true for HTML template, so is not implemented here)
692688
*
693689
* @param node - Node to adopt
694690
* @param document - Document to adopt node into
695691
*/
696692
export function adoptNode(node: Node, document: Document): void {
697-
// 1. Let oldDocument be node’s node document.
693+
// 1. If forceDocumentFragmentAdoption is not given, then set it false.
694+
// (value unused)
695+
696+
// 2. Let oldDocument be node’s node document.
698697
const oldDocument = getNodeDocument(node);
699698

700699
// 2. If node’s parent is non-null, remove node.
@@ -708,15 +707,21 @@ export function adoptNode(node: Node, document: Document): void {
708707
}
709708

710709
// 3.1. For each inclusiveDescendant in node’s shadow-including inclusive descendants:
711-
forEachInclusiveDescendant(node, (node) => {
712-
// 3.1.1. Set inclusiveDescendant’s node document to document.
710+
forEachInclusiveDescendant(node, (inclusiveDescendant) => {
711+
// 3.1.1. If forceDocumentFragmentAdoption is false, inclusiveDescendant is a
712+
// DocumentFragment node, inclusiveDescendant is node, and node's host is non-null, then
713+
// continue
714+
// Note: this is only reasonable as long as all adopt callers remove the children of node.
715+
// (shadow dom and HTML templates not implemented)
716+
717+
// 3.1.2. Set inclusiveDescendant’s node document to document.
713718
// (calling code ensures that node is never a Document)
714-
node.ownerDocument = document;
719+
inclusiveDescendant.ownerDocument = document;
715720

716-
// 3.1.2. If inclusiveDescendant is an element, then set the node document of each attribute
721+
// 3.1.3. If inclusiveDescendant is an element, then set the node document of each attribute
717722
// in inclusiveDescendant’s attribute list to document.
718-
if (isNodeOfType(node, NodeType.ELEMENT_NODE)) {
719-
for (const attr of (node as Element).attributes) {
723+
if (isNodeOfType(inclusiveDescendant, NodeType.ELEMENT_NODE)) {
724+
for (const attr of (inclusiveDescendant as Element).attributes) {
720725
attr.ownerDocument = document;
721726
}
722727
}

0 commit comments

Comments
 (0)