diff --git a/lib/dom.js b/lib/dom.js index d2001fdac..be7891c78 100644 --- a/lib/dom.js +++ b/lib/dom.js @@ -514,7 +514,7 @@ Node.prototype = { }, replaceChild: function (newChild, oldChild) { //raises - this.insertBefore(newChild, oldChild); + _insertBefore(this, newChild, oldChild, assertPreReplacementValidityInDocument); if (oldChild) { this.removeChild(oldChild); } @@ -831,8 +831,35 @@ function isElementInsertionPossible(doc, child) { var docTypeNode = find(parentChildNodes, isDocTypeNode); return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child)); } + +/** + * Check if en element node can be inserted before `child`, or at the end if child is falsy, + * according to the presence and position of a doctype node on the same level. + * + * @param {Node} doc The document node + * @param {Node} child the node that would become the nextSibling if the element would be inserted + * @returns {boolean} `true` if an element can be inserted before child + * @private + * https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity + */ +function isElementReplacementPossible(doc, child) { + var parentChildNodes = doc.childNodes || []; + + function hasElementChildThatIsNotChild(node) { + return isElementNode(node) && node !== child; + } + + if (find(parentChildNodes, hasElementChildThatIsNotChild)) { + return false; + } + var docTypeNode = find(parentChildNodes, isDocTypeNode); + return !(child && docTypeNode && parentChildNodes.indexOf(docTypeNode) > parentChildNodes.indexOf(child)); +} + /** * @private + * Steps 1-5 of the checks before inserting and before replacing a child are the same. + * * @param {Node} parent the parent node to insert `node` into * @param {Node} node the node to insert * @param {Node=} child the node that should become the `nextSibling` of `node` @@ -840,18 +867,26 @@ function isElementInsertionPossible(doc, child) { * @throws DOMException for several node combinations that would create a DOM that is not well-formed. * @throws DOMException if `child` is provided but is not a child of `parent`. * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity + * @see https://dom.spec.whatwg.org/#concept-node-replace */ -function _insertBefore(parent, node, child) { +function assertPreInsertionValidity1to5(parent, node, child) { + // 1. If `parent` is not a Document, DocumentFragment, or Element node, then throw a "HierarchyRequestError" DOMException. if (!hasValidParentNodeType(parent)) { throw new DOMException(HIERARCHY_REQUEST_ERR, 'Unexpected parent node type ' + parent.nodeType); } + // 2. If `node` is a host-including inclusive ancestor of `parent`, then throw a "HierarchyRequestError" DOMException. + // not implemented! + // 3. If `child` is non-null and its parent is not `parent`, then throw a "NotFoundError" DOMException. if (child && child.parentNode !== parent) { throw new DOMException(NOT_FOUND_ERR, 'child not in parent'); } if ( + // 4. If `node` is not a DocumentFragment, DocumentType, Element, or CharacterData node, then throw a "HierarchyRequestError" DOMException. !hasInsertableNodeType(node) || + // 5. If either `node` is a Text node and `parent` is a document, // the sax parser currently adds top level text nodes, this will be fixed in 0.9.0 // || (node.nodeType === Node.TEXT_NODE && parent.nodeType === Node.DOCUMENT_NODE) + // or `node` is a doctype and `parent` is not a document, then throw a "HierarchyRequestError" DOMException. (isDocTypeNode(node) && parent.nodeType !== Node.DOCUMENT_NODE) ) { throw new DOMException( @@ -859,36 +894,137 @@ function _insertBefore(parent, node, child) { 'Unexpected node type ' + node.nodeType + ' for parent node type ' + parent.nodeType ); } +} + +/** + * @private + * Step 6 of the checks before inserting and before replacing a child are different. + * + * @param {Document} parent the parent node to insert `node` into + * @param {Node} node the node to insert + * @param {Node | undefined} child the node that should become the `nextSibling` of `node` + * @returns {Node} + * @throws DOMException for several node combinations that would create a DOM that is not well-formed. + * @throws DOMException if `child` is provided but is not a child of `parent`. + * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity + * @see https://dom.spec.whatwg.org/#concept-node-replace + */ +function assertPreInsertionValidityInDocument(parent, node, child) { var parentChildNodes = parent.childNodes || []; var nodeChildNodes = node.childNodes || []; - if (parent.nodeType === Node.DOCUMENT_NODE) { - if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - var nodeChildElements = nodeChildNodes.filter(isElementNode); - if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) { - throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment'); - } - if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) { - throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype'); - } + + // DocumentFragment + if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + var nodeChildElements = nodeChildNodes.filter(isElementNode); + // If node has more than one element child or has a Text node child. + if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) { + throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment'); } - if (isElementNode(node)) { - if (find(parentChildNodes, isElementNode) || !isElementInsertionPossible(parent, child)) { - throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype'); - } + // Otherwise, if `node` has one element child and either `parent` has an element child, + // `child` is a doctype, or `child` is non-null and a doctype is following `child`. + if (nodeChildElements.length === 1 && !isElementInsertionPossible(parent, child)) { + throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype'); } - if (isDocTypeNode(node)) { - if (find(parentChildNodes, isDocTypeNode)) { - throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed'); - } - var parentElementChild = find(parentChildNodes, isElementNode); - if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) { - throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element'); - } - if (!child && parentElementChild) { - throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present'); - } + } + // Element + if (isElementNode(node)) { + // `parent` has an element child, `child` is a doctype, + // or `child` is non-null and a doctype is following `child`. + if (!isElementInsertionPossible(parent, child)) { + throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype'); } } + // DocumentType + if (isDocTypeNode(node)) { + // `parent` has a doctype child, + if (find(parentChildNodes, isDocTypeNode)) { + throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed'); + } + var parentElementChild = find(parentChildNodes, isElementNode); + // `child` is non-null and an element is preceding `child`, + if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) { + throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element'); + } + // or `child` is null and `parent` has an element child. + if (!child && parentElementChild) { + throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can not be appended since element is present'); + } + } +} + +/** + * @private + * Step 6 of the checks before inserting and before replacing a child are different. + * + * @param {Document} parent the parent node to insert `node` into + * @param {Node} node the node to insert + * @param {Node | undefined} child the node that should become the `nextSibling` of `node` + * @returns {Node} + * @throws DOMException for several node combinations that would create a DOM that is not well-formed. + * @throws DOMException if `child` is provided but is not a child of `parent`. + * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity + * @see https://dom.spec.whatwg.org/#concept-node-replace + */ +function assertPreReplacementValidityInDocument(parent, node, child) { + var parentChildNodes = parent.childNodes || []; + var nodeChildNodes = node.childNodes || []; + + // DocumentFragment + if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + var nodeChildElements = nodeChildNodes.filter(isElementNode); + // If `node` has more than one element child or has a Text node child. + if (nodeChildElements.length > 1 || find(nodeChildNodes, isTextNode)) { + throw new DOMException(HIERARCHY_REQUEST_ERR, 'More than one element or text in fragment'); + } + // Otherwise, if `node` has one element child and either `parent` has an element child that is not `child` or a doctype is following `child`. + if (nodeChildElements.length === 1 && !isElementReplacementPossible(parent, child)) { + throw new DOMException(HIERARCHY_REQUEST_ERR, 'Element in fragment can not be inserted before doctype'); + } + } + // Element + if (isElementNode(node)) { + // `parent` has an element child that is not `child` or a doctype is following `child`. + if (!isElementReplacementPossible(parent, child)) { + throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one element can be added and only after doctype'); + } + } + // DocumentType + if (isDocTypeNode(node)) { + function hasDoctypeChildThatIsNotChild(node) { + return isDocTypeNode(node) && node !== child; + } + + // `parent` has a doctype child that is not `child`, + if (find(parentChildNodes, hasDoctypeChildThatIsNotChild)) { + throw new DOMException(HIERARCHY_REQUEST_ERR, 'Only one doctype is allowed'); + } + var parentElementChild = find(parentChildNodes, isElementNode); + // or an element is preceding `child`. + if (child && parentChildNodes.indexOf(parentElementChild) < parentChildNodes.indexOf(child)) { + throw new DOMException(HIERARCHY_REQUEST_ERR, 'Doctype can only be inserted before an element'); + } + } +} + +/** + * @private + * @param {Node} parent the parent node to insert `node` into + * @param {Node} node the node to insert + * @param {Node=} child the node that should become the `nextSibling` of `node` + * @returns {Node} + * @throws DOMException for several node combinations that would create a DOM that is not well-formed. + * @throws DOMException if `child` is provided but is not a child of `parent`. + * @see https://dom.spec.whatwg.org/#concept-node-ensure-pre-insertion-validity + */ +function _insertBefore(parent, node, child, _inDocumentAssertion) { + // To ensure pre-insertion validity of a node into a parent before a child, run these steps: + assertPreInsertionValidity1to5(parent, node, child); + + // If parent is a document, and any of the statements below, switched on the interface node implements, + // are true, then throw a "HierarchyRequestError" DOMException. + if (parent.nodeType === Node.DOCUMENT_NODE) { + (_inDocumentAssertion || assertPreInsertionValidityInDocument)(parent, node, child); + } var cp = node.parentNode; if (cp) { @@ -1001,6 +1137,17 @@ Document.prototype = { } return _removeChild(this, oldChild); }, + replaceChild: function (newChild, oldChild) { + //raises + _insertBefore(this, newChild, oldChild, assertPreReplacementValidityInDocument); + newChild.ownerDocument = this; + if (oldChild) { + this.removeChild(oldChild); + } + if (isElementNode(newChild)) { + this.documentElement = newChild; + } + }, // Introduced in DOM Level 2: importNode: function (importedNode, deep) { return importNode(this, importedNode, deep); diff --git a/test/dom/document.test.js b/test/dom/document.test.js index 6bd9db09a..ea4c93a24 100644 --- a/test/dom/document.test.js +++ b/test/dom/document.test.js @@ -230,4 +230,33 @@ describe('Document.prototype', () => { expect(root.parentNode).toBeNull(); }); }); + describe('replaceChild', () => { + it('should remove the only element and add the new one', () => { + const doc = new DOMImplementation().createDocument('', 'xml'); + const initialFirstChild = doc.firstChild; + const replacement = doc.createElement('replaced'); + + doc.replaceChild(replacement, doc.firstChild); + + expect(doc.childNodes).toHaveLength(1); + expect(initialFirstChild.parentNode).toBeNull(); + expect(doc.documentElement.name).toBe(replacement.name); + }); + }); + describe('removeChild', () => { + it('should remove all connections to node', () => { + const doc = new DOMImplementation().createDocument('', 'xml'); + doc.insertBefore(doc.createComment('just a comment'), doc.firstChild); + expect(doc.childNodes).toHaveLength(2); + const initialElement = doc.firstChild; + + doc.removeChild(initialElement); + + // expect(doc.documentElement).toBeNull(); + expect(initialElement.parentNode).toBeNull(); + expect(initialElement.nextSibling).toBeNull(); + // expect(initialElement.previousSibling).toBeNull(); + expect(doc.childNodes).toHaveLength(1); + }); + }); });