You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
951 lines
28 KiB
951 lines
28 KiB
"use strict";
|
|
|
|
const DOMException = require("domexception");
|
|
|
|
const EventTargetImpl = require("../events/EventTarget-impl").implementation;
|
|
const { simultaneousIterators } = require("../../utils");
|
|
const NODE_TYPE = require("../node-type");
|
|
const NODE_DOCUMENT_POSITION = require("../node-document-position");
|
|
const NodeList = require("../generated/NodeList");
|
|
const { clone, locateNamespacePrefix, locateNamespace } = require("../node");
|
|
const attributes = require("../attributes");
|
|
|
|
const { domSymbolTree } = require("../helpers/internal-constants");
|
|
const { documentBaseURLSerialized } = require("../helpers/document-base-url");
|
|
const { queueTreeMutationRecord } = require("../helpers/mutation-observers");
|
|
const {
|
|
isShadowRoot, getRoot, shadowIncludingRoot, assignSlot, assignSlotableForTree, assignSlotable,
|
|
signalSlotChange, isSlot
|
|
} = require("../helpers/shadow-dom");
|
|
|
|
function isObsoleteNodeType(node) {
|
|
return node.nodeType === NODE_TYPE.ENTITY_NODE ||
|
|
node.nodeType === NODE_TYPE.ENTITY_REFERENCE_NODE ||
|
|
node.nodeType === NODE_TYPE.NOTATION_NODE ||
|
|
// node.nodeType === NODE_TYPE.ATTRIBUTE_NODE || // this is missing how do we handle?
|
|
node.nodeType === NODE_TYPE.CDATA_SECTION_NODE;
|
|
}
|
|
|
|
function nodeEquals(a, b) {
|
|
if (a.nodeType !== b.nodeType) {
|
|
return false;
|
|
}
|
|
|
|
switch (a.nodeType) {
|
|
case NODE_TYPE.DOCUMENT_TYPE_NODE:
|
|
if (a.name !== b.name || a.publicId !== b.publicId ||
|
|
a.systemId !== b.systemId) {
|
|
return false;
|
|
}
|
|
break;
|
|
case NODE_TYPE.ELEMENT_NODE:
|
|
if (a._namespaceURI !== b._namespaceURI || a._prefix !== b._prefix || a._localName !== b._localName ||
|
|
a._attributes.length !== b._attributes.length) {
|
|
return false;
|
|
}
|
|
break;
|
|
case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
|
|
if (a._target !== b._target || a._data !== b._data) {
|
|
return false;
|
|
}
|
|
break;
|
|
case NODE_TYPE.TEXT_NODE:
|
|
case NODE_TYPE.COMMENT_NODE:
|
|
if (a._data !== b._data) {
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (a.nodeType === NODE_TYPE.ELEMENT_NODE && !attributes.attributeListsEqual(a, b)) {
|
|
return false;
|
|
}
|
|
|
|
for (const nodes of simultaneousIterators(domSymbolTree.childrenIterator(a), domSymbolTree.childrenIterator(b))) {
|
|
if (!nodes[0] || !nodes[1]) {
|
|
// mismatch in the amount of childNodes
|
|
return false;
|
|
}
|
|
|
|
if (!nodeEquals(nodes[0], nodes[1])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#concept-tree-host-including-inclusive-ancestor
|
|
function isHostInclusiveAncestor(nodeImplA, nodeImplB) {
|
|
for (const ancestor of domSymbolTree.ancestorsIterator(nodeImplB)) {
|
|
if (ancestor === nodeImplA) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const rootImplB = getRoot(nodeImplB);
|
|
if (rootImplB._host) {
|
|
return isHostInclusiveAncestor(nodeImplA, rootImplB._host);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
class NodeImpl extends EventTargetImpl {
|
|
constructor(args, privateData) {
|
|
super();
|
|
|
|
domSymbolTree.initialize(this);
|
|
|
|
this._ownerDocument = privateData.ownerDocument;
|
|
|
|
this._childNodesList = null;
|
|
this._childrenList = null;
|
|
this._version = 0;
|
|
this._memoizedQueries = {};
|
|
this._registeredObserverList = [];
|
|
}
|
|
|
|
_getTheParent() {
|
|
if (this._assignedSlot) {
|
|
return this._assignedSlot;
|
|
}
|
|
|
|
return domSymbolTree.parent(this);
|
|
}
|
|
|
|
get parentNode() {
|
|
return domSymbolTree.parent(this);
|
|
}
|
|
|
|
getRootNode(options) {
|
|
return options.composed ? shadowIncludingRoot(this) : getRoot(this);
|
|
}
|
|
|
|
get nodeName() {
|
|
switch (this.nodeType) {
|
|
case NODE_TYPE.ELEMENT_NODE:
|
|
return this.tagName;
|
|
case NODE_TYPE.TEXT_NODE:
|
|
return "#text";
|
|
case NODE_TYPE.CDATA_SECTION_NODE:
|
|
return "#cdata-section";
|
|
case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
|
|
return this.target;
|
|
case NODE_TYPE.COMMENT_NODE:
|
|
return "#comment";
|
|
case NODE_TYPE.DOCUMENT_NODE:
|
|
return "#document";
|
|
case NODE_TYPE.DOCUMENT_TYPE_NODE:
|
|
return this.name;
|
|
case NODE_TYPE.DOCUMENT_FRAGMENT_NODE:
|
|
return "#document-fragment";
|
|
}
|
|
|
|
// should never happen
|
|
return null;
|
|
}
|
|
|
|
get firstChild() {
|
|
return domSymbolTree.firstChild(this);
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#connected
|
|
// https://dom.spec.whatwg.org/#dom-node-isconnected
|
|
get isConnected() {
|
|
const root = shadowIncludingRoot(this);
|
|
return root && root.nodeType === NODE_TYPE.DOCUMENT_NODE;
|
|
}
|
|
|
|
get ownerDocument() {
|
|
return this.nodeType === NODE_TYPE.DOCUMENT_NODE ? null : this._ownerDocument;
|
|
}
|
|
|
|
get lastChild() {
|
|
return domSymbolTree.lastChild(this);
|
|
}
|
|
|
|
get childNodes() {
|
|
if (!this._childNodesList) {
|
|
this._childNodesList = NodeList.createImpl([], {
|
|
element: this,
|
|
query: () => domSymbolTree.childrenToArray(this)
|
|
});
|
|
} else {
|
|
this._childNodesList._update();
|
|
}
|
|
|
|
return this._childNodesList;
|
|
}
|
|
|
|
get nextSibling() {
|
|
return domSymbolTree.nextSibling(this);
|
|
}
|
|
|
|
get previousSibling() {
|
|
return domSymbolTree.previousSibling(this);
|
|
}
|
|
|
|
_modified() {
|
|
this._version++;
|
|
for (const ancestor of domSymbolTree.ancestorsIterator(this)) {
|
|
ancestor._version++;
|
|
}
|
|
|
|
if (this._childrenList) {
|
|
this._childrenList._update();
|
|
}
|
|
if (this._childNodesList) {
|
|
this._childNodesList._update();
|
|
}
|
|
this._clearMemoizedQueries();
|
|
}
|
|
|
|
_childTextContentChangeSteps() {
|
|
// Default: do nothing
|
|
}
|
|
|
|
_clearMemoizedQueries() {
|
|
this._memoizedQueries = {};
|
|
const myParent = domSymbolTree.parent(this);
|
|
if (myParent) {
|
|
myParent._clearMemoizedQueries();
|
|
}
|
|
}
|
|
|
|
_descendantRemoved(parent, child) {
|
|
const myParent = domSymbolTree.parent(this);
|
|
if (myParent) {
|
|
myParent._descendantRemoved(parent, child);
|
|
}
|
|
}
|
|
|
|
_descendantAdded(parent, child) {
|
|
const myParent = domSymbolTree.parent(this);
|
|
if (myParent) {
|
|
myParent._descendantAdded(parent, child);
|
|
}
|
|
}
|
|
|
|
_attach() {
|
|
this._attached = true;
|
|
|
|
for (const child of domSymbolTree.childrenIterator(this)) {
|
|
if (child._attach) {
|
|
child._attach();
|
|
}
|
|
}
|
|
}
|
|
|
|
_detach() {
|
|
this._attached = false;
|
|
|
|
if (this._ownerDocument && this._ownerDocument._lastFocusedElement === this) {
|
|
this._ownerDocument._lastFocusedElement = null;
|
|
}
|
|
|
|
for (const child of domSymbolTree.childrenIterator(this)) {
|
|
if (child._detach) {
|
|
child._detach();
|
|
}
|
|
}
|
|
}
|
|
|
|
hasChildNodes() {
|
|
return domSymbolTree.hasChildren(this);
|
|
}
|
|
|
|
normalize() {
|
|
for (const child of domSymbolTree.childrenIterator(this)) {
|
|
if (child.normalize) {
|
|
child.normalize();
|
|
}
|
|
|
|
// Normalize should only transform Text nodes, and nothing else.
|
|
if (child.nodeType !== NODE_TYPE.TEXT_NODE) {
|
|
continue;
|
|
}
|
|
|
|
if (child.nodeValue === "") {
|
|
this._remove(child);
|
|
continue;
|
|
}
|
|
|
|
const prevChild = domSymbolTree.previousSibling(child);
|
|
|
|
if (prevChild && prevChild.nodeType === NODE_TYPE.TEXT_NODE) {
|
|
// merge text nodes
|
|
prevChild.appendData(child.nodeValue);
|
|
this._remove(child);
|
|
}
|
|
}
|
|
}
|
|
|
|
get parentElement() {
|
|
const parentNode = domSymbolTree.parent(this);
|
|
return parentNode !== null && parentNode.nodeType === NODE_TYPE.ELEMENT_NODE ? parentNode : null;
|
|
}
|
|
|
|
get baseURI() {
|
|
return documentBaseURLSerialized(this._ownerDocument);
|
|
}
|
|
|
|
compareDocumentPosition(otherImpl) {
|
|
// Let reference be the context object.
|
|
const reference = this;
|
|
|
|
if (isObsoleteNodeType(reference) || isObsoleteNodeType(otherImpl)) {
|
|
throw new Error("Obsolete node type");
|
|
}
|
|
|
|
const result = domSymbolTree.compareTreePosition(reference, otherImpl);
|
|
|
|
// “If other and reference are not in the same tree, return the result of adding DOCUMENT_POSITION_DISCONNECTED,
|
|
// DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, and either DOCUMENT_POSITION_PRECEDING or
|
|
// DOCUMENT_POSITION_FOLLOWING, with the constraint that this is to be consistent, together.”
|
|
if (result === NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_DISCONNECTED) {
|
|
// symbol-tree does not add these bits required by the spec:
|
|
return NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_DISCONNECTED |
|
|
NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC |
|
|
NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_FOLLOWING;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
lookupPrefix(namespace) {
|
|
if (namespace === null || namespace === "") {
|
|
return null;
|
|
}
|
|
|
|
switch (this.nodeType) {
|
|
case NODE_TYPE.ELEMENT_NODE: {
|
|
return locateNamespacePrefix(this, namespace);
|
|
}
|
|
case NODE_TYPE.DOCUMENT_NODE: {
|
|
return this.documentElement !== null ? locateNamespacePrefix(this.documentElement, namespace) : null;
|
|
}
|
|
case NODE_TYPE.DOCUMENT_TYPE_NODE:
|
|
case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: {
|
|
return null;
|
|
}
|
|
case NODE_TYPE.ATTRIBUTE_NODE: {
|
|
return this._element !== null ? locateNamespacePrefix(this._element, namespace) : null;
|
|
}
|
|
default: {
|
|
return this.parentElement !== null ? locateNamespacePrefix(this.parentElement, namespace) : null;
|
|
}
|
|
}
|
|
}
|
|
|
|
lookupNamespaceURI(prefix) {
|
|
if (prefix === "") {
|
|
prefix = null;
|
|
}
|
|
|
|
return locateNamespace(this, prefix);
|
|
}
|
|
|
|
isDefaultNamespace(namespace) {
|
|
if (namespace === "") {
|
|
namespace = null;
|
|
}
|
|
|
|
const defaultNamespace = locateNamespace(this, null);
|
|
return defaultNamespace === namespace;
|
|
}
|
|
|
|
contains(other) {
|
|
if (other === null) {
|
|
return false;
|
|
} else if (this === other) {
|
|
return true;
|
|
}
|
|
return Boolean(this.compareDocumentPosition(other) & NODE_DOCUMENT_POSITION.DOCUMENT_POSITION_CONTAINED_BY);
|
|
}
|
|
|
|
isEqualNode(node) {
|
|
if (node === null) {
|
|
return false;
|
|
}
|
|
|
|
// Fast-path, not in the spec
|
|
if (this === node) {
|
|
return true;
|
|
}
|
|
|
|
return nodeEquals(this, node);
|
|
}
|
|
|
|
isSameNode(node) {
|
|
if (this === node) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
cloneNode(deep) {
|
|
if (isShadowRoot(this)) {
|
|
throw new DOMException("ShadowRoot nodes are not clonable.", "NotSupportedError");
|
|
}
|
|
|
|
deep = Boolean(deep);
|
|
|
|
return clone(this, undefined, deep);
|
|
}
|
|
|
|
get nodeValue() {
|
|
switch (this.nodeType) {
|
|
case NODE_TYPE.ATTRIBUTE_NODE: {
|
|
return this._value;
|
|
}
|
|
case NODE_TYPE.TEXT_NODE:
|
|
case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text
|
|
case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
|
|
case NODE_TYPE.COMMENT_NODE: {
|
|
return this._data;
|
|
}
|
|
default: {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
set nodeValue(value) {
|
|
if (value === null) {
|
|
value = "";
|
|
}
|
|
|
|
switch (this.nodeType) {
|
|
case NODE_TYPE.ATTRIBUTE_NODE: {
|
|
attributes.setAnExistingAttributeValue(this, value);
|
|
break;
|
|
}
|
|
case NODE_TYPE.TEXT_NODE:
|
|
case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text
|
|
case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
|
|
case NODE_TYPE.COMMENT_NODE: {
|
|
this.replaceData(0, this.length, value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
get textContent() {
|
|
switch (this.nodeType) {
|
|
case NODE_TYPE.DOCUMENT_FRAGMENT_NODE:
|
|
case NODE_TYPE.ELEMENT_NODE: {
|
|
let text = "";
|
|
for (const child of domSymbolTree.treeIterator(this)) {
|
|
if (child.nodeType === NODE_TYPE.TEXT_NODE || child.nodeType === NODE_TYPE.CDATA_SECTION_NODE) {
|
|
text += child.nodeValue;
|
|
}
|
|
}
|
|
return text;
|
|
}
|
|
|
|
case NODE_TYPE.ATTRIBUTE_NODE: {
|
|
return this._value;
|
|
}
|
|
|
|
case NODE_TYPE.TEXT_NODE:
|
|
case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text
|
|
case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
|
|
case NODE_TYPE.COMMENT_NODE: {
|
|
return this._data;
|
|
}
|
|
|
|
default: {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
set textContent(value) {
|
|
switch (this.nodeType) {
|
|
case NODE_TYPE.DOCUMENT_FRAGMENT_NODE:
|
|
case NODE_TYPE.ELEMENT_NODE: {
|
|
let nodeImpl = null;
|
|
|
|
if (value !== null && value !== "") {
|
|
nodeImpl = this._ownerDocument.createTextNode(value);
|
|
}
|
|
|
|
this._replaceAll(nodeImpl);
|
|
break;
|
|
}
|
|
|
|
case NODE_TYPE.ATTRIBUTE_NODE: {
|
|
attributes.setAnExistingAttributeValue(this, value);
|
|
break;
|
|
}
|
|
|
|
case NODE_TYPE.TEXT_NODE:
|
|
case NODE_TYPE.CDATA_SECTION_NODE: // CDATASection is a subclass of Text
|
|
case NODE_TYPE.PROCESSING_INSTRUCTION_NODE:
|
|
case NODE_TYPE.COMMENT_NODE: {
|
|
this.replaceData(0, this.length, value);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#dom-node-insertbefore
|
|
insertBefore(nodeImpl, childImpl) {
|
|
return this._preInsert(nodeImpl, childImpl);
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#dom-node-appendchild
|
|
appendChild(nodeImpl) {
|
|
return this._append(nodeImpl);
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#dom-node-replacechild
|
|
replaceChild(nodeImpl, childImpl) {
|
|
return this._replace(nodeImpl, childImpl);
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#dom-node-removechild
|
|
removeChild(oldChildImpl) {
|
|
return this._preRemove(oldChildImpl);
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity
|
|
_preInsertValidity(nodeImpl, childImpl) {
|
|
const { nodeType, nodeName } = nodeImpl;
|
|
const { nodeType: parentType, nodeName: parentName } = this;
|
|
|
|
if (
|
|
parentType !== NODE_TYPE.DOCUMENT_NODE &&
|
|
parentType !== NODE_TYPE.DOCUMENT_FRAGMENT_NODE &&
|
|
parentType !== NODE_TYPE.ELEMENT_NODE
|
|
) {
|
|
throw new DOMException(`Node can't be inserted in a ${parentName} parent.`, "HierarchyRequestError");
|
|
}
|
|
|
|
if (isHostInclusiveAncestor(nodeImpl, this)) {
|
|
throw new DOMException("The operation would yield an incorrect node tree.", "HierarchyRequestError");
|
|
}
|
|
|
|
if (childImpl && domSymbolTree.parent(childImpl) !== this) {
|
|
throw new DOMException("The child can not be found in the parent.", "NotFoundError");
|
|
}
|
|
|
|
if (
|
|
nodeType !== NODE_TYPE.DOCUMENT_FRAGMENT_NODE &&
|
|
nodeType !== NODE_TYPE.DOCUMENT_TYPE_NODE &&
|
|
nodeType !== NODE_TYPE.ELEMENT_NODE &&
|
|
nodeType !== NODE_TYPE.TEXT_NODE &&
|
|
nodeType !== NODE_TYPE.CDATA_SECTION_NODE && // CData section extends from Text
|
|
nodeType !== NODE_TYPE.PROCESSING_INSTRUCTION_NODE &&
|
|
nodeType !== NODE_TYPE.COMMENT_NODE
|
|
) {
|
|
throw new DOMException(`${nodeName} node can't be inserted in parent node.`, "HierarchyRequestError");
|
|
}
|
|
|
|
if (
|
|
(nodeType === NODE_TYPE.TEXT_NODE && parentType === NODE_TYPE.DOCUMENT_NODE) ||
|
|
(nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE && parentType !== NODE_TYPE.DOCUMENT_NODE)
|
|
) {
|
|
throw new DOMException(`${nodeName} node can't be inserted in ${parentName} parent.`, "HierarchyRequestError");
|
|
}
|
|
|
|
if (parentType === NODE_TYPE.DOCUMENT_NODE) {
|
|
const nodeChildren = domSymbolTree.childrenToArray(nodeImpl);
|
|
const parentChildren = domSymbolTree.childrenToArray(this);
|
|
|
|
switch (nodeType) {
|
|
case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: {
|
|
const nodeChildrenElements = nodeChildren.filter(child => child.nodeType === NODE_TYPE.ELEMENT_NODE);
|
|
if (nodeChildrenElements.length > 1) {
|
|
throw new DOMException(
|
|
`Invalid insertion of ${nodeName} node in ${parentName} node.`,
|
|
"HierarchyRequestError"
|
|
);
|
|
}
|
|
|
|
const hasNodeTextChildren = nodeChildren.some(child => child.nodeType === NODE_TYPE.TEXT_NODE);
|
|
if (hasNodeTextChildren) {
|
|
throw new DOMException(
|
|
`Invalid insertion of ${nodeName} node in ${parentName} node.`,
|
|
"HierarchyRequestError"
|
|
);
|
|
}
|
|
|
|
if (
|
|
nodeChildrenElements.length === 1 &&
|
|
(
|
|
parentChildren.some(child => child.nodeType === NODE_TYPE.ELEMENT_NODE) ||
|
|
(childImpl && childImpl.nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE) ||
|
|
(
|
|
childImpl &&
|
|
domSymbolTree.nextSibling(childImpl) &&
|
|
domSymbolTree.nextSibling(childImpl).nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE
|
|
)
|
|
)
|
|
) {
|
|
throw new DOMException(
|
|
`Invalid insertion of ${nodeName} node in ${parentName} node.`,
|
|
"HierarchyRequestError"
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case NODE_TYPE.ELEMENT_NODE:
|
|
if (
|
|
parentChildren.some(child => child.nodeType === NODE_TYPE.ELEMENT_NODE) ||
|
|
(childImpl && childImpl.nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE) ||
|
|
(
|
|
childImpl &&
|
|
domSymbolTree.nextSibling(childImpl) &&
|
|
domSymbolTree.nextSibling(childImpl).nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE
|
|
)
|
|
) {
|
|
throw new DOMException(
|
|
`Invalid insertion of ${nodeName} node in ${parentName} node.`,
|
|
"HierarchyRequestError"
|
|
);
|
|
}
|
|
break;
|
|
|
|
case NODE_TYPE.DOCUMENT_TYPE_NODE:
|
|
if (
|
|
parentChildren.some(child => child.nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE) ||
|
|
(
|
|
childImpl &&
|
|
domSymbolTree.previousSibling(childImpl) &&
|
|
domSymbolTree.previousSibling(childImpl).nodeType === NODE_TYPE.ELEMENT_NODE
|
|
) ||
|
|
(!childImpl && parentChildren.some(child => child.nodeType === NODE_TYPE.ELEMENT_NODE))
|
|
) {
|
|
throw new DOMException(
|
|
`Invalid insertion of ${nodeName} node in ${parentName} node.`,
|
|
"HierarchyRequestError"
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#concept-node-pre-insert
|
|
_preInsert(nodeImpl, childImpl) {
|
|
this._preInsertValidity(nodeImpl, childImpl);
|
|
|
|
let referenceChildImpl = childImpl;
|
|
if (referenceChildImpl === nodeImpl) {
|
|
referenceChildImpl = domSymbolTree.nextSibling(nodeImpl);
|
|
}
|
|
|
|
this._ownerDocument._adoptNode(nodeImpl);
|
|
|
|
this._insert(nodeImpl, referenceChildImpl);
|
|
|
|
return nodeImpl;
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#concept-node-insert
|
|
_insert(nodeImpl, childImpl, suppressObservers) {
|
|
const nodesImpl = nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE ?
|
|
domSymbolTree.childrenToArray(nodeImpl) :
|
|
[nodeImpl];
|
|
|
|
if (nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE) {
|
|
let grandChildImpl;
|
|
while ((grandChildImpl = domSymbolTree.firstChild(nodeImpl))) {
|
|
nodeImpl._remove(grandChildImpl, true);
|
|
}
|
|
}
|
|
|
|
if (nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE) {
|
|
queueTreeMutationRecord(nodeImpl, [], nodesImpl, null, null);
|
|
}
|
|
|
|
const previousChildImpl = childImpl ?
|
|
domSymbolTree.previousSibling(childImpl) :
|
|
domSymbolTree.lastChild(this);
|
|
|
|
for (const node of nodesImpl) {
|
|
if (!childImpl) {
|
|
domSymbolTree.appendChild(this, node);
|
|
} else {
|
|
domSymbolTree.insertBefore(childImpl, node);
|
|
}
|
|
|
|
if (
|
|
(this.nodeType === NODE_TYPE.ELEMENT_NODE && this._shadowRoot !== null) &&
|
|
(node.nodeType === NODE_TYPE.ELEMENT_NODE || node.nodeType === NODE_TYPE.TEXT_NODE)
|
|
) {
|
|
assignSlot(node);
|
|
}
|
|
|
|
this._modified();
|
|
|
|
if (node.nodeType === NODE_TYPE.TEXT_NODE ||
|
|
node.nodeType === NODE_TYPE.CDATA_SECTION_NODE) {
|
|
this._childTextContentChangeSteps();
|
|
}
|
|
|
|
if (isSlot(this) && this._assignedNodes.length === 0 && isShadowRoot(getRoot(this))) {
|
|
signalSlotChange(this);
|
|
}
|
|
|
|
const root = getRoot(node);
|
|
if (isShadowRoot(root)) {
|
|
assignSlotableForTree(root);
|
|
}
|
|
|
|
if (this._attached && nodeImpl._attach) {
|
|
node._attach();
|
|
}
|
|
|
|
this._descendantAdded(this, node);
|
|
}
|
|
|
|
if (!suppressObservers) {
|
|
queueTreeMutationRecord(this, nodesImpl, [], previousChildImpl, childImpl);
|
|
}
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#concept-node-append
|
|
_append(nodeImpl) {
|
|
return this._preInsert(nodeImpl, null);
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#concept-node-replace
|
|
_replace(nodeImpl, childImpl) {
|
|
const { nodeType, nodeName } = nodeImpl;
|
|
const { nodeType: parentType, nodeName: parentName } = this;
|
|
|
|
// Note: This section differs from the pre-insert validation algorithm.
|
|
if (
|
|
parentType !== NODE_TYPE.DOCUMENT_NODE &&
|
|
parentType !== NODE_TYPE.DOCUMENT_FRAGMENT_NODE &&
|
|
parentType !== NODE_TYPE.ELEMENT_NODE
|
|
) {
|
|
throw new DOMException(`Node can't be inserted in a ${parentName} parent.`, "HierarchyRequestError");
|
|
}
|
|
|
|
if (isHostInclusiveAncestor(nodeImpl, this)) {
|
|
throw new DOMException("The operation would yield an incorrect node tree.", "HierarchyRequestError");
|
|
}
|
|
|
|
if (childImpl && domSymbolTree.parent(childImpl) !== this) {
|
|
throw new DOMException("The child can not be found in the parent.", "NotFoundError");
|
|
}
|
|
|
|
if (
|
|
nodeType !== NODE_TYPE.DOCUMENT_FRAGMENT_NODE &&
|
|
nodeType !== NODE_TYPE.DOCUMENT_TYPE_NODE &&
|
|
nodeType !== NODE_TYPE.ELEMENT_NODE &&
|
|
nodeType !== NODE_TYPE.TEXT_NODE &&
|
|
nodeType !== NODE_TYPE.CDATA_SECTION_NODE && // CData section extends from Text
|
|
nodeType !== NODE_TYPE.PROCESSING_INSTRUCTION_NODE &&
|
|
nodeType !== NODE_TYPE.COMMENT_NODE
|
|
) {
|
|
throw new DOMException(`${nodeName} node can't be inserted in parent node.`, "HierarchyRequestError");
|
|
}
|
|
|
|
if (
|
|
(nodeType === NODE_TYPE.TEXT_NODE && parentType === NODE_TYPE.DOCUMENT_NODE) ||
|
|
(nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE && parentType !== NODE_TYPE.DOCUMENT_NODE)
|
|
) {
|
|
throw new DOMException(`${nodeName} node can't be inserted in ${parentName} parent.`, "HierarchyRequestError");
|
|
}
|
|
|
|
if (parentType === NODE_TYPE.DOCUMENT_NODE) {
|
|
const nodeChildren = domSymbolTree.childrenToArray(nodeImpl);
|
|
const parentChildren = domSymbolTree.childrenToArray(this);
|
|
|
|
switch (nodeType) {
|
|
case NODE_TYPE.DOCUMENT_FRAGMENT_NODE: {
|
|
const nodeChildrenElements = nodeChildren.filter(child => child.nodeType === NODE_TYPE.ELEMENT_NODE);
|
|
if (nodeChildrenElements.length > 1) {
|
|
throw new DOMException(
|
|
`Invalid insertion of ${nodeName} node in ${parentName} node.`,
|
|
"HierarchyRequestError"
|
|
);
|
|
}
|
|
|
|
const hasNodeTextChildren = nodeChildren.some(child => child.nodeType === NODE_TYPE.TEXT_NODE);
|
|
if (hasNodeTextChildren) {
|
|
throw new DOMException(
|
|
`Invalid insertion of ${nodeName} node in ${parentName} node.`,
|
|
"HierarchyRequestError"
|
|
);
|
|
}
|
|
|
|
|
|
const parentChildElements = parentChildren.filter(child => child.nodeType === NODE_TYPE.ELEMENT_NODE);
|
|
if (
|
|
nodeChildrenElements.length === 1 &&
|
|
(
|
|
(parentChildElements.length === 1 && parentChildElements[0] !== childImpl) ||
|
|
(
|
|
childImpl &&
|
|
domSymbolTree.nextSibling(childImpl) &&
|
|
domSymbolTree.nextSibling(childImpl).nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE
|
|
)
|
|
)
|
|
) {
|
|
throw new DOMException(
|
|
`Invalid insertion of ${nodeName} node in ${parentName} node.`,
|
|
"HierarchyRequestError"
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case NODE_TYPE.ELEMENT_NODE:
|
|
if (
|
|
parentChildren.some(child => child.nodeType === NODE_TYPE.ELEMENT_NODE && child !== childImpl) ||
|
|
(
|
|
childImpl &&
|
|
domSymbolTree.nextSibling(childImpl) &&
|
|
domSymbolTree.nextSibling(childImpl).nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE
|
|
)
|
|
) {
|
|
throw new DOMException(
|
|
`Invalid insertion of ${nodeName} node in ${parentName} node.`,
|
|
"HierarchyRequestError"
|
|
);
|
|
}
|
|
break;
|
|
|
|
case NODE_TYPE.DOCUMENT_TYPE_NODE:
|
|
if (
|
|
parentChildren.some(child => child.nodeType === NODE_TYPE.DOCUMENT_TYPE_NODE && child !== childImpl) ||
|
|
(
|
|
childImpl &&
|
|
domSymbolTree.previousSibling(childImpl) &&
|
|
domSymbolTree.previousSibling(childImpl).nodeType === NODE_TYPE.ELEMENT_NODE
|
|
)
|
|
) {
|
|
throw new DOMException(
|
|
`Invalid insertion of ${nodeName} node in ${parentName} node.`,
|
|
"HierarchyRequestError"
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
let referenceChildImpl = domSymbolTree.nextSibling(childImpl);
|
|
if (referenceChildImpl === nodeImpl) {
|
|
referenceChildImpl = domSymbolTree.nextSibling(nodeImpl);
|
|
}
|
|
|
|
const previousSiblingImpl = domSymbolTree.previousSibling(childImpl);
|
|
|
|
this._ownerDocument._adoptNode(nodeImpl);
|
|
|
|
let removedNodesImpl = [];
|
|
|
|
if (domSymbolTree.parent(childImpl)) {
|
|
removedNodesImpl = [childImpl];
|
|
this._remove(childImpl, true);
|
|
}
|
|
|
|
const nodesImpl = nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE ?
|
|
domSymbolTree.childrenToArray(nodeImpl) :
|
|
[nodeImpl];
|
|
|
|
this._insert(nodeImpl, referenceChildImpl, true);
|
|
|
|
queueTreeMutationRecord(this, nodesImpl, removedNodesImpl, previousSiblingImpl, referenceChildImpl);
|
|
|
|
return childImpl;
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#concept-node-replace-all
|
|
_replaceAll(nodeImpl) {
|
|
if (nodeImpl !== null) {
|
|
this._ownerDocument._adoptNode(nodeImpl);
|
|
}
|
|
|
|
const removedNodesImpl = domSymbolTree.childrenToArray(this);
|
|
|
|
let addedNodesImpl;
|
|
if (nodeImpl === null) {
|
|
addedNodesImpl = [];
|
|
} else if (nodeImpl.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE) {
|
|
addedNodesImpl = domSymbolTree.childrenToArray(nodeImpl);
|
|
} else {
|
|
addedNodesImpl = [nodeImpl];
|
|
}
|
|
|
|
for (const childImpl of domSymbolTree.childrenIterator(this)) {
|
|
this._remove(childImpl, true);
|
|
}
|
|
|
|
if (nodeImpl) {
|
|
this._insert(nodeImpl, null, true);
|
|
}
|
|
|
|
queueTreeMutationRecord(this, addedNodesImpl, removedNodesImpl, null, null);
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#concept-node-pre-remove
|
|
_preRemove(childImpl) {
|
|
if (domSymbolTree.parent(childImpl) !== this) {
|
|
throw new DOMException("The node to be removed is not a child of this node.", "NotFoundError");
|
|
}
|
|
|
|
this._remove(childImpl);
|
|
|
|
return childImpl;
|
|
}
|
|
|
|
// https://dom.spec.whatwg.org/#concept-node-remove
|
|
_remove(nodeImpl, suppressObservers) {
|
|
if (this._ownerDocument) {
|
|
this._ownerDocument._runPreRemovingSteps(nodeImpl);
|
|
}
|
|
|
|
const oldPreviousSiblingImpl = domSymbolTree.previousSibling(nodeImpl);
|
|
const oldNextSiblingImpl = domSymbolTree.nextSibling(nodeImpl);
|
|
|
|
domSymbolTree.remove(nodeImpl);
|
|
|
|
if (nodeImpl._assignedSlot) {
|
|
assignSlotable(nodeImpl._assignedSlot);
|
|
}
|
|
|
|
if (isSlot(this) && this._assignedNodes.length === 0 && isShadowRoot(getRoot(this))) {
|
|
signalSlotChange(this);
|
|
}
|
|
|
|
let hasSlotDescendant = isSlot(nodeImpl);
|
|
if (!hasSlotDescendant) {
|
|
for (const child of domSymbolTree.treeIterator(nodeImpl)) {
|
|
if (isSlot(child)) {
|
|
hasSlotDescendant = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasSlotDescendant) {
|
|
assignSlotableForTree(getRoot(this));
|
|
assignSlotableForTree(nodeImpl);
|
|
}
|
|
|
|
this._modified();
|
|
nodeImpl._detach();
|
|
this._descendantRemoved(this, nodeImpl);
|
|
|
|
if (!suppressObservers) {
|
|
queueTreeMutationRecord(this, [], [nodeImpl], oldPreviousSiblingImpl, oldNextSiblingImpl);
|
|
}
|
|
|
|
if (nodeImpl.nodeType === NODE_TYPE.TEXT_NODE) {
|
|
this._childTextContentChangeSteps();
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
implementation: NodeImpl
|
|
};
|
|
|