From 99285cd5c3785fcdf760b0b9983997ce621bad23 Mon Sep 17 00:00:00 2001 From: Jonathan <61032310+workjonathan@users.noreply.github.com> Date: Wed, 2 Oct 2024 20:44:41 -0500 Subject: [PATCH] fix for hx-swab-oob within web components (#2846) * Failing test for oob-swap within web components * hx-swap-oob respects shadow roots * Lint and type fixes * fix jsdoc types for rootNode parameter * Fix for linter issue I was confused about before * oob swaps handle global correctly * swap uses contextElement if available, document if not Previous a commit made swapOptions.contextElement a required field. This could have harmful ramifications for extensions and users, so instead, the old behavior of assuming document as a root will be used if the contextElement is not provided. * rootNode parameter is optional in oobSwap If not provided, it will fall back to using document as rootNode. jsdocs have been updated for oobSwap and findAndSwapElements accordingly. --- src/htmx.js | 18 ++++--- test/attributes/hx-swap-oob.js | 87 ++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/htmx.js b/src/htmx.js index 0bdac100..04faaea0 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1405,9 +1405,11 @@ var htmx = (function() { * @param {string} oobValue * @param {Element} oobElement * @param {HtmxSettleInfo} settleInfo + * @param {Node|Document} [rootNode] * @returns */ - function oobSwap(oobValue, oobElement, settleInfo) { + function oobSwap(oobValue, oobElement, settleInfo, rootNode) { + rootNode = rootNode || getDocument() let selector = '#' + getRawAttribute(oobElement, 'id') /** @type HtmxSwapStyle */ let swapStyle = 'outerHTML' @@ -1422,7 +1424,7 @@ var htmx = (function() { oobElement.removeAttribute('hx-swap-oob') oobElement.removeAttribute('data-hx-swap-oob') - const targets = getDocument().querySelectorAll(selector) + const targets = querySelectorAllExt(rootNode, selector, false) if (targets) { forEach( targets, @@ -1807,14 +1809,15 @@ var htmx = (function() { /** * @param {DocumentFragment} fragment * @param {HtmxSettleInfo} settleInfo + * @param {Node|Document} [rootNode] */ - function findAndSwapOobElements(fragment, settleInfo) { + function findAndSwapOobElements(fragment, settleInfo, rootNode) { var oobElts = findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]') forEach(oobElts, function(oobElement) { if (htmx.config.allowNestedOobSwaps || oobElement.parentElement === null) { const oobValue = getAttributeValue(oobElement, 'hx-swap-oob') if (oobValue != null) { - oobSwap(oobValue, oobElement, settleInfo) + oobSwap(oobValue, oobElement, settleInfo, rootNode) } } else { oobElement.removeAttribute('hx-swap-oob') @@ -1838,6 +1841,7 @@ var htmx = (function() { } target = resolveTarget(target) + const rootNode = swapOptions.contextElement ? getRootNode(swapOptions.contextElement, false) : getDocument() // preserve focus and selection const activeElt = document.activeElement @@ -1876,14 +1880,14 @@ var htmx = (function() { const oobValue = oobSelectValue[1] || 'true' const oobElement = fragment.querySelector('#' + id) if (oobElement) { - oobSwap(oobValue, oobElement, settleInfo) + oobSwap(oobValue, oobElement, settleInfo, rootNode) } } } // oob swaps - findAndSwapOobElements(fragment, settleInfo) + findAndSwapOobElements(fragment, settleInfo, rootNode) forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) { - if (findAndSwapOobElements(template.content, settleInfo)) { + if (findAndSwapOobElements(template.content, settleInfo, rootNode)) { // Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap template.remove() } diff --git a/test/attributes/hx-swap-oob.js b/test/attributes/hx-swap-oob.js index 327d583d..e7f54a45 100644 --- a/test/attributes/hx-swap-oob.js +++ b/test/attributes/hx-swap-oob.js @@ -260,4 +260,91 @@ describe('hx-swap-oob attribute', function() { byId('td1').innerHTML.should.equal('hey') }) } + for (const config of [{ allowNestedOobSwaps: true }, { allowNestedOobSwaps: false }]) { + it('handles oob target in web components with both inside shadow root and config ' + JSON.stringify(config), function() { + this.server.respondWith('GET', '/test', '