From 8e7ef427e0b40c4c5c269aef7f3cfa4cabebdd82 Mon Sep 17 00:00:00 2001 From: Kory Nunn Date: Tue, 30 Jul 2024 01:26:51 +1000 Subject: [PATCH] Remove dependance on global window and document (#2897) * Remove dependance on global window and document * Use any available document, prioritising parent.ownerDocument * Fix mockDom for DocumentFragment, revert to better ownerDocument implementation * Simplify activeElement usage --- render/render.js | 36 +++++++++++++++++++----------------- test-utils/domMock.js | 3 +++ 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/render/render.js b/render/render.js index 530313809..a95396716 100644 --- a/render/render.js +++ b/render/render.js @@ -5,9 +5,7 @@ var df = require("../render/domFor") var delayedRemoval = df.delayedRemoval var domFor = df.domFor -module.exports = function($window) { - var $doc = $window && $window.document - +module.exports = function() { var nameSpace = { svg: "http://www.w3.org/2000/svg", math: "http://www.w3.org/1998/Math/MathML" @@ -16,6 +14,10 @@ module.exports = function($window) { var currentRedraw var currentRender + function getDocument(dom) { + return dom.ownerDocument; + } + function getNameSpace(vnode) { return vnode.attrs && vnode.attrs.xmlns || nameSpace[vnode.tag] } @@ -40,9 +42,9 @@ module.exports = function($window) { // IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when // inside an iframe. Catch and swallow this error, and heavy-handidly return null. - function activeElement() { + function activeElement(dom) { try { - return $doc.activeElement + return getDocument(dom).activeElement } catch (e) { return null } @@ -71,7 +73,7 @@ module.exports = function($window) { else createComponent(parent, vnode, hooks, ns, nextSibling) } function createText(parent, vnode, nextSibling) { - vnode.dom = $doc.createTextNode(vnode.children) + vnode.dom = getDocument(parent).createTextNode(vnode.children) insertDOM(parent, vnode.dom, nextSibling) } var possibleParents = {caption: "table", thead: "table", tbody: "table", tfoot: "table", tr: "tbody", th: "tr", td: "tr", colgroup: "table", col: "colgroup"} @@ -82,7 +84,7 @@ module.exports = function($window) { // div.innerHTML = "ij" // console.log(div.innerHTML) // --> "ij", no in sight. - var temp = $doc.createElement(possibleParents[match[1]] || "div") + var temp = getDocument(parent).createElement(possibleParents[match[1]] || "div") if (ns === "http://www.w3.org/2000/svg") { temp.innerHTML = "" + vnode.children + "" temp = temp.firstChild @@ -92,7 +94,7 @@ module.exports = function($window) { vnode.dom = temp.firstChild vnode.domSize = temp.childNodes.length // Capture nodes to remove, so we don't confuse them. - var fragment = $doc.createDocumentFragment() + var fragment = getDocument(parent).createDocumentFragment() var child while (child = temp.firstChild) { fragment.appendChild(child) @@ -100,7 +102,7 @@ module.exports = function($window) { insertDOM(parent, fragment, nextSibling) } function createFragment(parent, vnode, hooks, ns, nextSibling) { - var fragment = $doc.createDocumentFragment() + var fragment = getDocument(parent).createDocumentFragment() if (vnode.children != null) { var children = vnode.children createNodes(fragment, children, 0, children.length, hooks, null, ns) @@ -117,8 +119,8 @@ module.exports = function($window) { ns = getNameSpace(vnode) || ns var element = ns ? - is ? $doc.createElementNS(ns, tag, {is: is}) : $doc.createElementNS(ns, tag) : - is ? $doc.createElement(tag, {is: is}) : $doc.createElement(tag) + is ? getDocument(parent).createElementNS(ns, tag, {is: is}) : getDocument(parent).createElementNS(ns, tag) : + is ? getDocument(parent).createElement(tag, {is: is}) : getDocument(parent).createElement(tag) vnode.dom = element if (attrs != null) { @@ -553,7 +555,7 @@ module.exports = function($window) { // don't allocate for the common case target = vnode.dom } else { - target = $doc.createDocumentFragment() + target = getDocument(parent).createDocumentFragment() for (var dom of domFor(vnode)) target.appendChild(dom) } insertDOM(parent, target, nextSibling) @@ -693,7 +695,7 @@ module.exports = function($window) { /* eslint-disable no-implicit-coercion */ //setting input[value] to same value by typing on focused element moves cursor to end in Chrome //setting input[type=file][value] to same value causes an error to be generated if it's non-empty - if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === "" + value && (isFileInput || vnode.dom === activeElement())) return + if ((vnode.tag === "input" || vnode.tag === "textarea") && vnode.dom.value === "" + value && (isFileInput || vnode.dom === activeElement(vnode.dom))) return //setting select[value] to same value while having select open blinks select dropdown in Chrome if (vnode.tag === "select" && old !== null && vnode.dom.value === "" + value) return //setting option[value] to same value while having select open blinks select dropdown in Chrome @@ -722,7 +724,7 @@ module.exports = function($window) { && key !== "title" // creates "null" as title && !(key === "value" && ( vnode.tag === "option" - || vnode.tag === "select" && vnode.dom.selectedIndex === -1 && vnode.dom === activeElement() + || vnode.tag === "select" && vnode.dom.selectedIndex === -1 && vnode.dom === activeElement(vnode.dom) )) && !(vnode.tag === "input" && key === "type") ) { @@ -771,7 +773,7 @@ module.exports = function($window) { } } function isFormAttribute(vnode, attr) { - return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === activeElement() || vnode.tag === "option" && vnode.dom.parentNode === $doc.activeElement + return attr === "value" || attr === "checked" || attr === "selectedIndex" || attr === "selected" && vnode.dom === activeElement(vnode.dom) || vnode.tag === "option" && vnode.dom.parentNode === activeElement(vnode.dom) } function isLifecycleMethod(attr) { return attr === "oninit" || attr === "oncreate" || attr === "onupdate" || attr === "onremove" || attr === "onbeforeremove" || attr === "onbeforeupdate" @@ -923,7 +925,7 @@ module.exports = function($window) { var prevRedraw = currentRedraw var prevDOM = currentDOM var hooks = [] - var active = activeElement() + var active = activeElement(dom) var namespace = dom.namespaceURI currentDOM = dom @@ -936,7 +938,7 @@ module.exports = function($window) { updateNodes(dom, dom.vnodes, vnodes, hooks, null, namespace === "http://www.w3.org/1999/xhtml" ? undefined : namespace) dom.vnodes = vnodes // `document.activeElement` can return null: https://html.spec.whatwg.org/multipage/interaction.html#dom-document-activeelement - if (active != null && activeElement() !== active && typeof active.focus === "function") active.focus() + if (active != null && activeElement(dom) !== active && typeof active.focus === "function") active.focus() for (var i = 0; i < hooks.length; i++) hooks[i]() } finally { currentRedraw = prevRedraw diff --git a/test-utils/domMock.js b/test-utils/domMock.js index 883b2cb1e..5536b49fd 100644 --- a/test-utils/domMock.js +++ b/test-utils/domMock.js @@ -307,6 +307,7 @@ module.exports = function(options) { parentNode: null, childNodes: [], attributes: {}, + ownerDocument: $window.document, contains: function(child) { while (child != null) { if (child === this) return true @@ -717,6 +718,7 @@ module.exports = function(options) { }, createDocumentFragment: function() { return { + ownerDocument: $window.document, nodeType: 11, nodeName: "#document-fragment", appendChild: appendChild, @@ -738,6 +740,7 @@ module.exports = function(options) { get activeElement() {return activeElement}, }, } + $window.document.defaultView = $window $window.document.documentElement = $window.document.createElement("html") appendChild.call($window.document.documentElement, $window.document.createElement("head")) $window.document.body = $window.document.createElement("body")