mirror of
https://github.com/bigskysoftware/htmx.git
synced 2026-04-19 21:36:31 +00:00
Allow CSS selectors with whitespace in hx-trigger (#1913)
* Allow CSS selectors with whitespace in `hx-trigger` Parsing of `hx-trigger` scans for whitespace, so if a CSS selector is used that contains whitespace, e.g. `form input`, a syntax error is raised. A workaround is implemented by allowing such a CSS selector to be wrapped in either curly braces or parentheses. * Add explanation whitespace in CSS selector to docs * Tests for CSS selectors containing whitespace * Use faster RegEx test, remove redundant variable declarations * Added Descendant Combinator support to `root` and `target` modifiers * Add missing semicolon * Tests for descendant combinators in `root` and `target` modifiers * Improve descendant combinator test coverage
This commit is contained in:
committed by
GitHub
parent
6a9a861ad9
commit
7ef95e8963
42
src/htmx.js
42
src/htmx.js
@@ -1146,6 +1146,8 @@ return (function () {
|
||||
var SYMBOL_CONT = /[_$a-zA-Z0-9]/;
|
||||
var STRINGISH_START = ['"', "'", "/"];
|
||||
var NOT_WHITESPACE = /[^\s]/;
|
||||
var COMBINED_SELECTOR_START = /[{(]/;
|
||||
var COMBINED_SELECTOR_END = /[})]/;
|
||||
function tokenizeString(str) {
|
||||
var tokens = [];
|
||||
var position = 0;
|
||||
@@ -1234,6 +1236,18 @@ return (function () {
|
||||
return result;
|
||||
}
|
||||
|
||||
function consumeCSSSelector(tokens) {
|
||||
var result;
|
||||
if (tokens.length > 0 && COMBINED_SELECTOR_START.test(tokens[0])) {
|
||||
tokens.shift();
|
||||
result = consumeUntil(tokens, COMBINED_SELECTOR_END).trim();
|
||||
tokens.shift();
|
||||
} else {
|
||||
result = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var INPUT_SELECTOR = 'input, textarea, select';
|
||||
|
||||
/**
|
||||
@@ -1282,29 +1296,33 @@ return (function () {
|
||||
triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
|
||||
} else if (token === "from" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
|
||||
tokens.shift();
|
||||
var selector = consumeUntil(
|
||||
tokens,
|
||||
WHITESPACE_OR_COMMA
|
||||
)
|
||||
// `next` and `previous` allow a selector-less syntax
|
||||
if (selector.length > 0) {
|
||||
from_arg += " " + selector;
|
||||
if (COMBINED_SELECTOR_START.test(tokens[0])) {
|
||||
var from_arg = consumeCSSSelector(tokens);
|
||||
} else {
|
||||
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
|
||||
tokens.shift();
|
||||
var selector = consumeCSSSelector(tokens);
|
||||
// `next` and `previous` allow a selector-less syntax
|
||||
if (selector.length > 0) {
|
||||
from_arg += " " + selector;
|
||||
}
|
||||
}
|
||||
}
|
||||
triggerSpec.from = from_arg;
|
||||
} else if (token === "target" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.target = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
triggerSpec.target = consumeCSSSelector(tokens);
|
||||
} else if (token === "throttle" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
|
||||
} else if (token === "queue" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
} else if ((token === "root" || token === "threshold") && tokens[0] === ":") {
|
||||
} else if (token === "root" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec[token] = consumeCSSSelector(tokens);
|
||||
} else if (token === "threshold" && tokens[0] === ":") {
|
||||
tokens.shift();
|
||||
triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
} else {
|
||||
|
||||
@@ -895,5 +895,64 @@ describe("hx-trigger attribute", function(){
|
||||
form.innerHTML.should.equal("Called!");
|
||||
})
|
||||
|
||||
it("correctly handles CSS descendant combinators", function(){
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
|
||||
var outer = make(`
|
||||
<div>
|
||||
<div id='outer'>
|
||||
<div id='first'>
|
||||
<div id='inner'></div>
|
||||
</div>
|
||||
<div id='second' hx-get='/test' hx-trigger='click from:previous (#outer div)'>Unclicked.</div>
|
||||
</div>
|
||||
<div id='other' hx-get='/test' hx-trigger='click from:(div #inner)'>Unclicked.</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
var inner = byId("inner");
|
||||
var second = byId("second");
|
||||
var other = byId("other");
|
||||
|
||||
second.innerHTML.should.equal("Unclicked.");
|
||||
other.innerHTML.should.equal("Unclicked.");
|
||||
|
||||
inner.click();
|
||||
this.server.respond();
|
||||
|
||||
second.innerHTML.should.equal("Clicked!");
|
||||
other.innerHTML.should.equal("Clicked!");
|
||||
})
|
||||
|
||||
|
||||
it('correctly handles CSS descendant combinators in modifier target', function() {
|
||||
this.server.respondWith('GET', '/test', 'Called');
|
||||
|
||||
document.addEventListener('htmx:syntax:error', function(evt) {
|
||||
chai.assert.fail('htmx:syntax:error');
|
||||
});
|
||||
|
||||
make('<div class="d1"><a id="a1" class="a1">Click me</a><a id="a2" class="a2">Click me</a></div>');
|
||||
var div = make('<div hx-trigger="click from:body target:(.d1 .a2)" hx-get="/test">Not Called</div>');
|
||||
|
||||
byId('a1').click();
|
||||
this.server.respond();
|
||||
div.innerHTML.should.equal("Not Called");
|
||||
|
||||
byId('a2').click();
|
||||
this.server.respond();
|
||||
div.innerHTML.should.equal("Called");
|
||||
});
|
||||
|
||||
it('correctly handles CSS descendant combinators in modifier root', function() {
|
||||
this.server.respondWith('GET', '/test', 'Called');
|
||||
|
||||
document.addEventListener('htmx:syntax:error', function(evt) {
|
||||
chai.assert.fail('htmx:syntax:error');
|
||||
});
|
||||
|
||||
make('<div hx-trigger="intersect root:{form input}" hx-get="/test">Not Called</div>');
|
||||
});
|
||||
|
||||
|
||||
})
|
||||
|
||||
@@ -153,3 +153,4 @@ The AJAX request can be triggered via JavaScript [`htmx.trigger()`](@/api.md#tri
|
||||
|
||||
* `hx-trigger` is not inherited
|
||||
* `hx-trigger` can be used without an AJAX request, in which case it will only fire the `htmx:trigger` event
|
||||
* In order to pass a CSS selector that contains whitespace (e.g. `form input`) to the `from`- or `target`-modifier, surround the selector in parentheses or curly brackets (e.g. `from:(form input)` or `from:nearest (form input)`)
|
||||
|
||||
Reference in New Issue
Block a user