diff --git a/src/dom/element.ts b/src/dom/element.ts index 8a14950..1edf082 100644 --- a/src/dom/element.ts +++ b/src/dom/element.ts @@ -591,7 +591,41 @@ export class Element extends Node { } set outerHTML(html: string) { - // TODO: Someday... + if (this.parentNode) { + const { parentElement, parentNode } = this; + let contextLocalName = parentElement?.localName; + + switch (parentNode.nodeType) { + case NodeType.DOCUMENT_NODE: { + throw new DOMException( + "Modifications are not allowed for this document", + ); + } + + // setting outerHTML, step 4. Document Fragment + // ref: https://w3c.github.io/DOM-Parsing/#dom-element-outerhtml + case NodeType.DOCUMENT_FRAGMENT_NODE: { + contextLocalName = "body"; + // fall-through + } + + default: { + const { childNodes: newChildNodes } = + fragmentNodesFromString(html, contextLocalName!).childNodes[0]; + const mutator = parentNode._getChildNodesMutator(); + const insertionIndex = mutator.indexOf(this); + + for (let i = newChildNodes.length - 1; i >= 0; i--) { + const child = newChildNodes[i]; + mutator.splice(insertionIndex, 0, child); + child._setParent(parentNode); + child._setOwnerDocument(parentNode.ownerDocument); + } + + this.remove(); + } + } + } } get innerHTML(): string { diff --git a/test/units/Element-outerHTML.ts b/test/units/Element-outerHTML.ts index 543e699..428c9fc 100644 --- a/test/units/Element-outerHTML.ts +++ b/test/units/Element-outerHTML.ts @@ -1,5 +1,8 @@ -import { DOMParser } from "../../deno-dom-wasm.ts"; -import { assertStrictEquals as assertEquals } from "https://deno.land/std@0.85.0/testing/asserts.ts"; +import { DocumentFragment, DOMParser } from "../../deno-dom-wasm.ts"; +import { + assertStrictEquals as assertEquals, + assertThrows, +} from "https://deno.land/std@0.85.0/testing/asserts.ts"; // TODO: More comprehensive tests @@ -69,3 +72,107 @@ Deno.test("Element.outerHTML won't overflow the stack for deeply nested HTML", ( const htmlElement = doc.documentElement!; assertEquals(htmlElement.outerHTML.length > 0, true); }); + +Deno.test("Element.outerHTML can be set to replace element", () => { + const doc = new DOMParser().parseFromString( + ` +
+
1st
second
+
hello
+ `, + "text/html", + )!; + const parent = doc.querySelector(".parent")!; + const child = doc.querySelector(".child")!; + const otherParent = doc.querySelector(".otherparent")!; + const otherChild = doc.querySelector(".otherchild")!; + const tbody = doc.querySelector("tbody")!; + const tr = tbody.children[0]; + + const newHTML = + `foo fibtext nodesbar`; + const serializedNewHTML = newHTML.replace("qux", '"qux"'); + child.outerHTML = newHTML; + + assertEquals(child.parentNode, null); + assertEquals( + Array.from(parent.childNodes).find((node) => node === child), + undefined, + ); + assertEquals(parent.children.length, 2); + assertEquals(parent.childNodes.length, 4); + assertEquals(parent.innerHTML, serializedNewHTML); + + const newChild = parent.children[0]; + newChild.outerHTML = `goodbye`; + assertEquals(newChild.parentNode, null); + assertEquals( + Array.from(parent.childNodes).find((node) => node === newChild), + undefined, + ); + assertEquals(parent.children.length, 1); + assertEquals(parent.childNodes.length, 4); + assertEquals( + parent.innerHTML, + "goodbye" + serializedNewHTML.slice(serializedNewHTML.indexOf("text")), + ); + + assertEquals(tbody.innerHTML, `hello`); + tr.outerHTML = `goodbye`; + + assertEquals(tr.parentNode, null); + assertEquals( + Array.from(tbody.childNodes).find((node) => node === tr), + undefined, + ); + assertEquals(tbody.children.length, 1); + assertEquals(tbody.childNodes.length, 1); + assertEquals(tbody.innerHTML, `goodbye`); + + otherChild.outerHTML = ``; + + assertEquals(otherChild.parentNode, null); + assertEquals(otherChild.parentElement, null); + assertEquals( + Array.from(otherParent.childNodes).find((node) => node === otherChild), + undefined, + ); + assertEquals(otherParent.children.length, 2); + assertEquals(otherParent.childNodes.length, 4); + assertEquals( + otherParent.outerHTML, + `
1st
`, + ); + + const solitaryDiv = doc.createElement("div"); + solitaryDiv.outerHTML = `no-op`; + + assertEquals(solitaryDiv.parentNode, null); + assertEquals(solitaryDiv.outerHTML, `
`); + + const frag = doc.createDocumentFragment(); + const fragChild = doc.createElement("div"); + frag.appendChild(fragChild); + assertEquals(fragChild.parentNode, frag); + assertEquals(frag.childNodes[0].nodeName, "DIV"); + assertEquals(frag.childNodes.length, 1); + + fragChild.outerHTML = ` + + + only text nodes allowed + + `.replace(/\s{2,}/g, ""); + + assertEquals(fragChild.parentNode, null); + assertEquals(frag.children.length, 2); + assertEquals(frag.childNodes.length, 4); + assertEquals( + Array.from(frag.childNodes).map((node) => node.nodeName).join("-"), + "ASIDE-#comment-#text-BUTTON", + ); + + assertThrows(() => { + doc.documentElement!.outerHTML = "
not new document element
"; + }); +});