From e9bce8db65c37f40935ca44bcb3b13581ea0dcd8 Mon Sep 17 00:00:00 2001
From: Fernando Comunello <66500627+fercomunello@users.noreply.github.com>
Date: Wed, 20 Dec 2023 19:37:42 -0300
Subject: [PATCH 1/5] Improve head tag parsing on template fragments (#2024)
Fix https://github.com/bigskysoftware/htmx/issues/2018
---
src/htmx.js | 74 +++++++++++--------
.../index-partial.html | 8 ++
.../index.html | 41 ++++++++++
.../other-content.html | 16 ++++
test/manual/index.html | 1 +
5 files changed, 111 insertions(+), 29 deletions(-)
create mode 100644 test/manual/hxboost_partial_template_parsing/index-partial.html
create mode 100644 test/manual/hxboost_partial_template_parsing/index.html
create mode 100644 test/manual/hxboost_partial_template_parsing/other-content.html
diff --git a/src/htmx.js b/src/htmx.js
index 668148ec..e81d96a9 100644
--- a/src/htmx.js
+++ b/src/htmx.js
@@ -128,10 +128,24 @@ return (function () {
return "[hx-" + verb + "], [data-hx-" + verb + "]"
}).join(", ");
+ var HEAD_TAG_REGEX = newTagRegex('head'),
+ TITLE_TAG_REGEX = newTagRegex('title'),
+ SVG_TAGS_REGEX = newTagRegex('svg', true);
+
//====================================================================
// Utilities
//====================================================================
+ /**
+ * @param {string} tag
+ * @param {boolean} global
+ * @returns {RegExp}
+ */
+ function newTagRegex(tag, global = false) {
+ return new RegExp(`<${tag}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${tag}>`,
+ global ? 'gim' : 'im');
+ }
+
function parseInterval(str) {
if (str == undefined) {
return undefined
@@ -282,38 +296,41 @@ return (function () {
/**
*
- * @param {string} resp
+ * @param {string} response
* @returns {Element}
*/
- function makeFragment(resp) {
- var partialResponse = !aFullPageResponse(resp);
+ function makeFragment(response) {
+ var partialResponse = !aFullPageResponse(response);
+ var startTag = getStartTag(response);
+ var content = response;
+ if (startTag === 'head') {
+ content = content.replace(HEAD_TAG_REGEX, '');
+ }
if (htmx.config.useTemplateFragments && partialResponse) {
- var documentFragment = parseHTML("
" + resp + " ", 0);
+ var documentFragment = parseHTML("" + content + " ", 0);
// @ts-ignore type mismatch between DocumentFragment and Element.
// TODO: Are these close enough for htmx to use interchangeably?
return documentFragment.querySelector('template').content;
- } else {
- var startTag = getStartTag(resp);
- switch (startTag) {
- case "thead":
- case "tbody":
- case "tfoot":
- case "colgroup":
- case "caption":
- return parseHTML("", 1);
- case "col":
- return parseHTML("", 2);
- case "tr":
- return parseHTML("", 2);
- case "td":
- case "th":
- return parseHTML("", 3);
- case "script":
- case "style":
- return parseHTML("" + resp + "
", 1);
- default:
- return parseHTML(resp, 0);
- }
+ }
+ switch (startTag) {
+ case "thead":
+ case "tbody":
+ case "tfoot":
+ case "colgroup":
+ case "caption":
+ return parseHTML("", 1);
+ case "col":
+ return parseHTML("", 2);
+ case "tr":
+ return parseHTML("", 2);
+ case "td":
+ case "th":
+ return parseHTML("", 3);
+ case "script":
+ case "style":
+ return parseHTML("" + content + "
", 1);
+ default:
+ return parseHTML(content, 0);
}
}
@@ -1097,9 +1114,8 @@ return (function () {
function findTitle(content) {
if (content.indexOf(' -1) {
- var contentWithSvgsRemoved = content.replace(/]*>|>)([\s\S]*?)<\/svg>/gim, '');
- var result = contentWithSvgsRemoved.match(/]*>|>)([\s\S]*?)<\/title>/im);
-
+ var contentWithSvgsRemoved = content.replace(SVG_TAGS_REGEX, '');
+ var result = contentWithSvgsRemoved.match(TITLE_TAG_REGEX);
if (result) {
return result[2];
}
diff --git a/test/manual/hxboost_partial_template_parsing/index-partial.html b/test/manual/hxboost_partial_template_parsing/index-partial.html
new file mode 100644
index 00000000..4ec55342
--- /dev/null
+++ b/test/manual/hxboost_partial_template_parsing/index-partial.html
@@ -0,0 +1,8 @@
+
+ Index content
+
+# Index
+
+ <title>
should not spawn inside <main>
+ <title>
should be index content
+
\ No newline at end of file
diff --git a/test/manual/hxboost_partial_template_parsing/index.html b/test/manual/hxboost_partial_template_parsing/index.html
new file mode 100644
index 00000000..dd8a93ff
--- /dev/null
+++ b/test/manual/hxboost_partial_template_parsing/index.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+ Index content
+
+
+
+
+
+
+
+
+
+ # Index
+
+ <title>
should not spawn inside <main>
+ <title>
should be index content
+
+
+
+
\ No newline at end of file
diff --git a/test/manual/hxboost_partial_template_parsing/other-content.html b/test/manual/hxboost_partial_template_parsing/other-content.html
new file mode 100644
index 00000000..2c749abe
--- /dev/null
+++ b/test/manual/hxboost_partial_template_parsing/other-content.html
@@ -0,0 +1,16 @@
+
+ Other content
+
+
+# Swapped content
+
+ <title>
and <style>
+ should not spawn inside <main>
+ <title>
should be other content
+ Background should be green
+
+
diff --git a/test/manual/index.html b/test/manual/index.html
index 99b25d65..19f41b5f 100644
--- a/test/manual/index.html
+++ b/test/manual/index.html
@@ -41,6 +41,7 @@
From 1f4903c213cc37f4d995c0585c2c01c850b0a5e9 Mon Sep 17 00:00:00 2001
From: Noa Aarts
Date: Wed, 20 Dec 2023 23:38:25 +0100
Subject: [PATCH 2/5] rewrite the HX-Retarget header to use extended query
selectors (#2017)
---
src/htmx.js | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/htmx.js b/src/htmx.js
index e81d96a9..2bb58239 100644
--- a/src/htmx.js
+++ b/src/htmx.js
@@ -3504,7 +3504,11 @@ return (function () {
}
if (hasHeader(xhr,/HX-Retarget:/i)) {
- responseInfo.target = getDocument().querySelector(xhr.getResponseHeader("HX-Retarget"));
+ if (xhr.getResponseHeader("HX-Retarget") === "this") {
+ responseInfo.target = elt;
+ } else {
+ responseInfo.target = querySelectorExt(elt, xhr.getResponseHeader("HX-Retarget"));
+ }
}
var historyUpdate = determineHistoryUpdates(elt, responseInfo);
From 078d5da5b4425bb779aedaad079b0643a24754cd Mon Sep 17 00:00:00 2001
From: Thomas Cowart
Date: Wed, 20 Dec 2023 17:46:04 -0500
Subject: [PATCH 3/5] Update parseInterval to handle "0" correctly (#1835)
* Update parseInterval to handle "0" correctly
When a parameter like "0ms" is passed in to parseInterval it gets parsed to 0.
Previously this would result in a return value of "undefined" because 0 is falsy
and thus the `return 0 || undefined` statements return undefined.
The purpose of the form `parseFloat(str) || undefined` was to return "undefined" if
parseFloat failed (parseFloat returns NaN, a falsy value, if it can't parse its
argument). Unfortunately, as mentioned, parseFloat can also succeed and return a
falsy value -- when the argument is "0" (or "0.0", etc.). So the new code, rather
than depending on the falsiness of the result of parseFloat, explicitly checks for
a NaN.
* Adds some semicolons
Adds some semicolons to parseInterval (and tests) for consistency.
* Add one more parseInterval test for "0"
Adds test test to make sure parseInterval works on "0".
* Adds functional tests for every, swap, settle, throttle, and delay
* Explcitly check that setTimeout values are > 0
These values come from user settings that are read from parseInterval,
so they could be a number or undefined.
If the value being checked is > 0 setTimeout will be called with some
associated function. If the value is 0 or 'undefined' the associated function
will be called immediately ('undefined' is not greater than 0).
* Change '!== undefined' to '> 0'
`pollInterval !== undefined` is a subtly different conditional than just `pollInterval` or `pollInterval > 0` (which are equivalent). Changes the conditional to `pollInterval > 0` so as to not change the behavior but also be more explicit in the test.
---
src/htmx.js | 28 ++++++------
test/attributes/hx-swap.js | 49 +++++++++++++++++++-
test/attributes/hx-trigger.js | 84 ++++++++++++++++++++++++++++++++++-
test/core/internals.js | 11 +++--
4 files changed, 152 insertions(+), 20 deletions(-)
diff --git a/src/htmx.js b/src/htmx.js
index 2bb58239..7861ae84 100644
--- a/src/htmx.js
+++ b/src/htmx.js
@@ -148,18 +148,20 @@ return (function () {
function parseInterval(str) {
if (str == undefined) {
- return undefined
+ return undefined;
}
+
+ let interval = NaN;
if (str.slice(-2) == "ms") {
- return parseFloat(str.slice(0,-2)) || undefined
+ interval = parseFloat(str.slice(0, -2));
+ } else if (str.slice(-1) == "s") {
+ interval = parseFloat(str.slice(0, -1)) * 1000;
+ } else if (str.slice(-1) == "m") {
+ interval = parseFloat(str.slice(0, -1)) * 1000 * 60;
+ } else {
+ interval = parseFloat(str);
}
- if (str.slice(-1) == "s") {
- return (parseFloat(str.slice(0,-1)) * 1000) || undefined
- }
- if (str.slice(-1) == "m") {
- return (parseFloat(str.slice(0,-1)) * 1000 * 60) || undefined
- }
- return parseFloat(str) || undefined
+ return isNaN(interval) ? undefined : interval;
}
/**
@@ -1538,14 +1540,14 @@ return (function () {
return;
}
- if (triggerSpec.throttle) {
+ if (triggerSpec.throttle > 0) {
if (!elementData.throttle) {
handler(elt, evt);
elementData.throttle = setTimeout(function () {
elementData.throttle = null;
}, triggerSpec.throttle);
}
- } else if (triggerSpec.delay) {
+ } else if (triggerSpec.delay > 0) {
elementData.delayed = setTimeout(function() { handler(elt, evt) }, triggerSpec.delay);
} else {
triggerEvent(elt, 'htmx:trigger')
@@ -1822,7 +1824,7 @@ return (function () {
handler(elt);
}
}
- if (delay) {
+ if (delay > 0) {
setTimeout(load, delay);
} else {
load();
@@ -1881,7 +1883,7 @@ return (function () {
if (!maybeFilterEvent(triggerSpec, elt, makeEvent("load", {elt: elt}))) {
loadImmediately(elt, handler, nodeData, triggerSpec.delay);
}
- } else if (triggerSpec.pollInterval) {
+ } else if (triggerSpec.pollInterval > 0) {
nodeData.polling = true;
processPolling(elt, handler, triggerSpec);
} else {
diff --git a/test/attributes/hx-swap.js b/test/attributes/hx-swap.js
index dff1b040..4ff63621 100644
--- a/test/attributes/hx-swap.js
+++ b/test/attributes/hx-swap.js
@@ -197,27 +197,43 @@ describe("hx-swap attribute", function(){
swapSpec(make("
")).swapDelay.should.equal(0)
swapSpec(make("
")).settleDelay.should.equal(0) // set to 0 in tests
swapSpec(make("
")).swapDelay.should.equal(10)
+ swapSpec(make("
")).swapDelay.should.equal(0)
+ swapSpec(make("
")).swapDelay.should.equal(0)
swapSpec(make("
")).settleDelay.should.equal(10)
+ swapSpec(make("
")).settleDelay.should.equal(0)
+ swapSpec(make("
")).settleDelay.should.equal(0)
swapSpec(make("
")).swapDelay.should.equal(10)
swapSpec(make("
")).settleDelay.should.equal(11)
swapSpec(make("
")).swapDelay.should.equal(10)
swapSpec(make("
")).settleDelay.should.equal(11)
+ swapSpec(make("
")).settleDelay.should.equal(0)
+ swapSpec(make("
")).settleDelay.should.equal(0)
+ swapSpec(make("
")).settleDelay.should.equal(0)
+ swapSpec(make("
")).settleDelay.should.equal(0)
swapSpec(make("
")).settleDelay.should.equal(11)
swapSpec(make("
")).settleDelay.should.equal(11)
-
+
swapSpec(make("
")).swapStyle.should.equal("innerHTML")
swapSpec(make("
")).swapDelay.should.equal(10)
+ swapSpec(make("
")).swapDelay.should.equal(0);
+ swapSpec(make("
")).swapDelay.should.equal(0);
swapSpec(make("
")).swapStyle.should.equal("innerHTML")
swapSpec(make("
")).settleDelay.should.equal(10)
-
+ swapSpec(make("
")).settleDelay.should.equal(0)
+ swapSpec(make("
")).settleDelay.should.equal(0)
+
swapSpec(make("
")).swapStyle.should.equal("innerHTML")
swapSpec(make("
")).swapDelay.should.equal(10)
swapSpec(make("
")).settleDelay.should.equal(11)
+ swapSpec(make("
")).swapDelay.should.equal(0)
+ swapSpec(make("
")).settleDelay.should.equal(0)
swapSpec(make("
")).swapStyle.should.equal("innerHTML")
swapSpec(make("
")).swapDelay.should.equal(10)
swapSpec(make("
")).settleDelay.should.equal(11)
+ swapSpec(make("
")).swapDelay.should.equal(10)
+ swapSpec(make("
")).settleDelay.should.equal(0)
swapSpec(make("
")).swapStyle.should.equal("customstyle")
})
@@ -234,6 +250,17 @@ describe("hx-swap attribute", function(){
}, 30);
});
+ it("works immediately with no swap delay", function (done) {
+ this.server.respondWith("GET", "/test", "Clicked!");
+ var div = make(
+ "
"
+ );
+ div.click();
+ this.server.respond();
+ div.innerText.should.equal("Clicked!");
+ done();
+ });
+
it('works with a settle delay', function(done) {
this.server.respondWith("GET", "/test", "
");
var div = make("
");
@@ -246,6 +273,24 @@ describe("hx-swap attribute", function(){
}, 30);
});
+ it("works with no settle delay", function (done) {
+ this.server.respondWith(
+ "GET",
+ "/test",
+ "
"
+ );
+ var div = make(
+ "
"
+ );
+ div.click();
+ this.server.respond();
+ div.classList.contains("foo").should.equal(false);
+ setTimeout(function () {
+ byId("d1").classList.contains("foo").should.equal(true);
+ done();
+ }, 30);
+ });
+
it('swap outerHTML properly w/ data-* prefix', function()
{
this.server.respondWith("GET", "/test", 'Click Me ');
diff --git a/test/attributes/hx-trigger.js b/test/attributes/hx-trigger.js
index 9e88b59d..96d64eaa 100644
--- a/test/attributes/hx-trigger.js
+++ b/test/attributes/hx-trigger.js
@@ -211,14 +211,20 @@ describe("hx-trigger attribute", function(){
var specExamples = {
"": [{trigger: 'click'}],
"every 1s": [{trigger: 'every', pollInterval: 1000}],
+ "every 0s": [{trigger: 'every', pollInterval: 0}],
+ "every 0ms": [{trigger: 'every', pollInterval: 0}],
"click": [{trigger: 'click'}],
"customEvent": [{trigger: 'customEvent'}],
"event changed": [{trigger: 'event', changed: true}],
"event once": [{trigger: 'event', once: true}],
- "event delay:1s": [{trigger: 'event', delay: 1000}],
"event throttle:1s": [{trigger: 'event', throttle: 1000}],
- "event delay:1s, foo": [{trigger: 'event', delay: 1000}, {trigger: 'foo'}],
+ "event throttle:0s": [{trigger: 'event', throttle: 0}],
+ "event throttle:0ms": [{trigger: 'event', throttle: 0}],
"event throttle:1s, foo": [{trigger: 'event', throttle: 1000}, {trigger: 'foo'}],
+ "event delay:1s": [{trigger: 'event', delay: 1000}],
+ "event delay:1s, foo": [{trigger: 'event', delay: 1000}, {trigger: 'foo'}],
+ "event delay:0s, foo": [{trigger: 'event', delay: 0}, {trigger: 'foo'}],
+ "event delay:0ms, foo": [{trigger: 'event', delay: 0}, {trigger: 'foo'}],
"event changed once delay:1s": [{trigger: 'event', changed: true, once: true, delay: 1000}],
"event1,event2": [{trigger: 'event1'}, {trigger: 'event2'}],
"event1, event2": [{trigger: 'event1'}, {trigger: 'event2'}],
@@ -398,6 +404,18 @@ describe("hx-trigger attribute", function(){
}, 100);
})
+ // Don't actually do this!
+ it("polling works every 0ms", function (done) {
+ this.server.respondWith("GET", "/test", "Called!");
+ var div = make('Not Called
');
+ this.server.autoRespond = true;
+ this.server.autoRespondAfter = 0;
+ setTimeout(function () {
+ div.innerHTML.should.equal("Called!");
+ done();
+ }, 100);
+ });
+
it('bad condition issues error', function(){
this.server.respondWith("GET", "/test", "Called!");
var div = make('Not Called
');
@@ -678,6 +696,37 @@ describe("hx-trigger attribute", function(){
}, 50);
});
+ it("A throttle of 0 does not multiple requests from happening", function (done) {
+ var requests = 0;
+ var server = this.server;
+ server.respondWith("GET", "/test", function (xhr) {
+ requests++;
+ xhr.respond(200, {}, "Requests: " + requests);
+ });
+ server.respondWith("GET", "/bar", "bar");
+ var div = make(
+ "
"
+ );
+
+ div.click();
+ server.respond();
+ div.innerText.should.equal("Requests: 1");
+
+ div.click();
+ server.respond();
+ div.innerText.should.equal("Requests: 2");
+
+ div.click();
+ server.respond();
+ div.innerText.should.equal("Requests: 3");
+
+ div.click();
+ server.respond();
+ div.innerText.should.equal("Requests: 4");
+
+ done()
+ });
+
it('delay delays the request', function(done)
{
var requests = 0;
@@ -714,6 +763,37 @@ describe("hx-trigger attribute", function(){
}, 50);
});
+ it("A 0 delay does not delay the request", function (done) {
+ var requests = 0;
+ this.server.respondWith("GET", "/test", function (xhr) {
+ requests++;
+ xhr.respond(200, {}, "Requests: " + requests);
+ });
+ this.server.respondWith("GET", "/bar", "bar");
+ var div = make(
+ "
"
+ );
+
+ div.click();
+ this.server.respond();
+ div.innerText.should.equal("Requests: 1");
+
+ div.click();
+ this.server.respond();
+ div.innerText.should.equal("Requests: 2");
+
+ div.click();
+ this.server.respond();
+ div.innerText.should.equal("Requests: 3");
+
+ div.click();
+ this.server.respond();
+ div.innerText.should.equal("Requests: 4");
+
+ done();
+ });
+
+
it('requests are queued with last one winning by default', function()
{
var requests = 0;
diff --git a/test/core/internals.js b/test/core/internals.js
index 1de1fb98..3d689293 100644
--- a/test/core/internals.js
+++ b/test/core/internals.js
@@ -76,9 +76,14 @@ describe("Core htmx internals Tests", function() {
it("handles parseInterval correctly", function() {
chai.expect(htmx.parseInterval("1ms")).to.be.equal(1);
chai.expect(htmx.parseInterval("300ms")).to.be.equal(300);
- chai.expect(htmx.parseInterval("1s")).to.be.equal(1000)
- chai.expect(htmx.parseInterval("1.5s")).to.be.equal(1500)
- chai.expect(htmx.parseInterval("2s")).to.be.equal(2000)
+ chai.expect(htmx.parseInterval("1s")).to.be.equal(1000);
+ chai.expect(htmx.parseInterval("1.5s")).to.be.equal(1500);
+ chai.expect(htmx.parseInterval("2s")).to.be.equal(2000);
+ chai.expect(htmx.parseInterval("0ms")).to.be.equal(0);
+ chai.expect(htmx.parseInterval("0s")).to.be.equal(0);
+ chai.expect(htmx.parseInterval("0m")).to.be.equal(0);
+ chai.expect(htmx.parseInterval("0")).to.be.equal(0);
+ chai.expect(htmx.parseInterval("5")).to.be.equal(5);
chai.expect(htmx.parseInterval(null)).to.be.undefined
chai.expect(htmx.parseInterval("")).to.be.undefined
From 126187fe8e643ed66be735682f6a63afed5eb4b3 Mon Sep 17 00:00:00 2001
From: Carson Gross
Date: Wed, 20 Dec 2023 15:47:05 -0700
Subject: [PATCH 4/5] slight rename
---
src/htmx.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/htmx.js b/src/htmx.js
index 7861ae84..36dfd1ed 100644
--- a/src/htmx.js
+++ b/src/htmx.js
@@ -128,9 +128,9 @@ return (function () {
return "[hx-" + verb + "], [data-hx-" + verb + "]"
}).join(", ");
- var HEAD_TAG_REGEX = newTagRegex('head'),
- TITLE_TAG_REGEX = newTagRegex('title'),
- SVG_TAGS_REGEX = newTagRegex('svg', true);
+ var HEAD_TAG_REGEX = makeTagRegEx('head'),
+ TITLE_TAG_REGEX = makeTagRegEx('title'),
+ SVG_TAGS_REGEX = makeTagRegEx('svg', true);
//====================================================================
// Utilities
@@ -141,7 +141,7 @@ return (function () {
* @param {boolean} global
* @returns {RegExp}
*/
- function newTagRegex(tag, global = false) {
+ function makeTagRegEx(tag, global = false) {
return new RegExp(`<${tag}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${tag}>`,
global ? 'gim' : 'im');
}
From b4080e71da1500ae986b02010d8238154954378b Mon Sep 17 00:00:00 2001
From: Carson Gross
Date: Wed, 20 Dec 2023 15:48:31 -0700
Subject: [PATCH 5/5] remove psychotic test
---
test/attributes/hx-trigger.js | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/test/attributes/hx-trigger.js b/test/attributes/hx-trigger.js
index 96d64eaa..f65f974b 100644
--- a/test/attributes/hx-trigger.js
+++ b/test/attributes/hx-trigger.js
@@ -404,18 +404,6 @@ describe("hx-trigger attribute", function(){
}, 100);
})
- // Don't actually do this!
- it("polling works every 0ms", function (done) {
- this.server.respondWith("GET", "/test", "Called!");
- var div = make('Not Called
');
- this.server.autoRespond = true;
- this.server.autoRespondAfter = 0;
- setTimeout(function () {
- div.innerHTML.should.equal("Called!");
- done();
- }, 100);
- });
-
it('bad condition issues error', function(){
this.server.respondWith("GET", "/test", "Called!");
var div = make('Not Called
');