diff --git a/src/idiomorph.js b/src/idiomorph.js index 76ac984..bd3a69f 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -87,7 +87,7 @@ /** * @callback Morph * - * @param {Element | Document} oldNode + * @param {Node} oldNode * @param {Node | HTMLCollection | Node[] | string | null} newContent * @param {Config} [config] * @returns {Promise | Node[]} @@ -152,27 +152,27 @@ var Idiomorph = (function () { /** * Core idiomorph function for morphing one DOM tree to another * - * @param {Element | Document} oldNode + * @param {Node} oldNode * @param {Node | HTMLCollection | Node[] | string | null} newContent * @param {Config} [config] * @returns {Promise | Node[]} */ function morph(oldNode, newContent, config = {}) { - oldNode = normalizeElement(oldNode); + const oldElt = normalizeElement(oldNode); const newNode = normalizeParent(newContent); - const ctx = createMorphContext(oldNode, newNode, config); + const ctx = createMorphContext(oldElt, newNode, config); return withHeadBlocking( ctx, - oldNode, + oldElt, newNode, /** @param {MorphContext} ctx */ (ctx) => { const morphedNodes = saveAndRestoreFocus(ctx, () => { if (ctx.morphStyle === "innerHTML") { - morphChildren(ctx, oldNode, newNode); - return Array.from(oldNode.childNodes); + morphChildren(ctx, oldElt, newNode); + return Array.from(oldElt.childNodes); } else { - return morphOuterHTML(ctx, oldNode, newNode); + return morphOuterHTML(ctx, oldElt, newNode); } }); ctx.pantry.remove(); @@ -1018,6 +1018,10 @@ var Idiomorph = (function () { if (!["innerHTML", "outerHTML"].includes(morphStyle)) { throw `Do not understand how to morph style ${morphStyle}`; } + // Text and Comment have no ParentNode methods, so they cannot take innerHTML + if (morphStyle === "innerHTML" && !oldNode.append) { + throw `Cannot morph the innerHTML of a ${oldNode.nodeName} node, as it cannot have children`; + } const headStyle = mergedConfig.head.style || "merge"; if (!["merge", "append", "morph", "none"].includes(headStyle)) { @@ -1095,23 +1099,26 @@ var Idiomorph = (function () { } /** - * Returns all elements with a non-empty ID contained within the root element and its + * Returns all elements with a non-empty ID contained within the root node and its * descendants, each paired with its id so that it only has to be read once. * - * @param {Element} root + * @param {Node} root * @returns {IdElement[]} */ function findIdElements(root) { /** @type {IdElement[]} */ let elements = []; - for (const elt of root.querySelectorAll("[id]")) { + // root could be a text or comment node which has no `querySelectorAll`, + // or a document fragment which has no `getAttribute` + const rootElt = /** @type {Partial} */ (root); + for (const elt of rootElt.querySelectorAll?.("[id]") ?? []) { // elt.id is unsafe because of form input shadowing, and `id=""` is not persistable const id = elt.getAttribute("id"); if (id) elements.push({ elt, id }); } - // root could be a document fragment which doesn't have `getAttribute` - const rootId = root.getAttribute?.("id"); - if (rootId) elements.push({ elt: root, id: rootId }); + const rootId = rootElt.getAttribute?.("id"); + if (rootId) + elements.push({ elt: /** @type {Element} */ (root), id: rootId }); return elements; } @@ -1230,14 +1237,15 @@ var Idiomorph = (function () { /** * - * @param {Element | Document} content + * @param {Node} content * @returns {Element} */ function normalizeElement(content) { if (content instanceof Document) { return content.documentElement; } else { - return content; + // a Text or Comment node is not an Element, but morphOuterHTML only ever reads Node members off it + return /** @type {Element} */ (content); } } diff --git a/test/core.js b/test/core.js index 0c3ce41..83f49e1 100644 --- a/test/core.js +++ b/test/core.js @@ -121,6 +121,28 @@ describe("Core morphing tests", function () { ); }); + it("morphs outerHTML properly when oldNode is a text node", function () { + let parent = make("
Foo
"); + let initial = parent.firstChild; + Idiomorph.morph(initial, "", { + morphStyle: "outerHTML", + }); + parent.innerHTML.should.equal(""); + }); + + it("morphs outerHTML properly when oldNode is a comment node", function () { + let parent = make( + "

Before

After

", + ); + let initial = parent.childNodes[1]; + Idiomorph.morph(initial, "", { + morphStyle: "outerHTML", + }); + parent.innerHTML.should.equal( + "

Before

After

", + ); + }); + it("morphs innerHTML as content properly when argument is null", function () { let initial = make("
Foo
"); Idiomorph.morph(initial, null, { morphStyle: "innerHTML" }); @@ -178,6 +200,14 @@ describe("Core morphing tests", function () { }).should.throw("Do not understand how to morph style magic"); }); + it("errors on innerHTML of a node that cannot have children", function () { + (() => { + Idiomorph.morph(make("
Foo
").firstChild, [], { + morphStyle: "innerHTML", + }); + }).should.throw("Cannot morph the innerHTML of a #text node"); + }); + it("errors on bad head style", function () { (() => { Idiomorph.morph(make("

"), [], { head: { style: "magic" } });