diff --git a/src/htmx.js b/src/htmx.js index d3411e2c4..eaa4d6c66 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -1126,34 +1126,77 @@ var htmx = (function() { * @returns {(Node|Window)[]} */ function querySelectorAllExt(elt, selector, global) { - elt = resolveTarget(elt) - if (selector.indexOf('closest ') === 0) { - return [closest(asElement(elt), normalizeSelector(selector.slice(8)))] - } else if (selector.indexOf('find ') === 0) { - return [find(asParentNode(elt), normalizeSelector(selector.slice(5)))] - } else if (selector === 'next') { - return [asElement(elt).nextElementSibling] - } else if (selector.indexOf('next ') === 0) { - return [scanForwardQuery(elt, normalizeSelector(selector.slice(5)), !!global)] - } else if (selector === 'previous') { - return [asElement(elt).previousElementSibling] - } else if (selector.indexOf('previous ') === 0) { - return [scanBackwardsQuery(elt, normalizeSelector(selector.slice(9)), !!global)] - } else if (selector === 'document') { - return [document] - } else if (selector === 'window') { - return [window] - } else if (selector === 'body') { - return [document.body] - } else if (selector === 'root') { - return [getRootNode(elt, !!global)] - } else if (selector === 'host') { - return [(/** @type ShadowRoot */(elt.getRootNode())).host] - } else if (selector.indexOf('global ') === 0) { + if (selector.indexOf('global ') === 0) { return querySelectorAllExt(elt, selector.slice(7), true) - } else { - return toArray(asParentNode(getRootNode(elt, !!global)).querySelectorAll(normalizeSelector(selector))) } + + elt = resolveTarget(elt) + + const parts = [] + { + let chevronsCount = 0 + let offset = 0 + for (let i = 0; i < selector.length; i++) { + const char = selector[i] + if (char === ',' && chevronsCount === 0) { + parts.push(selector.substring(offset, i)) + offset = i + 1 + continue + } + if (char === '<') { + chevronsCount++ + } else if (char === '/' && i < selector.length - 1 && selector[i + 1] === '>') { + chevronsCount-- + } + } + if (offset < selector.length) { + parts.push(selector.substring(offset)) + } + } + + const result = [] + const unprocessedParts = [] + while (parts.length > 0) { + const selector = normalizeSelector(parts.shift()) + let item + if (selector.indexOf('closest ') === 0) { + item = closest(asElement(elt), normalizeSelector(selector.substr(8))) + } else if (selector.indexOf('find ') === 0) { + item = find(asParentNode(elt), normalizeSelector(selector.substr(5))) + } else if (selector === 'next' || selector === 'nextElementSibling') { + item = asElement(elt).nextElementSibling + } else if (selector.indexOf('next ') === 0) { + item = scanForwardQuery(elt, normalizeSelector(selector.substr(5)), !!global) + } else if (selector === 'previous' || selector === 'previousElementSibling') { + item = asElement(elt).previousElementSibling + } else if (selector.indexOf('previous ') === 0) { + item = scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)), !!global) + } else if (selector === 'document') { + item = document + } else if (selector === 'window') { + item = window + } else if (selector === 'body') { + item = document.body + } else if (selector === 'root') { + item = getRootNode(elt, !!global) + } else if (selector === 'host') { + item = (/** @type ShadowRoot */(elt.getRootNode())).host + } else { + unprocessedParts.push(selector) + } + + if (item) { + result.push(item) + } + } + + if (unprocessedParts.length > 0) { + const standardSelector = unprocessedParts.join(',') + const rootNode = asParentNode(getRootNode(elt, !!global)) + result.push(...toArray(rootNode.querySelectorAll(standardSelector))) + } + + return result } /** @@ -2327,7 +2370,7 @@ var htmx = (function() { path = getDocument().location.href } if (verb === 'get' && path.includes('?')) { - path = path.replace(/\?[^#]+/, ''); + path = path.replace(/\?[^#]+/, '') } } triggerSpecs.forEach(function(triggerSpec) { diff --git a/test/attributes/hx-boost.js b/test/attributes/hx-boost.js index 8f23c7413..e788dfa0a 100644 --- a/test/attributes/hx-boost.js +++ b/test/attributes/hx-boost.js @@ -157,17 +157,16 @@ describe('hx-boost attribute', function() { }) it('form get with no action properly clears existing parameters on submit', function() { - /// add a foo=bar to the current url - var path = location.href; - if (!path.includes("foo=bar")) { - if (!path.includes("?")) { - path += "?foo=bar"; + var path = location.href + if (!path.includes('foo=bar')) { + if (!path.includes('?')) { + path += '?foo=bar' } else { - path += "&foo=bar"; + path += '&foo=bar' } } - history.replaceState({ htmx: true }, '', path); + history.replaceState({ htmx: true }, '', path) this.server.respondWith('GET', /\/*/, function(xhr) { // foo should not be present because the form is a get with no action @@ -183,17 +182,16 @@ describe('hx-boost attribute', function() { }) it('form get with an empty action properly clears existing parameters on submit', function() { - /// add a foo=bar to the current url - var path = location.href; - if (!path.includes("foo=bar")) { - if (!path.includes("?")) { - path += "?foo=bar"; + var path = location.href + if (!path.includes('foo=bar')) { + if (!path.includes('?')) { + path += '?foo=bar' } else { - path += "&foo=bar"; + path += '&foo=bar' } } - history.replaceState({ htmx: true }, '', path); + history.replaceState({ htmx: true }, '', path) this.server.respondWith('GET', /\/*/, function(xhr) { // foo should not be present because the form is a get with no action diff --git a/test/attributes/hx-disabled-elt.js b/test/attributes/hx-disabled-elt.js index 66d8386bf..abdfa3066 100644 --- a/test/attributes/hx-disabled-elt.js +++ b/test/attributes/hx-disabled-elt.js @@ -92,4 +92,43 @@ describe('hx-disabled-elt attribute', function() { div.innerHTML.should.equal('Loaded!') btn.hasAttribute('disabled').should.equal(false) }) + + it('hx-disabled-elt supports multiple extended selectors', function() { + this.server.respondWith('GET', '/test', 'Clicked!') + var form = make('
') + var i1 = byId('i1') + var b2 = byId('b2') + + i1.hasAttribute('disabled').should.equal(false) + b2.hasAttribute('disabled').should.equal(false) + + b2.click() + i1.hasAttribute('disabled').should.equal(true) + b2.hasAttribute('disabled').should.equal(true) + + this.server.respond() + + i1.hasAttribute('disabled').should.equal(false) + b2.hasAttribute('disabled').should.equal(false) + }) + + it('closest/find/next/previous handle nothing to find without exception', function() { + this.server.respondWith('GET', '/test', 'Clicked!') + var btn1 = make('') + var btn2 = make('') + var btn3 = make('') + var btn4 = make('') + btn1.click() + btn1.hasAttribute('disabled').should.equal(false) + this.server.respond() + btn2.click() + btn2.hasAttribute('disabled').should.equal(false) + this.server.respond() + btn3.click() + btn3.hasAttribute('disabled').should.equal(false) + this.server.respond() + btn4.click() + btn4.hasAttribute('disabled').should.equal(false) + this.server.respond() + }) }) diff --git a/test/attributes/hx-include.js b/test/attributes/hx-include.js index 299810ad0..502e78a1f 100644 --- a/test/attributes/hx-include.js +++ b/test/attributes/hx-include.js @@ -224,4 +224,116 @@ describe('hx-include attribute', function() { this.server.respond() btn.innerHTML.should.equal('Clicked!') }) + + it('Multiple extended selectors can be used in hx-include', function() { + this.server.respondWith('POST', '/include', function(xhr) { + var params = getParameters(xhr) + params.i1.should.equal('test') + params.i2.should.equal('foo') + params.i3.should.equal('bar') + params.i4.should.equal('test2') + xhr.respond(200, {}, 'Clicked!') + }) + make('' + + '
' + + '' + + '' + + '' + + '
' + + '') + var btn = byId('btn') + btn.click() + this.server.respond() + btn.innerHTML.should.equal('Clicked!') + }) + + it('hx-include processes extended selector in between standard selectors', function() { + this.server.respondWith('POST', '/include', function(xhr) { + var params = getParameters(xhr) + params.i1.should.equal('test') + should.equal(params.i2, undefined) + params.i3.should.equal('bar') + params.i4.should.equal('test2') + xhr.respond(200, {}, 'Clicked!') + }) + make('' + + '
' + + '' + + '' + + '' + + '
' + + '') + var btn = byId('btn') + btn.click() + this.server.respond() + btn.innerHTML.should.equal('Clicked!') + }) + + it('hx-include processes nested standard selectors correctly', function() { + this.server.respondWith('POST', '/include', function(xhr) { + var params = getParameters(xhr) + params.i1.should.equal('test') + params.i2.should.equal('foo') + params.i3.should.equal('bar') + should.equal(params.i4, undefined) + should.equal(params.i5, undefined) + xhr.respond(200, {}, 'Clicked!') + }) + make('' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '') + var btn = byId('btn') + btn.click() + this.server.respond() + btn.innerHTML.should.equal('Clicked!') + }) + + it('hx-include processes wrapped next/previous selectors correctly', function() { + this.server.respondWith('POST', '/include', function(xhr) { + var params = getParameters(xhr) + should.equal(params.i1, undefined) + params.i2.should.equal('foo') + params.i3.should.equal('bar') + should.equal(params.i4, undefined) + should.equal(params.i5, undefined) + xhr.respond(200, {}, 'Clicked!') + }) + make('' + + '
' + + '' + + '' + + '' + + '
' + + '' + + '') + var btn = byId('btn') + btn.click() + this.server.respond() + btn.innerHTML.should.equal('Clicked!') + }) + + it('hx-include processes wrapped closest selector correctly', function() { + this.server.respondWith('POST', '/include', function(xhr) { + var params = getParameters(xhr) + should.equal(params.i1, undefined) + params.i2.should.equal('bar') + xhr.respond(200, {}, 'Clicked!') + }) + make('
' + + '' + + '
' + + '' + + '' + + '
' + + '
') + var btn = byId('btn') + btn.click() + this.server.respond() + btn.innerHTML.should.equal('Clicked!') + }) }) diff --git a/test/attributes/hx-trigger.js b/test/attributes/hx-trigger.js index 704e9ec96..b673a8a51 100644 --- a/test/attributes/hx-trigger.js +++ b/test/attributes/hx-trigger.js @@ -657,6 +657,26 @@ describe('hx-trigger attribute', function() { div1.innerHTML.should.equal('Requests: 2') }) + it('from clause works with multiple extended selectors', function() { + var requests = 0 + this.server.respondWith('GET', '/test', function(xhr) { + requests++ + xhr.respond(200, {}, 'Requests: ' + requests) + }) + make('' + + '
' + + 'Requests: 0') + var btn = byId('btn') + var a1 = byId('a1') + a1.innerHTML.should.equal('Requests: 0') + btn.click() + this.server.respond() + a1.innerHTML.should.equal('Requests: 1') + a1.click() + this.server.respond() + a1.innerHTML.should.equal('Requests: 2') + }) + it('event listeners can filter on target', function() { var requests = 0 this.server.respondWith('GET', '/test', function(xhr) {