Repository: capricorn86/happy-dom
Pull request #228 (2021-03-12 14:18:49)

Task/221 implement insert adjacent functions
Hi!

I've finished with the changes related to the insertAdjacent functionalities and the missing implementation if insertBefore's null parameter handling.
If there's anything i should change, just tell me.
As for the solution, i basically went with a polyfill like solution, because it looked just fine to add this new functionality without write huge chunks of code. That's why i had to fix insertBefore, it depends on that missing behaviour.


Patch:
diff --git a/packages/happy-dom/src/nodes/element/Element.ts b/packages/happy-dom/src/nodes/element/Element.ts
index 0a8b382b..8761fbcf 100644
--- a/packages/happy-dom/src/nodes/element/Element.ts
+++ b/packages/happy-dom/src/nodes/element/Element.ts
@@ -22,6 +22,8 @@ import IDocument from '../document/IDocument';
 import IHTMLCollection from './IHTMLCollection';
 import INodeList from '../node/INodeList';
 import HTMLCollectionFactory from './HTMLCollectionFactory';
+import { TInsertAdjacentPositions } from './IElement';
+import IText from '../text/IText';
 
 /**
  * Element.
@@ -410,6 +412,60 @@ export default class Element extends Node implements IElement {
 		ParentNodeUtility.replaceChildren(this, ...nodes);
 	}
 
+	/**
+	 * Inserts a node to the given position.
+	 *
+	 * @param position Position to insert element.
+	 * @param element Node to insert.
+	 * @return Inserted node or null if couldn't insert.
+	 */
+	public insertAdjacentElement(position: TInsertAdjacentPositions, element: INode): INode | null {
+		if (position === 'beforebegin') {
+			if (!this.parentElement) {
+				return null;
+			}
+
+			this.parentElement.insertBefore(element, this);
+		} else if (position === 'afterbegin') {
+			this.insertBefore(element, this.firstChild);
+		} else if (position === 'beforeend') {
+			this.appendChild(element);
+		} else if (position === 'afterend') {
+			if (!this.parentElement) {
+				return null;
+			}
+
+			this.parentElement.insertBefore(element, this.nextSibling);
+		}
+
+		return element;
+	}
+
+	/**
+	 * Inserts an HTML string to the given position.
+	 *
+	 * @param position Position to insert text.
+	 * @param text HTML string to insert.
+	 * @return Inserted node or null if couldn't insert.
+	 */
+	public insertAdjacentHTML(position: TInsertAdjacentPositions, text: string): void {
+		for (const node of XMLParser.parse(this.ownerDocument, text).childNodes.slice()) {
+			this.insertAdjacentElement(position, node);
+		}
+	}
+
+	/**
+	 * Inserts text to the given position.
+	 *
+	 * @param position Position to insert text.
+	 * @param text String to insert.
+	 * @return Inserted node or null if couldn't insert.
+	 */
+	public insertAdjacentText(position: TInsertAdjacentPositions, text: string): void {
+		const textNode = <IText>this.ownerDocument.createTextNode(text);
+		this.insertAdjacentElement(position, textNode);
+	}
+
 	/**
 	 * Sets an attribute.
 	 *
diff --git a/packages/happy-dom/src/nodes/element/IElement.ts b/packages/happy-dom/src/nodes/element/IElement.ts
index b2da9daa..c7bc679b 100644
--- a/packages/happy-dom/src/nodes/element/IElement.ts
+++ b/packages/happy-dom/src/nodes/element/IElement.ts
@@ -7,6 +7,8 @@ import IChildNode from '../child-node/IChildNode';
 import IParentNode from '../parent-node/IParentNode';
 import INonDocumentTypeChildNode from '../child-node/INonDocumentTypeChildNode';
 
+export type TInsertAdjacentPositions = 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend';
+
 /**
  * Element.
  */
@@ -180,4 +182,31 @@ export default interface IElement extends IChildNode, INonDocumentTypeChildNode,
 	 * @return Cloned node.
 	 */
 	cloneNode(deep: boolean): IElement;
+
+	/**
+	 * Inserts a node to the given position.
+	 *
+	 * @param position Position to insert element.
+	 * @param element Node to insert.
+	 * @return Inserted node or null if couldn't insert.
+	 */
+	insertAdjacentElement(position: TInsertAdjacentPositions, node: INode): INode | null;
+
+	/**
+	 * Inserts an HTML string to the given position.
+	 *
+	 * @param position Position to insert text.
+	 * @param text HTML string to insert.
+	 * @return Inserted node or null if couldn't insert.
+	 */
+	insertAdjacentHTML(position: TInsertAdjacentPositions, text: string): void;
+
+	/**
+	 * Inserts text to the given position.
+	 *
+	 * @param position Position to insert text.
+	 * @param text String to insert.
+	 * @return Inserted node or null if couldn't insert.
+	 */
+	insertAdjacentText(position: TInsertAdjacentPositions, text: string): void;
 }
diff --git a/packages/happy-dom/src/nodes/node/INode.ts b/packages/happy-dom/src/nodes/node/INode.ts
index a37d3a9b..3aa1ec51 100644
--- a/packages/happy-dom/src/nodes/node/INode.ts
+++ b/packages/happy-dom/src/nodes/node/INode.ts
@@ -21,7 +21,7 @@ export default interface INode extends IEventTarget {
 	cloneNode(deep: boolean): INode;
 	appendChild(node: INode): INode;
 	removeChild(node: INode): void;
-	insertBefore(newNode: INode, referenceNode?: INode): INode;
+	insertBefore(newNode: INode, referenceNode?: INode | null): INode;
 	replaceChild(newChild: INode, oldChild: INode): INode;
 	toString(): string;
 }
diff --git a/packages/happy-dom/src/nodes/node/Node.ts b/packages/happy-dom/src/nodes/node/Node.ts
index 908d34ca..040b8ec1 100644
--- a/packages/happy-dom/src/nodes/node/Node.ts
+++ b/packages/happy-dom/src/nodes/node/Node.ts
@@ -307,7 +307,7 @@ export default class Node extends EventTarget implements INode {
 	 * @param [referenceNode] Node to insert before.
 	 * @return Inserted node.
 	 */
-	public insertBefore(newNode: INode, referenceNode?: INode): INode {
+	public insertBefore(newNode: INode, referenceNode?: INode | null): INode {
 		// If the type is DocumentFragment, then the child nodes of if it should be moved instead of the actual node.
 		// See: https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
 		if (newNode.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
@@ -317,6 +317,11 @@ export default class Node extends EventTarget implements INode {
 			return newNode;
 		}
 
+		if (referenceNode === null) {
+			this.appendChild(newNode);
+			return newNode;
+		}
+
 		let index = referenceNode ? this.childNodes.indexOf(referenceNode) : 0;
 		index = index === -1 ? 0 : index;
 


Test patch:
diff --git a/packages/happy-dom/test/nodes/element/Element.test.ts b/packages/happy-dom/test/nodes/element/Element.test.ts
index 2b0b4f9e..e5047bbb 100644
--- a/packages/happy-dom/test/nodes/element/Element.test.ts
+++ b/packages/happy-dom/test/nodes/element/Element.test.ts
@@ -14,6 +14,7 @@ import QuerySelector from '../../../src/query-selector/QuerySelector';
 import ChildNodeUtility from '../../../src/nodes/child-node/ChildNodeUtility';
 import NonDocumentChildNodeUtility from '../../../src/nodes/child-node/NonDocumentChildNodeUtility';
 import HTMLTemplateElement from '../../../src/nodes/html-template-element/HTMLTemplateElement';
+import Node from '../../../src/nodes/node/Node';
 import IHTMLCollection from '../../../src/nodes/element/IHTMLCollection';
 import IElement from '../../../src/nodes/element/IElement';
 import INodeList from '../../../src/nodes/node/INodeList';
@@ -302,6 +303,243 @@ describe('Element', () => {
 		});
 	});
 
+	describe('insertAdjacentElement()', () => {
+		test('Inserts a Node right before the reference element and returns with it.', () => {
+			const parent = document.createElement('div');
+			const newNode = document.createElement('span');
+
+			document.body.appendChild(parent);
+
+			const insertedNode = parent.insertAdjacentElement('beforebegin', newNode);
+
+			expect(insertedNode).toBe(newNode);
+			expect(parent.childNodes).toEqual([]);
+			expect(insertedNode.isConnected).toBe(true);
+			expect(document.body.childNodes[0]).toBe(newNode);
+		});
+
+		test('Returns with null if cannot insert with "beforebegin".', () => {
+			const parent = document.createElement('div');
+			const newNode = document.createElement('span');
+			const insertedNode = parent.insertAdjacentElement('beforebegin', newNode);
+
+			expect(insertedNode).toBe(null);
+			expect(newNode.isConnected).toBe(false);
+		});
+
+		test('Inserts a Node inside the reference element before the first child and returns with it.', () => {
+			const parent = document.createElement('div');
+			const child = document.createElement('span');
+			const newNode = document.createElement('span');
+
+			parent.appendChild(child);
+
+			document.body.appendChild(parent);
+
+			const insertedNode = parent.insertAdjacentElement('afterbegin', newNode);
+
+			expect(insertedNode).toBe(newNode);
+			expect(parent.childNodes[0]).toBe(insertedNode);
+			expect(insertedNode.isConnected).toBe(true);
+		});
+
+		test('Inserts a Node inside the reference element after the last child and returns with it.', () => {
+			const parent = document.createElement('div');
+			const child = document.createElement('span');
+			const newNode = document.createElement('span');
+
+			parent.appendChild(child);
+			document.body.appendChild(parent);
+
+			const insertedNode = parent.insertAdjacentElement('beforeend', newNode);
+
+			expect(insertedNode).toBe(newNode);
+			expect(parent.childNodes[1]).toBe(insertedNode);
+			expect(insertedNode.isConnected).toBe(true);
+		});
+
+		test('Inserts a Node right after the reference element and returns with it.', () => {
+			const parent = document.createElement('div');
+			const newNode = document.createElement('span');
+
+			document.body.appendChild(parent);
+
+			const insertedNode = parent.insertAdjacentElement('afterend', newNode);
+
+			expect(insertedNode).toBe(newNode);
+			expect(parent.childNodes).toEqual([]);
+			expect(insertedNode.isConnected).toBe(true);
+
+			expect(document.body.childNodes[0]).toBe(parent);
+			expect(document.body.childNodes[1]).toBe(insertedNode);
+		});
+
+		test('Inserts a Node right after the reference element and returns with it.', () => {
+			const parent = document.createElement('div');
+			const sibling = document.createElement('div');
+			const newNode = document.createElement('span');
+
+			document.body.appendChild(parent);
+			document.body.appendChild(sibling);
+
+			const insertedNode = parent.insertAdjacentElement('afterend', newNode);
+
+			expect(insertedNode).toBe(newNode);
+			expect(parent.childNodes).toEqual([]);
+			expect(newNode.isConnected).toBe(true);
+
+			expect(document.body.childNodes[0]).toBe(parent);
+			expect(document.body.childNodes[1]).toBe(insertedNode);
+			expect(document.body.childNodes[2]).toBe(sibling);
+		});
+
+		test('Returns with null if cannot insert with "afterend".', () => {
+			const parent = document.createElement('div');
+			const newNode = document.createElement('span');
+			const insertedNode = parent.insertAdjacentElement('afterend', newNode);
+
+			expect(insertedNode).toBe(null);
+			expect(newNode.isConnected).toBe(false);
+		});
+	});
+
+	describe('insertAdjacentHTML()', () => {
+		test('Inserts the given HTML right before the reference element.', () => {
+			const parent = document.createElement('div');
+			const markup = '<span>markup</span>';
+
+			document.body.appendChild(parent);
+			parent.insertAdjacentHTML('beforebegin', markup);
+
+			expect(parent.childNodes).toEqual([]);
+			expect((<IElement>document.body.childNodes[0]).outerHTML).toEqual(markup);
+		});
+
+		test('Inserts the given HTML inside the reference element before the first child.', () => {
+			const parent = document.createElement('div');
+			const child = document.createElement('span');
+			const markup = '<span>markup</span>';
+
+			parent.appendChild(child);
+			document.body.appendChild(parent);
+			parent.insertAdjacentHTML('afterbegin', markup);
+
+			expect((<IElement>parent.childNodes[0]).outerHTML).toEqual(markup);
+			expect(parent.childNodes[1]).toBe(child);
+		});
+
+		test('Inserts the given HTML inside the reference element after the last child.', () => {
+			const parent = document.createElement('div');
+			const child = document.createElement('span');
+			const markup = '<span>markup</span>';
+
+			parent.appendChild(child);
+			document.body.appendChild(parent);
+			parent.insertAdjacentHTML('beforeend', markup);
+
+			expect(parent.childNodes[0]).toBe(child);
+			expect((<IElement>parent.childNodes[1]).outerHTML).toEqual(markup);
+		});
+
+		test('Inserts the given HTML right after the reference element.', () => {
+			const parent = document.createElement('div');
+			const markup = '<span>markup</span>';
+
+			document.body.appendChild(parent);
+			parent.insertAdjacentHTML('afterend', markup);
+
+			expect(parent.childNodes).toEqual([]);
+			expect(document.body.childNodes[0]).toBe(parent);
+			expect((<IElement>document.body.childNodes[1]).outerHTML).toEqual(markup);
+		});
+
+		test('Inserts the given HTML right after the reference element if it has a sibling.', () => {
+			const parent = document.createElement('div');
+			const sibling = document.createElement('div');
+			const markup = '<span>markup</span>';
+
+			document.body.appendChild(parent);
+			document.body.appendChild(sibling);
+			parent.insertAdjacentHTML('afterend', markup);
+
+			expect(parent.childNodes).toEqual([]);
+			expect(document.body.childNodes[0]).toBe(parent);
+			expect((<IElement>document.body.childNodes[1]).outerHTML).toEqual(markup);
+			expect(document.body.childNodes[2]).toBe(sibling);
+		});
+	});
+
+	describe('insertAdjacentText()', () => {
+		test('Inserts the given text right before the reference element.', () => {
+			const parent = document.createElement('div');
+			const text = 'lorem';
+
+			document.body.appendChild(parent);
+			parent.insertAdjacentText('beforebegin', text);
+
+			expect(parent.childNodes).toEqual([]);
+			expect(document.body.childNodes[0].nodeType).toBe(Node.TEXT_NODE);
+			expect(document.body.childNodes[0].textContent).toEqual(text);
+		});
+
+		test('Inserts the given text inside the reference element before the first child.', () => {
+			const parent = document.createElement('div');
+			const child = document.createElement('span');
+			const text = 'lorem';
+
+			parent.appendChild(child);
+			document.body.appendChild(parent);
+			parent.insertAdjacentText('afterbegin', text);
+
+			expect(parent.childNodes[0].nodeType).toBe(Node.TEXT_NODE);
+			expect(parent.childNodes[0].textContent).toEqual(text);
+			expect(parent.childNodes[1]).toBe(child);
+		});
+
+		test('Inserts the given text inside the reference element after the last child.', () => {
+			const parent = document.createElement('div');
+			const child = document.createElement('span');
+			const text = 'lorem';
+
+			parent.appendChild(child);
+			document.body.appendChild(parent);
+			parent.insertAdjacentText('beforeend', text);
+
+			expect(parent.childNodes[0]).toBe(child);
+			expect(parent.childNodes[1].nodeType).toBe(Node.TEXT_NODE);
+			expect(parent.childNodes[1].textContent).toEqual(text);
+		});
+
+		test('Inserts the given text right after the reference element.', () => {
+			const parent = document.createElement('div');
+			const text = 'lorem';
+
+			document.body.appendChild(parent);
+			parent.insertAdjacentText('afterend', text);
+
+			expect(parent.childNodes).toEqual([]);
+			expect(document.body.childNodes[0]).toBe(parent);
+			expect(document.body.childNodes[1].nodeType).toBe(Node.TEXT_NODE);
+			expect(document.body.childNodes[1].textContent).toEqual(text);
+		});
+
+		test('Inserts the given text right after the reference element.', () => {
+			const parent = document.createElement('div');
+			const sibling = document.createElement('div');
+			const text = 'lorem';
+
+			document.body.appendChild(parent);
+			document.body.appendChild(sibling);
+			parent.insertAdjacentText('afterend', text);
+
+			expect(parent.childNodes).toEqual([]);
+			expect(document.body.childNodes[0]).toBe(parent);
+			expect(document.body.childNodes[1].nodeType).toBe(Node.TEXT_NODE);
+			expect(document.body.childNodes[1].textContent).toEqual(text);
+			expect(document.body.childNodes[2]).toBe(sibling);
+		});
+	});
+
 	describe('replaceChildren()', () => {
 		test('Replaces the existing children of a ParentNode with a specified new set of children.', () => {
 			const node1 = document.createComment('test1');
diff --git a/packages/happy-dom/test/nodes/node/Node.test.ts b/packages/happy-dom/test/nodes/node/Node.test.ts
index 41f5b21d..98c20a41 100644
--- a/packages/happy-dom/test/nodes/node/Node.test.ts
+++ b/packages/happy-dom/test/nodes/node/Node.test.ts
@@ -332,6 +332,26 @@ describe('Node', () => {
 				'<span></span><div>Template DIV 1</div><span>Template SPAN 1</span><span></span>'
 			);
 		});
+
+		test('Inserts a Node after all children if no reference given.', () => {
+			const child1 = document.createElement('span');
+			const child2 = document.createElement('span');
+			const newNode = document.createElement('span');
+			const parent = document.createElement('div');
+
+			parent.appendChild(child1);
+			parent.appendChild(child2);
+			parent.insertBefore(newNode, null);
+
+			expect(parent.childNodes[0]).toBe(child1);
+			expect(parent.childNodes[1]).toBe(child2);
+			expect(parent.childNodes[2]).toBe(newNode);
+			expect(newNode.isConnected).toBe(false);
+
+			document.body.appendChild(parent);
+
+			expect(newNode.isConnected).toBe(true);
+		});
 	});
 
 	describe('replaceChild()', () => {