From 01cb5e0d8dd497c0a5f24afb7d00753e84b408e4 Mon Sep 17 00:00:00 2001 From: Carson Gross Date: Wed, 17 Apr 2024 05:25:28 -0600 Subject: [PATCH] support hx-on in shadowroot --- src/htmx.js | 26 ++++-- test/attributes/hx-on.js | 193 --------------------------------------- test/core/shadowdom.js | 16 ++++ 3 files changed, 34 insertions(+), 201 deletions(-) delete mode 100644 test/attributes/hx-on.js diff --git a/src/htmx.js b/src/htmx.js index a24926aa..6d55eec8 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -2602,19 +2602,29 @@ var htmx = (function() { * @param {Node} elt * @returns {Element[]} */ + const HX_ON_QUERY = new XPathEvaluator() + .createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' + + ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]'); + + function processHXOnRoot(elt, elements) { + if (shouldProcessHxOn(elt)) { + elements.push(asElement(elt)) + } + const iter = HX_ON_QUERY.evaluate(elt) + let node = null; + while (node = iter.iterateNext()) elements.push(asElement(node)) + } + function findHxOnWildcardElements(elt) { let node = null /** @type {Element[]} */ const elements = [] - - if (!(elt instanceof ShadowRoot)) { - if (shouldProcessHxOn(elt)) { - elements.push(asElement(elt)) + if (elt instanceof DocumentFragment) { + for (const child of elt.childNodes) { + processHXOnRoot(child, elements); } - - const iter = document.evaluate('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' + - ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', elt) - while (node = iter.iterateNext()) elements.push(asElement(node)) + } else { + processHXOnRoot(elt, elements); } return elements } diff --git a/test/attributes/hx-on.js b/test/attributes/hx-on.js deleted file mode 100644 index fe1a814a..00000000 --- a/test/attributes/hx-on.js +++ /dev/null @@ -1,193 +0,0 @@ -describe('hx-on attribute', function() { - beforeEach(function() { - this.server = makeServer() - clearWorkArea() - }) - afterEach(function() { - this.server.restore() - clearWorkArea() - }) - - it('can handle basic events w/ no other attributes', function() { - var btn = make("") - btn.click() - window.foo.should.equal(true) - delete window.foo - }) - - it('can modify a parameter via htmx:configRequest', function() { - this.server.respondWith('POST', '/test', function(xhr) { - var params = parseParams(xhr.requestBody) - xhr.respond(200, {}, params.foo) - }) - var btn = make("") - btn.click() - this.server.respond() - btn.innerText.should.equal('bar') - }) - - it('can cancel an event via preventDefault for htmx:configRequest', function() { - this.server.respondWith('POST', '/test', function(xhr) { - xhr.respond(200, {}, '') - }) - var btn = make("") - btn.click() - this.server.respond() - btn.innerText.should.equal('Foo') - }) - - it('can respond to kebab-case events', function() { - this.server.respondWith('POST', '/test', function(xhr) { - var params = parseParams(xhr.requestBody) - xhr.respond(200, {}, params.foo) - }) - var btn = make("") - btn.click() - this.server.respond() - btn.innerText.should.equal('bar') - }) - - it('has the this symbol set to the element', function() { - this.server.respondWith('POST', '/test', function(xhr) { - xhr.respond(200, {}, 'foo') - }) - var btn = make("") - btn.click() - this.server.respond() - btn.innerText.should.equal('foo') - btn.should.equal(window.elt) - delete window.elt - }) - - it('can handle multi-line JSON', function() { - this.server.respondWith('POST', '/test', function(xhr) { - xhr.respond(200, {}, 'foo') - }) - var btn = make("") - btn.click() - this.server.respond() - btn.innerText.should.equal('foo') - var obj = { foo: true, bar: false } - obj.should.deep.equal(window.elt) - delete window.elt - }) - - it('can handle multiple event handlers in the presence of multi-line JSON', function() { - this.server.respondWith('POST', '/test', function(xhr) { - xhr.respond(200, {}, 'foo') - }) - var btn = make("") - btn.click() - this.server.respond() - btn.innerText.should.equal('foo') - - var obj = { foo: true, bar: false } - obj.should.deep.equal(window.elt) - delete window.elt - - window.foo.should.equal(true) - delete window.foo - }) - - it('de-initializes hx-on content properly', function() { - window.tempCount = 0 - this.server.respondWith('POST', '/test', function(xhr) { - xhr.respond(200, {}, "") - }) - var div = make("
Foo
") - - // get response - div.click() - this.server.respond() - - // click button - byId('foo').click() - window.tempCount.should.equal(1) - - // get second response - div.click() - this.server.respond() - - // click button again - byId('foo').click() - window.tempCount.should.equal(2) - - delete window.tempCount - }) - - it('is not evaluated when allowEval is false', function() { - var calledEvent = false - var handler = htmx.on('htmx:evalDisallowedError', function() { - calledEvent = true - }) - htmx.config.allowEval = false - try { - var btn = make("") - btn.click() - should.not.exist(window.foo) - } finally { - htmx.config.allowEval = true - htmx.off('htmx:evalDisallowedError', handler) - delete window.foo - } - calledEvent.should.equal(true) - }) - - it('can handle event types with dots', function() { - var btn = make("") - // IE11 doesn't support `new CustomEvent()` so call htmx' internal utility function - btn.dispatchEvent(htmx._('makeEvent')('my.custom.event')) - window.foo.should.equal(true) - delete window.foo - }) - - it('can handle being swapped using innerHTML', function() { - this.server.respondWith('GET', '/test', function(xhr) { - xhr.respond(200, {}, '') - }) - - make( - '
' + - '' + - '
' + - '
' - ) - - var fooBtn = byId('foo') - fooBtn.click() - window.foo.should.equal(true) - - var swapBtn = byId('swap') - swapBtn.click() - this.server.respond() - - var barBtn = byId('bar') - barBtn.click() - window.bar.should.equal(true) - - delete window.foo - delete window.bar - }) - - it('cleans up all handlers when the DOM updates', function() { - // setup - window.foo = 0 - window.bar = 0 - var div = make("
Foo
") - make('
Another Div
') // sole purpose is to update the DOM - - // check there is just one handler against each event - htmx.trigger(div, 'increment-foo') - htmx.trigger(div, 'increment-bar') - window.foo.should.equal(1) - window.bar.should.equal(1) - - // teardown - delete window.foo - delete window.bar - }) -}) diff --git a/test/core/shadowdom.js b/test/core/shadowdom.js index abe53ccc..dc8a8e98 100644 --- a/test/core/shadowdom.js +++ b/test/core/shadowdom.js @@ -1298,4 +1298,20 @@ describe('Core htmx Shadow DOM Tests', function() { this.server.respond() values.should.deep.equal({ name: '', outside: '' }) }) + + it('can handle basic events w/ no other attributes', function() { + var btn = make("") + btn.click() + window.foo.should.equal(true) + delete window.foo + }) + + it('can handle basic events w/ no other attributes in child', function() { + var div = make("
") + var btn = div.querySelector("#b1") + btn.click() + window.foo.should.equal(true) + delete window.foo + }) + })