From 501f8d8b14a45788e4b1adc9d127e10dcd39e412 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Tue, 1 Sep 2026 13:11:35 -0800 Subject: [PATCH 1/3] read ownerDocument off the Node prototype `saveAndRestoreFocus` resolves the document that owns the focus by reading `ctx.target.ownerDocument`. `
` is [LegacyOverrideBuiltIns], so a named control such as `` installs an own property on the form that shadows `Node.prototype.ownerDocument`. When the morph target is such a form, `doc` is an `HTMLInputElement`, `doc.activeElement` is `undefined`, and the function early-returns, so focus and selection restoration silently stop happening. Add an `ownerDocumentOf` helper that reads through the prototype getter, caching the descriptor once at module scope. The getter is also realm-safe: it brand-checks the internal slot rather than the realm. --- src/idiomorph.js | 18 +++++++++++++++++- test/restore-focus.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index bbd972e..a0b6b68 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -1019,7 +1019,7 @@ var Idiomorph = (function () { throw `Do not understand how to morph head style ${headStyle}`; } - const doc = oldNode.ownerDocument; + const doc = ownerDocumentOf(oldNode); return { target: oldNode, @@ -1452,6 +1452,22 @@ var Idiomorph = (function () { }; })(); + /** + * `` is [LegacyOverrideBuiltIns], so a named control such as + * `` installs an own property on the form that + * shadows `Node.prototype.ownerDocument`. Read through the prototype getter + * instead, which is also realm-safe: it brand-checks the internal slot + * rather than the realm the node was created in. + */ + const ownerDocumentGetter = /** @type {() => Document} */ ( + /** @type {PropertyDescriptor} */ ( + Object.getOwnPropertyDescriptor(Node.prototype, "ownerDocument") + ).get + ); + + /** @param {Node} node @returns {Document} */ + const ownerDocumentOf = (node) => ownerDocumentGetter.call(node); + //============================================================================= // This is what ends up becoming the Idiomorph global object //============================================================================= diff --git a/test/restore-focus.js b/test/restore-focus.js index 916af6b..4811a96 100644 --- a/test/restore-focus.js +++ b/test/restore-focus.js @@ -43,6 +43,35 @@ describe("Option to forcibly restore focus after morph", function () { focused.selectionEnd.should.equal(2); }); + it("restores focus and selection state when the target form shadows ownerDocument", function () { + // a is [LegacyOverrideBuiltIns]: a named control installs an own + // property that shadows Node.prototype.ownerDocument + let form = make(`
`); + getWorkArea().append(form); + form.innerHTML = ` +
+ `; + for (const elt of form.querySelectorAll("input")) { + elt.parentElement.moveBefore = undefined; + } + let input = document.getElementById("focused"); + input.focus(); + input.setSelectionRange(1, 2); + + Idiomorph.morph( + form, + ` +
+ `, + { morphStyle: "innerHTML" }, + ); + + let focused = document.getElementById("focused"); + (document.activeElement === focused).should.equal(true); + focused.selectionStart.should.equal(1); + focused.selectionEnd.should.equal(2); + }); + it("restores focus and selection state with outerHTML morphStyle", function () { const div = make(`
From f66723a8f617bb20d3797d767f45daa1db11f7fc Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Sat, 5 Sep 2026 16:04:40 +0100 Subject: [PATCH 2/3] Defer owner document lookup until morphing --- src/idiomorph.js | 17 +++++++++++------ test/core.js | 10 ++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index a0b6b68..a01b499 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -1459,14 +1459,19 @@ var Idiomorph = (function () { * instead, which is also realm-safe: it brand-checks the internal slot * rather than the realm the node was created in. */ - const ownerDocumentGetter = /** @type {() => Document} */ ( - /** @type {PropertyDescriptor} */ ( - Object.getOwnPropertyDescriptor(Node.prototype, "ownerDocument") - ).get - ); + /** @type {(() => Document) | undefined} */ + let ownerDocumentGetter; /** @param {Node} node @returns {Document} */ - const ownerDocumentOf = (node) => ownerDocumentGetter.call(node); + const ownerDocumentOf = (node) => { + // Resolve lazily so importing the module does not require a DOM. + ownerDocumentGetter ??= /** @type {() => Document} */ ( + /** @type {PropertyDescriptor} */ ( + Object.getOwnPropertyDescriptor(Node.prototype, "ownerDocument") + ).get + ); + return ownerDocumentGetter.call(node); + }; //============================================================================= // This is what ends up becoming the Idiomorph global object diff --git a/test/core.js b/test/core.js index d543b5f..8fea55f 100644 --- a/test/core.js +++ b/test/core.js @@ -1,4 +1,14 @@ describe("Core morphing tests", function () { + it("can load before DOM constructors are available", async function () { + const source = await (await fetch("/src/idiomorph.js")).text(); + const load = new Function( + "Node", + "Element", + source + "; return Idiomorph;", + ); + load(undefined, undefined).morph.should.be.a("function"); + }); + setup(); it("morphs outerHTML by default", function () { let initial = make(""); From a7692d985e10408fa79b6a659fa34792e7b6b8a6 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Sat, 5 Sep 2026 17:47:28 +0100 Subject: [PATCH 3/3] Type the owner document getter receiver --- src/idiomorph.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/idiomorph.js b/src/idiomorph.js index a01b499..333a2e3 100644 --- a/src/idiomorph.js +++ b/src/idiomorph.js @@ -1459,13 +1459,13 @@ var Idiomorph = (function () { * instead, which is also realm-safe: it brand-checks the internal slot * rather than the realm the node was created in. */ - /** @type {(() => Document) | undefined} */ + /** @type {((this: Node) => Document) | undefined} */ let ownerDocumentGetter; /** @param {Node} node @returns {Document} */ const ownerDocumentOf = (node) => { // Resolve lazily so importing the module does not require a DOM. - ownerDocumentGetter ??= /** @type {() => Document} */ ( + ownerDocumentGetter ??= /** @type {(this: Node) => Document} */ ( /** @type {PropertyDescriptor} */ ( Object.getOwnPropertyDescriptor(Node.prototype, "ownerDocument") ).get