mirror of
https://github.com/bigskysoftware/htmx.git
synced 2026-04-19 05:16:13 +00:00
Support multiple extended selectors for hx-include, hx-trigger from, and hx-disabled-elt (#2902)
* Initial suggestion (squashed) Support multiple extended selectors for hx-include Additional test for nested standard selector Add @MichaelWest22 hx-disabled-elt multiple selector test Add hx-trigger `from` test with multiple extended selectors Simplify Include #2915 fix Update htmx.js Split for readability Don't apply global to previous selectors Rewrite loop, restore global recursive call, minimize diff Use break for better readability Co-Authored-By: MichaelWest22 <12867972+MichaelWest22@users.noreply.github.com> * Keep global as a first-position-only keyword * Wrapped selector syntax * Replace substring check by individual chars check * Fix format --------- Co-authored-by: MichaelWest22 <12867972+MichaelWest22@users.noreply.github.com>
This commit is contained in:
97
src/htmx.js
97
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('<form hx-get="/test" hx-disabled-elt="find input[type=\'text\'], find button" hx-swap="none"><input id="i1" type="text" placeholder="Type here..."><button id="b2" type="submit">Send</button></form>')
|
||||
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('<button hx-get="/test" hx-disabled-elt="closest input">Click Me!</button>')
|
||||
var btn2 = make('<button hx-get="/test" hx-disabled-elt="find input">Click Me!</button>')
|
||||
var btn3 = make('<button hx-get="/test" hx-disabled-elt="next input">Click Me!</button>')
|
||||
var btn4 = make('<button hx-get="/test" hx-disabled-elt="previous input">Click Me!</button>')
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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('<input name="i4" value="test2" id="i4"/>' +
|
||||
'<div id="i">' +
|
||||
'<input name="i1" value="test"/>' +
|
||||
'<input name="i2" value="foo"/>' +
|
||||
'<button id="btn" hx-post="/include" hx-include="closest div, next input, #i4"></button>' +
|
||||
'</div>' +
|
||||
'<input name="i3" value="bar"/>')
|
||||
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('<input name="i4" value="test2" id="i4"/>' +
|
||||
'<div id="i">' +
|
||||
'<input name="i1" value="test" id="i1"/>' +
|
||||
'<input name="i2" value="foo"/>' +
|
||||
'<button id="btn" hx-post="/include" hx-include="#i1, next input, #i4"></button>' +
|
||||
'</div>' +
|
||||
'<input name="i3" value="bar"/>')
|
||||
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('<input name="i4" value="test2" id="i4"/>' +
|
||||
'<div id="i">' +
|
||||
'<input name="i1" value="test" id="i1"/>' +
|
||||
'<input name="i2" value="foo"/>' +
|
||||
'<input name="i5" value="test"/>' +
|
||||
'<button id="btn" hx-post="/include" hx-include="next input, #i > :is([name=\'i1\'], [name=\'i2\'])"></button>' +
|
||||
'</div>' +
|
||||
'<input name="i3" value="bar"/>')
|
||||
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('<input name="i4" value="test2" id="i4"/>' +
|
||||
'<div id="i">' +
|
||||
'<input name="i1" value="test" id="i1"/>' +
|
||||
'<input name="i2" value="foo"/>' +
|
||||
'<button id="btn" hx-post="/include" hx-include="next <#nonexistent, input/>, previous <#i5, [name=\'i2\'], #i4/>"></button>' +
|
||||
'</div>' +
|
||||
'<input name="i3" value="bar"/>' +
|
||||
'<input name="i5" value="test"/>')
|
||||
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('<section>' +
|
||||
'<input name="i1" value="foo"/>' +
|
||||
'<div>' +
|
||||
'<input name="i2" value="bar"/>' +
|
||||
'<button id="btn" hx-post="/include" hx-include="closest <section, div/>"></button>' +
|
||||
'</div>' +
|
||||
'</section>')
|
||||
var btn = byId('btn')
|
||||
btn.click()
|
||||
this.server.respond()
|
||||
btn.innerHTML.should.equal('Clicked!')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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('<button id="btn" type="button">Click me</button>' +
|
||||
'<div hx-trigger="click from:(previous button, next a)" hx-target="#a1" hx-get="/test"></div>' +
|
||||
'<a id="a1">Requests: 0</a>')
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user