Skip to content

Commit 4466697

Browse files
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>
1 parent 3d1a2e5 commit 4466697

File tree

5 files changed

+197
-24
lines changed

5 files changed

+197
-24
lines changed

src/htmx.js

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,31 +1141,55 @@ var htmx = (function() {
11411141
*/
11421142
function querySelectorAllExt(elt, selector, global) {
11431143
elt = resolveTarget(elt)
1144-
if (selector.indexOf('closest ') === 0) {
1145-
return [closest(asElement(elt), normalizeSelector(selector.substr(8)))]
1146-
} else if (selector.indexOf('find ') === 0) {
1147-
return [find(asParentNode(elt), normalizeSelector(selector.substr(5)))]
1148-
} else if (selector === 'next') {
1149-
return [asElement(elt).nextElementSibling]
1150-
} else if (selector.indexOf('next ') === 0) {
1151-
return [scanForwardQuery(elt, normalizeSelector(selector.substr(5)), !!global)]
1152-
} else if (selector === 'previous') {
1153-
return [asElement(elt).previousElementSibling]
1154-
} else if (selector.indexOf('previous ') === 0) {
1155-
return [scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)), !!global)]
1156-
} else if (selector === 'document') {
1157-
return [document]
1158-
} else if (selector === 'window') {
1159-
return [window]
1160-
} else if (selector === 'body') {
1161-
return [document.body]
1162-
} else if (selector === 'root') {
1163-
return [getRootNode(elt, !!global)]
1164-
} else if (selector.indexOf('global ') === 0) {
1165-
return querySelectorAllExt(elt, selector.slice(7), true)
1166-
} else {
1167-
return toArray(asParentNode(getRootNode(elt, !!global)).querySelectorAll(normalizeSelector(selector)))
1144+
1145+
const parts = selector.split(',')
1146+
const result = []
1147+
const unprocessedParts = []
1148+
while (parts.length > 0) {
1149+
const selector = normalizeSelector(parts.shift())
1150+
let item
1151+
if (selector.indexOf('closest ') === 0) {
1152+
item = closest(asElement(elt), normalizeSelector(selector.substr(8)))
1153+
} else if (selector.indexOf('find ') === 0) {
1154+
item = find(asParentNode(elt), normalizeSelector(selector.substr(5)))
1155+
} else if (selector === 'next' || selector === 'nextElementSibling') {
1156+
item = asElement(elt).nextElementSibling
1157+
} else if (selector.indexOf('next ') === 0) {
1158+
item = scanForwardQuery(elt, normalizeSelector(selector.substr(5)), !!global)
1159+
} else if (selector === 'previous' || selector === 'previousElementSibling') {
1160+
item = asElement(elt).previousElementSibling
1161+
} else if (selector.indexOf('previous ') === 0) {
1162+
item = scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)), !!global)
1163+
} else if (selector === 'document') {
1164+
item = document
1165+
} else if (selector === 'window') {
1166+
item = window
1167+
} else if (selector === 'body') {
1168+
item = document.body
1169+
} else if (selector === 'root') {
1170+
item = getRootNode(elt, !!global)
1171+
} else if (selector.indexOf('global ') === 0) {
1172+
// Previous implementation of `global` only supported it at the first position and applied it to the entire selector string.
1173+
// For backward compatibility and to maintain logical consistency, we make it apply to everything that follows.
1174+
parts.unshift(selector.slice(7))
1175+
result.push(...querySelectorAllExt(elt, parts.join(','), true))
1176+
break
1177+
} else {
1178+
unprocessedParts.push(selector)
1179+
}
1180+
1181+
if (item) {
1182+
result.push(item)
1183+
}
1184+
}
1185+
1186+
if (unprocessedParts.length > 0) {
1187+
const standardSelector = unprocessedParts.join(',')
1188+
const rootNode = asParentNode(getRootNode(elt, !!global))
1189+
result.push(...toArray(rootNode.querySelectorAll(standardSelector)))
11681190
}
1191+
1192+
return result
11691193
}
11701194

11711195
/**

test/attributes/hx-disabled-elt.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,43 @@ describe('hx-disabled-elt attribute', function() {
9292
div.innerHTML.should.equal('Loaded!')
9393
btn.hasAttribute('disabled').should.equal(false)
9494
})
95+
96+
it('hx-disabled-elt supports multiple extended selectors', function() {
97+
this.server.respondWith('GET', '/test', 'Clicked!')
98+
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>')
99+
var i1 = byId('i1')
100+
var b2 = byId('b2')
101+
102+
i1.hasAttribute('disabled').should.equal(false)
103+
b2.hasAttribute('disabled').should.equal(false)
104+
105+
b2.click()
106+
i1.hasAttribute('disabled').should.equal(true)
107+
b2.hasAttribute('disabled').should.equal(true)
108+
109+
this.server.respond()
110+
111+
i1.hasAttribute('disabled').should.equal(false)
112+
b2.hasAttribute('disabled').should.equal(false)
113+
})
114+
115+
it('closest/find/next/previous handle nothing to find without exception', function() {
116+
this.server.respondWith('GET', '/test', 'Clicked!')
117+
var btn1 = make('<button hx-get="/test" hx-disabled-elt="closest input">Click Me!</button>')
118+
var btn2 = make('<button hx-get="/test" hx-disabled-elt="find input">Click Me!</button>')
119+
var btn3 = make('<button hx-get="/test" hx-disabled-elt="next input">Click Me!</button>')
120+
var btn4 = make('<button hx-get="/test" hx-disabled-elt="previous input">Click Me!</button>')
121+
btn1.click()
122+
btn1.hasAttribute('disabled').should.equal(false)
123+
this.server.respond()
124+
btn2.click()
125+
btn2.hasAttribute('disabled').should.equal(false)
126+
this.server.respond()
127+
btn3.click()
128+
btn3.hasAttribute('disabled').should.equal(false)
129+
this.server.respond()
130+
btn4.click()
131+
btn4.hasAttribute('disabled').should.equal(false)
132+
this.server.respond()
133+
})
95134
})

test/attributes/hx-include.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,4 +224,72 @@ describe('hx-include attribute', function() {
224224
this.server.respond()
225225
btn.innerHTML.should.equal('Clicked!')
226226
})
227+
228+
it('Multiple extended selectors can be used in hx-include', function() {
229+
this.server.respondWith('POST', '/include', function(xhr) {
230+
var params = getParameters(xhr)
231+
params.i1.should.equal('test')
232+
params.i2.should.equal('foo')
233+
params.i3.should.equal('bar')
234+
params.i4.should.equal('test2')
235+
xhr.respond(200, {}, 'Clicked!')
236+
})
237+
make('<input name="i4" value="test2" id="i4"/>' +
238+
'<div id="i">' +
239+
'<input name="i1" value="test"/>' +
240+
'<input name="i2" value="foo"/>' +
241+
'<button id="btn" hx-post="/include" hx-include="closest div, next input, #i4"></button>' +
242+
'</div>' +
243+
'<input name="i3" value="bar"/>')
244+
var btn = byId('btn')
245+
btn.click()
246+
this.server.respond()
247+
btn.innerHTML.should.equal('Clicked!')
248+
})
249+
250+
it('hx-include processes extended selector in between standard selectors', function() {
251+
this.server.respondWith('POST', '/include', function(xhr) {
252+
var params = getParameters(xhr)
253+
params.i1.should.equal('test')
254+
should.equal(params.i2, undefined)
255+
params.i3.should.equal('bar')
256+
params.i4.should.equal('test2')
257+
xhr.respond(200, {}, 'Clicked!')
258+
})
259+
make('<input name="i4" value="test2" id="i4"/>' +
260+
'<div id="i">' +
261+
'<input name="i1" value="test" id="i1"/>' +
262+
'<input name="i2" value="foo"/>' +
263+
'<button id="btn" hx-post="/include" hx-include="#i1, next input, #i4"></button>' +
264+
'</div>' +
265+
'<input name="i3" value="bar"/>')
266+
var btn = byId('btn')
267+
btn.click()
268+
this.server.respond()
269+
btn.innerHTML.should.equal('Clicked!')
270+
})
271+
272+
it('hx-include processes nested standard selectors correctly', function() {
273+
this.server.respondWith('POST', '/include', function(xhr) {
274+
var params = getParameters(xhr)
275+
params.i1.should.equal('test')
276+
params.i2.should.equal('foo')
277+
params.i3.should.equal('bar')
278+
should.equal(params.i4, undefined)
279+
should.equal(params.i5, undefined)
280+
xhr.respond(200, {}, 'Clicked!')
281+
})
282+
make('<input name="i4" value="test2" id="i4"/>' +
283+
'<div id="i">' +
284+
'<input name="i1" value="test" id="i1"/>' +
285+
'<input name="i2" value="foo"/>' +
286+
'<input name="i5" value="test"/>' +
287+
'<button id="btn" hx-post="/include" hx-include="next input, #i > :is([name=\'i1\'], [name=\'i2\'])"></button>' +
288+
'</div>' +
289+
'<input name="i3" value="bar"/>')
290+
var btn = byId('btn')
291+
btn.click()
292+
this.server.respond()
293+
btn.innerHTML.should.equal('Clicked!')
294+
})
227295
})

test/attributes/hx-trigger.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,26 @@ describe('hx-trigger attribute', function() {
570570
div1.innerHTML.should.equal('Requests: 2')
571571
})
572572

573+
it('from clause works with multiple extended selectors', function() {
574+
var requests = 0
575+
this.server.respondWith('GET', '/test', function(xhr) {
576+
requests++
577+
xhr.respond(200, {}, 'Requests: ' + requests)
578+
})
579+
make('<button id="btn" type="button">Click me</button>' +
580+
'<div hx-trigger="click from:(previous button, next a)" hx-target="#a1" hx-get="/test"></div>' +
581+
'<a id="a1">Requests: 0</a>')
582+
var btn = byId('btn')
583+
var a1 = byId('a1')
584+
a1.innerHTML.should.equal('Requests: 0')
585+
btn.click()
586+
this.server.respond()
587+
a1.innerHTML.should.equal('Requests: 1')
588+
a1.click()
589+
this.server.respond()
590+
a1.innerHTML.should.equal('Requests: 2')
591+
})
592+
573593
it('event listeners can filter on target', function() {
574594
var requests = 0
575595
this.server.respondWith('GET', '/test', function(xhr) {

test/core/shadowdom.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,4 +1313,26 @@ describe('Core htmx Shadow DOM Tests', function() {
13131313
window.foo.should.equal(true)
13141314
delete window.foo
13151315
})
1316+
1317+
it('global selector modifier does not apply to previous selectors', function() {
1318+
this.server.respondWith('POST', '/include', function(xhr) {
1319+
var params = getParameters(xhr)
1320+
params.i1.should.equal('test')
1321+
should.equal(params.i2, undefined)
1322+
params.i3.should.equal('bar')
1323+
params.i4.should.equal('test2')
1324+
xhr.respond(200, {}, 'Clicked!')
1325+
})
1326+
make('<div>' +
1327+
'<input id="i1" name="i1" value="test"/>' +
1328+
'<input name="i2" value="foo"/>' +
1329+
'<button id="btn" hx-post="/include" hx-include="#i1, next input, global #i4"></button>' +
1330+
'</div>' +
1331+
'<input name="i3" value="bar"/>')
1332+
getWorkArea().innerHTML += '<input name="i4" value="test2" id="i4"/>'
1333+
var btn = byId('btn')
1334+
btn.click()
1335+
this.server.respond()
1336+
btn.innerHTML.should.equal('Clicked!')
1337+
})
13161338
})

0 commit comments

Comments
 (0)