diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts index aa04248f0..203b3f249 100644 --- a/packages/happy-dom/src/nodes/element/Element.ts +++ b/packages/happy-dom/src/nodes/element/Element.ts @@ -2,7 +2,6 @@ import Node from '../node/Node'; import ShadowRoot from '../shadow-root/ShadowRoot'; import Attr from '../../attribute/Attr'; import DOMRect from './DOMRect'; -import Range from '../../range/Range'; import DOMTokenList from '../../dom-token-list/DOMTokenList'; import IDOMTokenList from '../../dom-token-list/IDOMTokenList'; import QuerySelector from '../../query-selector/QuerySelector'; diff --git a/packages/happy-dom/src/nodes/text/IText.ts b/packages/happy-dom/src/nodes/text/IText.ts index 01e818560..44979c386 100644 --- a/packages/happy-dom/src/nodes/text/IText.ts +++ b/packages/happy-dom/src/nodes/text/IText.ts @@ -1,6 +1,16 @@ import ICharacterData from '../character-data/ICharacterData'; export default interface IText extends ICharacterData { + /** + * Breaks the Text node into two nodes at the specified offset, keeping both nodes in the tree as siblings. + * + * @see https://dom.spec.whatwg.org/#dom-text-splittext + * @see https://dom.spec.whatwg.org/#dom-text-splittext + * @param offset Offset. + * @returns New text node. + */ + splitText(offset: number): IText; + /** * Clones a node. * diff --git a/packages/happy-dom/src/nodes/text/Text.ts b/packages/happy-dom/src/nodes/text/Text.ts index b64f7622b..d48a992ea 100644 --- a/packages/happy-dom/src/nodes/text/Text.ts +++ b/packages/happy-dom/src/nodes/text/Text.ts @@ -1,6 +1,8 @@ import Node from '../node/Node'; import CharacterData from '../character-data/CharacterData'; import IText from './IText'; +import DOMException from '../../exception/DOMException'; +import DOMExceptionNameEnum from '../../exception/DOMExceptionNameEnum'; /** * Text node. @@ -17,6 +19,37 @@ export default class Text extends CharacterData implements IText { return '#text'; } + /** + * Breaks the Text node into two nodes at the specified offset, keeping both nodes in the tree as siblings. + * + * @see https://dom.spec.whatwg.org/#dom-text-splittext + * @see https://dom.spec.whatwg.org/#dom-text-splittext + * @param offset Offset. + * @returns New text node. + */ + public splitText(offset: number): IText { + const length = this._data.length; + + if (offset > length) { + new DOMException( + 'The index is not in the allowed range.', + DOMExceptionNameEnum.indexSizeError + ); + } + + const count = length - offset; + const newData = this.substringData(offset, count); + const newNode = this.ownerDocument.createTextNode(newData); + + if (this.parentNode !== null) { + this.parentNode.insertBefore(newNode, this.nextSibling); + } + + this.replaceData(offset, count, ''); + + return newNode; + } + /** * Converts to string. * diff --git a/packages/happy-dom/src/range/Range.ts b/packages/happy-dom/src/range/Range.ts index 70866042e..cc7caca40 100644 --- a/packages/happy-dom/src/range/Range.ts +++ b/packages/happy-dom/src/range/Range.ts @@ -32,7 +32,7 @@ export default class Range { public readonly END_TO_START: number = RangeHowEnum.endToStart; public readonly START_TO_END: number = RangeHowEnum.startToEnd; public readonly START_TO_START: number = RangeHowEnum.startToStart; - public readonly this.startOffset: number = 0; + public readonly startOffset: number = 0; public readonly endOffset: number = 0; public readonly startContainer: INode = null; public readonly endContainer: INode = null; @@ -52,50 +52,41 @@ export default class Range { /** * Returns a boolean value indicating whether the range's start and end points are at the same position. * + * @see https://dom.spec.whatwg.org/#dom-range-collapsed * @returns Collapsed. */ public get collapsed(): boolean { - return this.startContainer === this.endContainer && this.this.startOffset === this.endOffset; + return this.startContainer === this.endContainer && this.startOffset === this.endOffset; } /** * Returns the deepest Node that contains the startContainer and endContainer nodes. * + * @see https://dom.spec.whatwg.org/#dom-range-commonancestorcontainer * @returns Node. */ public get commonAncestorContainer(): INode { - if (this.startContainer === this.endContainer) { - return this.startContainer; - } - - const endAncestors = []; - let parent = this.endContainer; - - while (parent) { - endAncestors.push(parent); - parent = parent.parentNode; - } + let container = this.startContainer; - parent = this.startContainer; - - while (parent) { - if (endAncestors.includes(parent)) { - return parent; + while (container) { + if (NodeUtility.isInclusiveAncestor(container, this.endContainer)) { + return container; } - parent = parent.parentNode; + container = container.parentNode; } - return this.endContainer || this.startContainer; + return null; } /** * Returns -1, 0, or 1 depending on whether the referenceNode is before, the same as, or after the Range. * + * @see https://dom.spec.whatwg.org/#dom-range-collapse * @param toStart A boolean value: true collapses the Range to its start, false to its end. If omitted, it defaults to false. */ public collapse(toStart = false): void { - if (toStart && !this.startContainer.contains(this.endContainer)) { - this._setEndContainer(this.startContainer, this.this.startOffset); + if (toStart) { + this._setEndContainer(this.startContainer, this.startOffset); } else { this._setStartContainer(this.endContainer, this.endOffset); } @@ -104,6 +95,10 @@ export default class Range { /** * Compares the boundary points of the Range with those of another range. * + * Based on logic from: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/Range-impl.js + * + * @see https://dom.spec.whatwg.org/#dom-range-compareboundarypoints * @param how How. * @param sourceRange Range. * @returns A number, -1, 0, or 1, indicating whether the corresponding boundary-point of the Range is respectively before, equal to, or after the corresponding boundary-point of sourceRange. @@ -116,7 +111,7 @@ export default class Range { how !== RangeHowEnum.endToStart ) { throw new DOMException( - `The comparison method provided must be one of 'START_TO_START', 'START_TO_END', 'END_TO_END' or 'END_TO_START'.`, + `The comparison method provided must be one of '${RangeHowEnum.startToStart}', '${RangeHowEnum.startToEnd}', '${RangeHowEnum.endToEnd}' or '${RangeHowEnum.endToStart}'.`, DOMExceptionNameEnum.notSupportedError ); } @@ -140,15 +135,15 @@ export default class Range { switch (how) { case RangeHowEnum.startToStart: thisPoint.node = this.startContainer; - thisPoint.offset = this.this.startOffset; + thisPoint.offset = this.startOffset; sourcePoint.node = sourceRange.startContainer; - sourcePoint.offset = sourceRange.this.startOffset; + sourcePoint.offset = sourceRange.startOffset; break; case RangeHowEnum.startToEnd: thisPoint.node = this.endContainer; thisPoint.offset = this.endOffset; sourcePoint.node = sourceRange.startContainer; - sourcePoint.offset = sourceRange.this.startOffset; + sourcePoint.offset = sourceRange.startOffset; break; case RangeHowEnum.endToEnd: thisPoint.node = this.endContainer; @@ -158,7 +153,7 @@ export default class Range { break; case RangeHowEnum.endToStart: thisPoint.node = this.startContainer; - thisPoint.offset = this.this.startOffset; + thisPoint.offset = this.startOffset; sourcePoint.node = sourceRange.endContainer; sourcePoint.offset = sourceRange.endOffset; break; @@ -170,35 +165,30 @@ export default class Range { /** * Returns -1, 0, or 1 depending on whether the referenceNode is before, the same as, or after the Range. * - * @param referenceNode Reference node. + * Based on logic from: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/Range-impl.js + * + * @see https://dom.spec.whatwg.org/#dom-range-comparepoint + * @param node Reference node. * @param offset Offset. * @returns -1,0, or 1. */ - public comparePoint(referenceNode: INode, offset): number { - if (referenceNode.ownerDocument !== this._ownerDocument) { + public comparePoint(node: INode, offset): number { + if (node.ownerDocument !== this._ownerDocument) { throw new DOMException( `The two Ranges are not in the same tree.`, DOMExceptionNameEnum.wrongDocumentError ); } - if (referenceNode.nodeType === NodeTypeEnum.documentTypeNode) { - throw new DOMException( - `DocumentType Node can't be used as boundary point.`, - DOMExceptionNameEnum.invalidNodeTypeError - ); - } - - if (offset > NodeUtility.getNodeLength(referenceNode)) { - throw new DOMException(`'Offset out of bound.`, DOMExceptionNameEnum.indexSizeError); - } + RangeUtility.validateBoundaryPoint({ node, offset }); - const boundaryPoint = { node: referenceNode, offset }; + const boundaryPoint = { node, offset }; if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { node: this.startContainer, - offset: this.this.startOffset + offset: this.startOffset }) === -1 ) { return -1; @@ -217,8 +207,8 @@ export default class Range { /** * Returns a DocumentFragment copying the objects of type Node included in the Range. * - * Based on: - * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/Range-impl.js#L306 + * Based on logic from: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/Range-impl.js * * @see https://dom.spec.whatwg.org/#concept-range-clone * @returns Document fragment. @@ -237,7 +227,7 @@ export default class Range { this.startContainer.nodeType === NodeTypeEnum.commentNode) ) { const clone = (this.startContainer).cloneNode(false); - clone['_data'] = clone.substringData(this.this.startOffset, this.endOffset - this.this.startOffset); + clone['_data'] = clone.substringData(this.startOffset, this.endOffset - this.startOffset); fragment.appendChild(clone); return fragment; } @@ -272,24 +262,19 @@ export default class Range { } const containedChildren = []; - let hasDoctypeChildren = false; for (const node of commonAncestor.childNodes) { if (RangeUtility.isContained(node, this)) { if (node.nodeType === NodeTypeEnum.documentTypeNode) { - hasDoctypeChildren = true; + throw new DOMException( + 'Invalid document type element.', + DOMExceptionNameEnum.hierarchyRequestError + ); } containedChildren.push(node); } } - if (hasDoctypeChildren) { - throw new DOMException( - 'Invalid document type element.', - DOMExceptionNameEnum.hierarchyRequestError - ); - } - if ( firstPartialContainedChild !== null && (firstPartialContainedChild.nodeType === NodeTypeEnum.textNode || @@ -298,8 +283,8 @@ export default class Range { ) { const clone = (this.startContainer).cloneNode(false); clone['_data'] = clone.substringData( - this.this.startOffset, - NodeUtility.getNodeLength(this.startContainer) - this.this.startOffset + this.startOffset, + NodeUtility.getNodeLength(this.startContainer) - this.startOffset ); fragment.appendChild(clone); @@ -308,7 +293,7 @@ export default class Range { fragment.appendChild(clone); const subRange = new Range(); - subRange._setStartContainer(this.startContainer, this.this.startOffset); + subRange._setStartContainer(this.startContainer, this.startOffset); subRange._setEndContainer( firstPartialContainedChild, NodeUtility.getNodeLength(firstPartialContainedChild) @@ -351,15 +336,14 @@ export default class Range { /** * Returns a Range object with boundary points identical to the cloned Range. * + * @see https://dom.spec.whatwg.org/#dom-range-clonerange * @returns Range. */ public cloneRange(): Range { const clone = new Range(); - (clone.startContainer) = this.startContainer; - (clone.this.startOffset) = this.this.startOffset; - (clone.endContainer) = this.endContainer; - (clone.endOffset) = this.endOffset; + clone._setStartContainer(this.startContainer, this.startOffset); + clone._setEndContainer(this.startContainer, this.endOffset); return clone; } @@ -378,6 +362,11 @@ export default class Range { /** * Removes the contents of the Range from the Document. + * + * Based on logic from: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/Range-impl.js + * + * @see https://dom.spec.whatwg.org/#dom-range-deletecontents */ public deleteContents(): void { if (this.collapsed) { @@ -391,8 +380,8 @@ export default class Range { this.startContainer.nodeType === NodeTypeEnum.commentNode) ) { (this.startContainer).replaceData( - this.this.startOffset, - this.endOffset - this.this.startOffset, + this.startOffset, + this.endOffset - this.startOffset, '' ); return; @@ -416,7 +405,7 @@ export default class Range { let newOffset; if (NodeUtility.isInclusiveAncestor(this.startContainer, this.endContainer)) { newNode = this.startContainer; - newOffset = this.this.startOffset; + newOffset = this.startOffset; } else { let referenceNode = this.startContainer; @@ -437,8 +426,8 @@ export default class Range { this.startContainer.nodeType === NodeTypeEnum.commentNode ) { (this.startContainer).replaceData( - this.this.startOffset, - NodeUtility.getNodeLength(this.startContainer) - this.this.startOffset, + this.startOffset, + NodeUtility.getNodeLength(this.startContainer) - this.startOffset, '' ); } @@ -472,6 +461,10 @@ export default class Range { /** * Moves contents of the Range from the document tree into a DocumentFragment. * + * Based on logic from: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/Range-impl.js + * + * @see https://dom.spec.whatwg.org/#dom-range-extractcontents * @returns Document fragment. */ public extractContents(): IDocumentFragment { @@ -488,13 +481,13 @@ export default class Range { this.startContainer.nodeType === NodeTypeEnum.commentNode) ) { const clone = this.startContainer.cloneNode(false); - clone['_data'] = clone.substringData(this.this.startOffset, this.endOffset - this.this.startOffset); + clone['_data'] = clone.substringData(this.startOffset, this.endOffset - this.startOffset); fragment.appendChild(clone); (this.startContainer).replaceData( - this.this.startOffset, - this.endOffset - this.this.startOffset, + this.startOffset, + this.endOffset - this.startOffset, '' ); @@ -531,29 +524,24 @@ export default class Range { } const containedChildren = []; - let hasDoctypeChildren = false; for (const node of commonAncestor.childNodes) { if (RangeUtility.isContained(node, this)) { if (node.nodeType === NodeTypeEnum.documentTypeNode) { - hasDoctypeChildren = true; + throw new DOMException( + 'Invalid document type element.', + DOMExceptionNameEnum.hierarchyRequestError + ); } containedChildren.push(node); } } - if (hasDoctypeChildren) { - throw new DOMException( - 'Invalid document type element.', - DOMExceptionNameEnum.hierarchyRequestError - ); - } - let newNode; let newOffset; if (NodeUtility.isInclusiveAncestor(this.startContainer, this.endContainer)) { newNode = this.startContainer; - newOffset = this.this.startOffset; + newOffset = this.startOffset; } else { let referenceNode = this.startContainer; @@ -576,15 +564,15 @@ export default class Range { ) { const clone = this.startContainer.cloneNode(false); clone['_data'] = clone.substringData( - this.this.startOffset, - NodeUtility.getNodeLength(this.startContainer) - this.this.startOffset + this.startOffset, + NodeUtility.getNodeLength(this.startContainer) - this.startOffset ); fragment.appendChild(clone); (this.startContainer).replaceData( - this.this.startOffset, - NodeUtility.getNodeLength(this.startContainer) - this.this.startOffset, + this.startOffset, + NodeUtility.getNodeLength(this.startContainer) - this.startOffset, '' ); } else if (firstPartialContainedChild !== null) { @@ -592,7 +580,7 @@ export default class Range { fragment.appendChild(clone); const subRange = new Range(); - subRange._setStartContainer(this.startContainer, this.this.startOffset); + subRange._setStartContainer(this.startContainer, this.startOffset); subRange._setEndContainer( firstPartialContainedChild, NodeUtility.getNodeLength(firstPartialContainedChild) @@ -659,6 +647,10 @@ export default class Range { /** * Returns a boolean indicating whether the given point is in the Range. * + * Based on logic from: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/Range-impl.js + * + * @see https://dom.spec.whatwg.org/#dom-range-ispointinrange * @param node Reference node. * @param offset Offset. * @returns "true" if in range. @@ -675,7 +667,7 @@ export default class Range { if ( RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { node: this.startContainer, - offset: this.this.startOffset + offset: this.startOffset }) === -1 || RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { node: this.endContainer, @@ -691,6 +683,10 @@ export default class Range { /** * Inserts a node at the start of the Range. * + * Based on logic from: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/Range-impl.js + * + * @see https://dom.spec.whatwg.org/#concept-range-insert * @param newNode New node. */ public insertNode(newNode: INode): void { @@ -700,212 +696,315 @@ export default class Range { (this.startContainer.nodeType === NodeTypeEnum.textNode && !this.startContainer.parentNode) || newNode === this.startContainer ) { - throw new DOMException('Invalid start node.'); - throw DOMException.create(newNode._globalObject, [ - 'Invalid start node.', - 'HierarchyRequestError' - ]); + throw new DOMException('Invalid start node.', DOMExceptionNameEnum.hierarchyRequestError); } let referenceNode = this.startContainer.nodeType === NodeTypeEnum.textNode ? this.startContainer - : domSymbolTree.childrenToArray(this.startContainer)[this.startOffset] || null; - const parent = !referenceNode ? this.startContainer : domSymbolTree.parent(referenceNode); - - parent._preInsertValidity(newNode, referenceNode); + : this.startContainer.childNodes[this.startOffset] || null; + const parent = !referenceNode ? this.startContainer : referenceNode.parentNode; if (this.startContainer.nodeType === NodeTypeEnum.textNode) { - referenceNode = this.startContainer.splitText(this.startOffset); + referenceNode = (this.startContainer).splitText(this.startOffset); } if (newNode === referenceNode) { - referenceNode = domSymbolTree.nextSibling(referenceNode); + referenceNode = referenceNode.nextSibling; } - const nodeParent = domSymbolTree.parent(newNode); + const nodeParent = newNode.parentNode; if (nodeParent) { nodeParent.removeChild(newNode); } - let newOffset = !referenceNode ? nodeLength(parent) : domSymbolTree.index(referenceNode); - newOffset += newNode.nodeType === NODE_TYPE.DOCUMENT_FRAGMENT_NODE ? nodeLength(newNode) : 1; + let newOffset = !referenceNode + ? NodeUtility.getNodeLength(parent) + : referenceNode.parentNode.childNodes.indexOf(referenceNode); + newOffset += + newNode.nodeType === NodeTypeEnum.documentFragmentNode + ? NodeUtility.getNodeLength(newNode) + : 1; parent.insertBefore(newNode, referenceNode); - if (range.collapsed) { - range._setLiveRangeEnd(parent, newOffset); + if (this.collapsed) { + this._setEndContainer(parent, newOffset); } } /** * Returns a boolean indicating whether the given Node intersects the Range. * - * @param _referenceNode Reference node. + * Based on logic from: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/Range-impl.js + * + * @see https://dom.spec.whatwg.org/#dom-range-intersectsnode + * @param node Reference node. * @returns "true" if it intersects. */ - public intersectsNode(_referenceNode: INode): boolean { - // TODO: Implement - return false; + public intersectsNode(node: INode): boolean { + if (node.ownerDocument !== this._ownerDocument) { + return false; + } + + const parent = node.parentNode; + + if (!parent) { + return true; + } + + const offset = parent.childNodes.indexOf(node); + + return ( + RangeUtility.compareBoundaryPointsPosition( + { node: parent, offset }, + { node: this.endContainer, offset: this.endOffset } + ) === -1 && + RangeUtility.compareBoundaryPointsPosition( + { node: parent, offset: offset + 1 }, + { node: this.startContainer, offset: this.startOffset } + ) === 1 + ); } /** * Sets the Range to contain the Node and its contents. * - * @param referenceNode Reference node. + * @see https://dom.spec.whatwg.org/#concept-range-select + * @param node Reference node. */ - public selectNode(referenceNode: INode): void { - if (!referenceNode.parentNode) { + public selectNode(node: INode): void { + if (!node.parentNode) { throw new DOMException( - `Failed to select node. Reference node is missing a parent node.`, + `The given Node has no parent.`, DOMExceptionNameEnum.invalidNodeTypeError ); } - const index = referenceNode.parentNode.childNodes.indexOf(referenceNode); + const index = node.parentNode.childNodes.indexOf(node); - this._setStartContainer(referenceNode.parentNode, index); - this._setEndContainer(referenceNode.parentNode, index + 1); + this._setStartContainer(node.parentNode, index); + this._setEndContainer(node.parentNode, index + 1); } /** * Sets the Range to contain the contents of a Node. * - * @param referenceNode Reference node. + * @see https://dom.spec.whatwg.org/#dom-range-selectnodecontents + * @param node Reference node. */ - public selectNodeContents(referenceNode: INode): void { - this._setStartContainer(referenceNode, 0); - this._setEndContainer(referenceNode, NodeUtility.getNodeLength(referenceNode)); + public selectNodeContents(node: INode): void { + if (node.nodeType === NodeTypeEnum.documentTypeNode) { + throw new DOMException( + "DocumentType Node can't be used as boundary point.", + DOMExceptionNameEnum.invalidNodeTypeError + ); + } + + this._setStartContainer(node, 0); + this._setEndContainer(node, NodeUtility.getNodeLength(node)); } /** * Sets the end position of a Range to be located at the given offset into the specified node x. * + * @see https://dom.spec.whatwg.org/#dom-range-setend * @param node End node. * @param offset End offset. */ public setEnd(node: INode, offset = 0): void { RangeUtility.validateBoundaryPoint({ node, offset }); + const boundaryPoint = { node, offset }; + if ( node.ownerDocument !== this._ownerDocument || - RangeUtility.compareBoundaryPointsPosition( - { - node, - offset - }, - { - node: this.startContainer, - offset: this.this.startOffset - } - ) === -1 + RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { + node: this.startContainer, + offset: this.startOffset + }) === -1 ) { - this.setStart(node, offset); + this._setStartContainer(node, offset); } + this._setEndContainer(node, offset); } /** * Sets the start position of a Range. * + * @see https://dom.spec.whatwg.org/#dom-range-setstart * @param node Start node. * @param offset Start offset. */ public setStart(node: INode, offset = 0): void { RangeUtility.validateBoundaryPoint({ node, offset }); + const boundaryPoint = { node, offset }; + if ( node.ownerDocument !== this._ownerDocument || - RangeUtility.compareBoundaryPointsPosition( - { - node, - offset - }, - { - node: this.endContainer, - offset: this.endOffset - } - ) === 1 + RangeUtility.compareBoundaryPointsPosition(boundaryPoint, { + node: this.endContainer, + offset: this.endOffset + }) === 1 ) { - this.setEnd(node, offset); + this._setEndContainer(node, offset); } + this._setStartContainer(node, offset); } /** * Sets the end position of a Range relative to another Node. * - * @param referenceNode Reference node. + * @see https://dom.spec.whatwg.org/#dom-range-setendafter + * @param node Reference node. */ - public setEndAfter(referenceNode: INode): void { - const sibling = referenceNode.nextSibling; - if (!sibling) { + public setEndAfter(node: INode): void { + if (!node.parentNode) { throw new DOMException( - 'Failed to set range end. "referenceNode" does not have any nodes after itself.' + 'The given Node has no parent.', + DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setEnd(sibling); + this.setEnd(node.parentNode, node.parentNode.childNodes.indexOf(node) + 1); } /** * Sets the end position of a Range relative to another Node. * - * @param referenceNode Reference node. + * @see https://dom.spec.whatwg.org/#dom-range-setendbefore + * @param node Reference node. */ - public setEndBefore(referenceNode: INode): void { - const sibling = referenceNode.previousSibling; - if (!sibling) { + public setEndBefore(node: INode): void { + if (!node.parentNode) { throw new DOMException( - 'Failed to set range end. "referenceNode" does not have any nodes before itself.' + 'The given Node has no parent.', + DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setEnd(sibling); + this.setEnd(node.parentNode, node.parentNode.childNodes.indexOf(node)); } /** * Sets the start position of a Range relative to a Node. * - * @param referenceNode Reference node. + * @see https://dom.spec.whatwg.org/#dom-range-setstartafter + * @param node Reference node. */ - public setStartAfter(referenceNode: INode): void { - const sibling = referenceNode.nextSibling; - if (!sibling) { + public setStartAfter(node: INode): void { + if (!node.parentNode) { throw new DOMException( - 'Failed to set range start. "referenceNode" does not have any nodes after itself.' + 'The given Node has no parent.', + DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setStart(sibling); + this.setStart(node.parentNode, node.parentNode.childNodes.indexOf(node) + 1); } /** * Sets the start position of a Range relative to another Node. * - * @param referenceNode Reference node. + * @see https://dom.spec.whatwg.org/#dom-range-setstartbefore + * @param node Reference node. */ - public setStartBefore(referenceNode: INode): void { - const sibling = referenceNode.previousSibling; - if (!sibling) { + public setStartBefore(node: INode): void { + if (!node.parentNode) { throw new DOMException( - 'Failed to set range start. "referenceNode" does not have any nodes before itself.' + 'The given Node has no parent.', + DOMExceptionNameEnum.invalidNodeTypeError ); } - this.setStart(sibling); + this.setStart(node.parentNode, node.parentNode.childNodes.indexOf(node)); } /** * Moves content of the Range into a new node, placing the new node at the start of the specified range. * - * @param _newParent New parent. + * Based on logic from: + * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/Range-impl.js + * + * @see https://dom.spec.whatwg.org/#dom-range-surroundcontents + * @param newParent New parent. */ - public surroundContents(_newParent: INode): void { - // TODO: Implement + public surroundContents(newParent: INode): void { + let node = this.commonAncestorContainer; + const endNode = NodeUtility.nextDecendantNode(node); + while (node !== endNode) { + if ( + node.nodeType !== NodeTypeEnum.textNode && + RangeUtility.isPartiallyContained(node, this) + ) { + throw new DOMException( + 'The Range has partially contains a non-Text node.', + DOMExceptionNameEnum.invalidStateError + ); + } + + node = NodeUtility.following(node); + } + + if ( + newParent.nodeType === NodeTypeEnum.documentNode || + newParent.nodeType === NodeTypeEnum.documentTypeNode || + newParent.nodeType === NodeTypeEnum.documentFragmentNode + ) { + throw new DOMException('Invalid element type.', DOMExceptionNameEnum.invalidNodeTypeError); + } + + const fragment = this.extractContents(); + + while (newParent.firstChild) { + newParent.removeChild(newParent.firstChild); + } + + this.insertNode(newParent); + + newParent.appendChild(fragment); + + this.selectNode(newParent); } /** * Returns the text of the Range. + * + * @see https://dom.spec.whatwg.org/#dom-range-stringifier */ public toString(): string { - return ''; + let string = ''; + + if ( + this.startContainer === this.endContainer && + this.startContainer.nodeType === NodeTypeEnum.textNode + ) { + return (this.startContainer).data.slice(this.startOffset, this.endOffset); + } + + if (this.startContainer.nodeType === NodeTypeEnum.textNode) { + string += (this.startContainer).data.slice(this.startOffset); + } + + const endNode = NodeUtility.nextDecendantNode(this.endContainer); + let currentNode = this.startContainer; + + while (currentNode && currentNode !== endNode) { + if ( + currentNode.nodeType === NodeTypeEnum.textNode && + RangeUtility.isContained(currentNode, this) + ) { + string += (currentNode).data; + } + + currentNode = NodeUtility.following(currentNode); + } + + if (this.endContainer.nodeType === NodeTypeEnum.textNode) { + string += (this.endContainer).data.slice(0, this.endOffset); + } + + return string; } /** @@ -918,13 +1017,13 @@ export default class Range { if ( this.startContainer && this._startObserver && - (this.startContainer !== container || this.this.startOffset !== offset) + (this.startContainer !== container || this.startOffset !== offset) ) { (this.startContainer)._unobserve(this._startObserver); } (this.startContainer) = container; - (this.this.startOffset) = offset; + (this.startOffset) = offset; if (offset !== 0) { this._startObserver = this._getMutationListener(container, 'this.startOffset'); diff --git a/packages/happy-dom/src/range/RangeUtility.ts b/packages/happy-dom/src/range/RangeUtility.ts index 413160853..d5b8e2f75 100644 --- a/packages/happy-dom/src/range/RangeUtility.ts +++ b/packages/happy-dom/src/range/RangeUtility.ts @@ -1,6 +1,6 @@ -import DOMException from 'src/exception/DOMException'; -import DOMExceptionNameEnum from 'src/exception/DOMExceptionNameEnum'; -import NodeTypeEnum from 'src/nodes/node/NodeTypeEnum'; +import DOMException from '../exception/DOMException'; +import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum'; +import NodeTypeEnum from '../nodes/node/NodeTypeEnum'; import INode from '../nodes/node/INode'; import NodeUtility from '../nodes/node/NodeUtility'; import Range from './Range'; @@ -17,7 +17,7 @@ export default class RangeUtility { /** * Compares boundary points. * - * Based on: + * Based on logic from: * https://github.com/jsdom/jsdom/blob/master/lib/jsdom/living/range/boundary-point.js * * @see https://dom.spec.whatwg.org/#concept-range-bp-after @@ -50,9 +50,7 @@ export default class RangeUtility { child = child.parentNode; } - const index = child.parentNode.childNodes.indexOf(child); - - if (index < pointA.offset) { + if (child.parentNode.childNodes.indexOf(child) < pointA.offset) { return 1; } } diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts index a80aa0e6d..2779c2e21 100644 --- a/packages/happy-dom/test/nodes/element/Element.test.ts +++ b/packages/happy-dom/test/nodes/element/Element.test.ts @@ -7,7 +7,6 @@ import ShadowRoot from '../../../src/nodes/shadow-root/ShadowRoot'; import IDocument from '../../../src/nodes/document/IDocument'; import Text from '../../../src/nodes/text/Text'; import DOMRect from '../../../src/nodes/element/DOMRect'; -import Range from '../../../src/range/Range'; import NamespaceURI from '../../../src/config/NamespaceURI'; import ParentNodeUtility from '../../../src/nodes/parent-node/ParentNodeUtility'; import QuerySelector from '../../../src/query-selector/QuerySelector'; @@ -177,7 +176,7 @@ describe('Element', () => { describe('set innerHTML()', () => { it('Creates child nodes from provided HTML.', () => { - const root = document.createElement('div'); + const root = document.createDocumentFragment(); const div = document.createElement('div'); const textNode = document.createTextNode('text1'); @@ -1263,13 +1262,6 @@ describe('Element', () => { }); }); - describe('createTextRange()', () => { - it('Returns an instance of Range.', () => { - const range = element.createTextRange(); - expect(range instanceof Range).toBe(true); - }); - }); - describe('cloneNode()', () => { it('Clones the properties of the element when cloned.', () => { const child = document.createElement('div'); diff --git a/packages/happy-dom/test/xml-parser/XMLParser.test.ts b/packages/happy-dom/test/xml-parser/XMLParser.test.ts index 85164e4b8..bf94943af 100644 --- a/packages/happy-dom/test/xml-parser/XMLParser.test.ts +++ b/packages/happy-dom/test/xml-parser/XMLParser.test.ts @@ -8,6 +8,7 @@ import IHTMLTemplateElement from '../../src/nodes/html-template-element/IHTMLTem import XMLParserHTML from './data/XMLParserHTML'; import NamespaceURI from '../../src/config/NamespaceURI'; import DocumentType from '../../src/nodes/document-type/DocumentType'; +import XMLSerializer from '../../src/xml-serializer/XMLSerializer'; const GET_EXPECTED_HTML = (html: string): string => html @@ -98,7 +99,9 @@ describe('XMLParser', () => { it('Parses an entire HTML page.', () => { const root = XMLParser.parse(window.document, XMLParserHTML); - expect(root.innerHTML.replace(/[\s]/gm, '')).toBe(GET_EXPECTED_HTML(XMLParserHTML)); + expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe( + GET_EXPECTED_HTML(XMLParserHTML) + ); }); it('Parses a page with document type set to "HTML 4.01".', () => { @@ -111,7 +114,9 @@ describe('XMLParser', () => { expect(doctype.name).toBe('HTML'); expect(doctype.publicId).toBe('-//W3C//DTD HTML 4.01//EN'); expect(doctype.systemId).toBe('http://www.w3.org/TR/html4/strict.dtd'); - expect(root.innerHTML.replace(/[\s]/gm, '')).toBe(GET_EXPECTED_HTML(pageHTML)); + expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe( + GET_EXPECTED_HTML(pageHTML) + ); }); it('Parses a page with document type set to "MathML 1.01".', () => { @@ -124,7 +129,9 @@ describe('XMLParser', () => { expect(doctype.name).toBe('math'); expect(doctype.publicId).toBe(''); expect(doctype.systemId).toBe('http://www.w3.org/Math/DTD/mathml1/mathml.dtd'); - expect(root.innerHTML.replace(/[\s]/gm, '')).toBe(GET_EXPECTED_HTML(pageHTML)); + expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe( + GET_EXPECTED_HTML(pageHTML) + ); }); it('Handles unclosed tags of unnestable elements (e.g. ,
  • ).', () => { @@ -142,7 +149,7 @@ describe('XMLParser', () => { ` ); - expect(root.innerHTML.replace(/[\s]/gm, '')).toBe( + expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe( `
      @@ -177,7 +184,7 @@ describe('XMLParser', () => { '' ); - expect(root.innerHTML.replace(/[\s]/gm, '')).toBe( + expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe( `
      @@ -289,7 +296,7 @@ describe('XMLParser', () => { length: 4 }); - expect(root.innerHTML.replace(/[\s]/gm, '')).toBe( + expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe( `
      @@ -329,7 +336,7 @@ describe('XMLParser', () => { ` ); - expect(root.innerHTML.replace(/[\s]/gm, '')).toBe( + expect(new XMLSerializer().serializeToString(root).replace(/[\s]/gm, '')).toBe( `