diff --git a/editors/jetbrains/htmx.web-types.json b/editors/jetbrains/htmx.web-types.json index 5ccf400a0..a4d85984a 100644 --- a/editors/jetbrains/htmx.web-types.json +++ b/editors/jetbrains/htmx.web-types.json @@ -206,6 +206,14 @@ }, "doc-url": "https://htmx.org/attributes/hx-put/" }, + { + "name": "hx-query", + "description": "The `hx-query` attribute will cause an element to issue a `QUERY` to the specified URL and swap\nthe HTML into the DOM using a swap strategy:\n\n```html\n\n```", + "description-sections": { + "Not Inherited": "" + }, + "doc-url": "https://htmx.org/attributes/hx-query/" + }, { "name": "hx-replace-url", "description": "The `hx-replace-url` attribute allows you to replace the current url of the browser [location history](https://developer.mozilla.org/en-US/docs/Web/API/History_API).\n\nThe possible values of this attribute are:\n\n1. `true`, which replaces the fetched URL in the browser navigation bar.\n2. `false`, which disables replacing the fetched URL if it would otherwise be replaced due to inheritance.\n3. A URL to be replaced into the location bar.\n This may be relative or absolute, as per [`history.replaceState()`](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState).", @@ -592,4 +600,4 @@ ] } } -} \ No newline at end of file +} diff --git a/src/htmx.js b/src/htmx.js index 4d76457bd..9f2f3bc71 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -351,7 +351,7 @@ var htmx = (function() { withExtensions } - const VERBS = ['get', 'post', 'put', 'delete', 'patch'] + const VERBS = ['get', 'post', 'put', 'delete', 'patch', 'query'] const VERB_SELECTOR = VERBS.map(function(verb) { return '[hx-' + verb + '], [data-hx-' + verb + ']' }).join(', ') @@ -4039,11 +4039,11 @@ var htmx = (function() { /** * @param {XMLHttpRequest} xhr - * @param {RegExp} regexp + * @param {string} name * @return {boolean} */ - function hasHeader(xhr, regexp) { - return regexp.test(xhr.getAllResponseHeaders()) + function hasHeader(xhr, name) { + return xhr.getResponseHeader(name) !== null } /** @@ -4668,13 +4668,13 @@ var htmx = (function() { //= ========================================== let pathFromHeaders = null let typeFromHeaders = null - if (hasHeader(xhr, /HX-Push:/i)) { + if (hasHeader(xhr, 'HX-Push')) { pathFromHeaders = xhr.getResponseHeader('HX-Push') typeFromHeaders = 'push' - } else if (hasHeader(xhr, /HX-Push-Url:/i)) { + } else if (hasHeader(xhr, 'HX-Push-Url')) { pathFromHeaders = xhr.getResponseHeader('HX-Push-Url') typeFromHeaders = 'push' - } else if (hasHeader(xhr, /HX-Replace-Url:/i)) { + } else if (hasHeader(xhr, 'HX-Replace-Url')) { pathFromHeaders = xhr.getResponseHeader('HX-Replace-Url') typeFromHeaders = 'replace' } @@ -4809,11 +4809,11 @@ var htmx = (function() { if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return - if (hasHeader(xhr, /HX-Trigger:/i)) { + if (hasHeader(xhr, 'HX-Trigger')) { handleTriggerHeader(xhr, 'HX-Trigger', elt) } - if (hasHeader(xhr, /HX-Location:/i)) { + if (hasHeader(xhr, 'HX-Location')) { let redirectPath = xhr.getResponseHeader('HX-Location') /** @type {HtmxAjaxHelperContext&{path?:string}} */ var redirectSwapSpec = {} @@ -4828,9 +4828,9 @@ var htmx = (function() { return } - const shouldRefresh = hasHeader(xhr, /HX-Refresh:/i) && xhr.getResponseHeader('HX-Refresh') === 'true' + const shouldRefresh = hasHeader(xhr, 'HX-Refresh') && xhr.getResponseHeader('HX-Refresh') === 'true' - if (hasHeader(xhr, /HX-Redirect:/i)) { + if (hasHeader(xhr, 'HX-Redirect')) { responseInfo.keepIndicators = true htmx.location.href = xhr.getResponseHeader('HX-Redirect') shouldRefresh && htmx.location.reload() @@ -4859,11 +4859,11 @@ var htmx = (function() { } // response headers override response handling config - if (hasHeader(xhr, /HX-Retarget:/i)) { + if (hasHeader(xhr, 'HX-Retarget')) { responseInfo.target = resolveRetarget(elt, xhr.getResponseHeader('HX-Retarget')) } - if (hasHeader(xhr, /HX-Reswap:/i)) { + if (hasHeader(xhr, 'HX-Reswap')) { swapOverride = xhr.getResponseHeader('HX-Reswap') } @@ -4919,7 +4919,7 @@ var htmx = (function() { selectOverride = responseInfoSelect } - if (hasHeader(xhr, /HX-Reselect:/i)) { + if (hasHeader(xhr, 'HX-Reselect')) { selectOverride = xhr.getResponseHeader('HX-Reselect') } @@ -4933,7 +4933,7 @@ var htmx = (function() { anchor: responseInfo.pathInfo.anchor, contextElement: elt, afterSwapCallback: function() { - if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) { + if (hasHeader(xhr, 'HX-Trigger-After-Swap')) { let finalElt = elt if (!bodyContains(elt)) { finalElt = getDocument().body @@ -4942,7 +4942,7 @@ var htmx = (function() { } }, afterSettleCallback: function() { - if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) { + if (hasHeader(xhr, 'HX-Trigger-After-Settle')) { let finalElt = elt if (!bodyContains(elt)) { finalElt = getDocument().body @@ -5155,7 +5155,7 @@ var htmx = (function() { return htmx })() -/** @typedef {'get'|'head'|'post'|'put'|'delete'|'connect'|'options'|'trace'|'patch'} HttpVerb */ +/** @typedef {'get'|'head'|'post'|'put'|'delete'|'connect'|'options'|'trace'|'patch'|'query'} HttpVerb */ /** * @typedef {Object} SwapOptions diff --git a/test/attributes/hx-query.js b/test/attributes/hx-query.js new file mode 100644 index 000000000..a4a3a2f4a --- /dev/null +++ b/test/attributes/hx-query.js @@ -0,0 +1,32 @@ +describe('hx-query attribute', function() { + beforeEach(function() { + this.server = makeServer() + clearWorkArea() + }) + afterEach(function() { + this.server.restore() + clearWorkArea() + }) + + it('issues a QUERY request', function() { + this.server.respondWith('QUERY', '/test', function(xhr) { + xhr.respond(200, {}, 'Queried!') + }) + + var btn = make('') + btn.click() + this.server.respond() + btn.innerHTML.should.equal('Queried!') + }) + + it('issues a QUERY request w/ data-* prefix', function() { + this.server.respondWith('QUERY', '/test', function(xhr) { + xhr.respond(200, {}, 'Queried!') + }) + + var btn = make('') + btn.click() + this.server.respond() + btn.innerHTML.should.equal('Queried!') + }) +}) diff --git a/test/core/headers.js b/test/core/headers.js index 5048272c1..056f826c8 100644 --- a/test/core/headers.js +++ b/test/core/headers.js @@ -533,4 +533,20 @@ describe('Core htmx AJAX headers', function() { htmx._('loadHistoryFromServer')('/test') this.server.respond() }) + + // Regression for #527: response headers whose names *contain* an htmx + // header name as a substring (e.g. X-HX-Trigger) must not be treated as + // the htmx header. The previous implementation tested a regex against + // getAllResponseHeaders() and false-positively matched the substring, + // then crashed when getResponseHeader returned null. + it('does not crash or fire trigger when X-HX-Trigger header is present without HX-Trigger', function() { + this.server.respondWith('GET', '/test', [200, { 'X-HX-Trigger': 'foo' }, '']) + + var div = make('
') + var invokedEvent = false + div.addEventListener('foo', function() { invokedEvent = true }) + div.click() + this.server.respond() + invokedEvent.should.equal(false) + }) }) diff --git a/test/core/verbs.js b/test/core/verbs.js index da20faab9..76824ef3b 100644 --- a/test/core/verbs.js +++ b/test/core/verbs.js @@ -39,4 +39,12 @@ describe('Core htmx AJAX Verbs', function() { this.server.respond() div.innerHTML.should.equal('delete') }) + + it('handles basic query properly', function() { + this.server.respondWith('QUERY', '/test', 'query') + var div = make('