Merge branch 'dev' into v2.0v2.0

This commit is contained in:
Carson Gross 2023-12-20 15:48:47 -07:00
commit 41e9ce3593
8 changed files with 256 additions and 50 deletions

View File

@ -120,24 +120,40 @@ return (function () {
return "[hx-" + verb + "], [data-hx-" + verb + "]"
}).join(", ");
var HEAD_TAG_REGEX = makeTagRegEx('head'),
TITLE_TAG_REGEX = makeTagRegEx('title'),
SVG_TAGS_REGEX = makeTagRegEx('svg', true);
//====================================================================
// Utilities
//====================================================================
/**
* @param {string} tag
* @param {boolean} global
* @returns {RegExp}
*/
function makeTagRegEx(tag, global = false) {
return new RegExp(`<${tag}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${tag}>`,
global ? 'gim' : 'im');
}
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;
}
/**
@ -283,38 +299,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("<body><template>" + resp + "</template></body>", 0);
var documentFragment = parseHTML("<body><template>" + content + "</template></body>", 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("<table>" + resp + "</table>", 1);
case "col":
return parseHTML("<table><colgroup>" + resp + "</colgroup></table>", 2);
case "tr":
return parseHTML("<table><tbody>" + resp + "</tbody></table>", 2);
case "td":
case "th":
return parseHTML("<table><tbody><tr>" + resp + "</tr></tbody></table>", 3);
case "script":
case "style":
return parseHTML("<div>" + resp + "</div>", 1);
default:
return parseHTML(resp, 0);
}
}
switch (startTag) {
case "thead":
case "tbody":
case "tfoot":
case "colgroup":
case "caption":
return parseHTML("<table>" + content + "</table>", 1);
case "col":
return parseHTML("<table><colgroup>" + content + "</colgroup></table>", 2);
case "tr":
return parseHTML("<table><tbody>" + content + "</tbody></table>", 2);
case "td":
case "th":
return parseHTML("<table><tbody><tr>" + content + "</tr></tbody></table>", 3);
case "script":
case "style":
return parseHTML("<div>" + content + "</div>", 1);
default:
return parseHTML(content, 0);
}
}
@ -1096,9 +1115,8 @@ return (function () {
function findTitle(content) {
if (content.indexOf('<title') > -1) {
var contentWithSvgsRemoved = content.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
var result = contentWithSvgsRemoved.match(/<title(\s[^>]*>|>)([\s\S]*?)<\/title>/im);
var contentWithSvgsRemoved = content.replace(SVG_TAGS_REGEX, '');
var result = contentWithSvgsRemoved.match(TITLE_TAG_REGEX);
if (result) {
return result[2];
}
@ -1519,14 +1537,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')
@ -1587,7 +1605,7 @@ return (function () {
handler(elt);
}
}
if (delay) {
if (delay > 0) {
setTimeout(load, delay);
} else {
load();
@ -1644,7 +1662,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 {
@ -3222,7 +3240,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);

View File

@ -197,27 +197,43 @@ describe("hx-swap attribute", function(){
swapSpec(make("<div hx-swap='innerHTML'/>")).swapDelay.should.equal(0)
swapSpec(make("<div hx-swap='innerHTML'/>")).settleDelay.should.equal(0) // set to 0 in tests
swapSpec(make("<div hx-swap='innerHTML swap:10'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='innerHTML swap:0'/>")).swapDelay.should.equal(0)
swapSpec(make("<div hx-swap='innerHTML swap:0ms'/>")).swapDelay.should.equal(0)
swapSpec(make("<div hx-swap='innerHTML settle:10'/>")).settleDelay.should.equal(10)
swapSpec(make("<div hx-swap='innerHTML settle:0'/>")).settleDelay.should.equal(0)
swapSpec(make("<div hx-swap='innerHTML settle:0s'/>")).settleDelay.should.equal(0)
swapSpec(make("<div hx-swap='innerHTML swap:10 settle:11'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='innerHTML swap:10 settle:11'/>")).settleDelay.should.equal(11)
swapSpec(make("<div hx-swap='innerHTML settle:11 swap:10'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='innerHTML settle:11 swap:10'/>")).settleDelay.should.equal(11)
swapSpec(make("<div hx-swap='innerHTML settle:0 swap:0'/>")).settleDelay.should.equal(0)
swapSpec(make("<div hx-swap='innerHTML settle:0 swap:0'/>")).settleDelay.should.equal(0)
swapSpec(make("<div hx-swap='innerHTML settle:0s swap:0ms'/>")).settleDelay.should.equal(0)
swapSpec(make("<div hx-swap='innerHTML settle:0s swap:0ms'/>")).settleDelay.should.equal(0)
swapSpec(make("<div hx-swap='innerHTML nonsense settle:11 swap:10'/>")).settleDelay.should.equal(11)
swapSpec(make("<div hx-swap='innerHTML nonsense settle:11 swap:10 '/>")).settleDelay.should.equal(11)
swapSpec(make("<div hx-swap='swap:10'/>")).swapStyle.should.equal("innerHTML")
swapSpec(make("<div hx-swap='swap:10'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='swap:0'/>")).swapDelay.should.equal(0);
swapSpec(make("<div hx-swap='swap:0s'/>")).swapDelay.should.equal(0);
swapSpec(make("<div hx-swap='settle:10'/>")).swapStyle.should.equal("innerHTML")
swapSpec(make("<div hx-swap='settle:10'/>")).settleDelay.should.equal(10)
swapSpec(make("<div hx-swap='settle:0'/>")).settleDelay.should.equal(0)
swapSpec(make("<div hx-swap='settle:0s'/>")).settleDelay.should.equal(0)
swapSpec(make("<div hx-swap='swap:10 settle:11'/>")).swapStyle.should.equal("innerHTML")
swapSpec(make("<div hx-swap='swap:10 settle:11'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='swap:10 settle:11'/>")).settleDelay.should.equal(11)
swapSpec(make("<div hx-swap='swap:0s settle:0'/>")).swapDelay.should.equal(0)
swapSpec(make("<div hx-swap='swap:0s settle:0'/>")).settleDelay.should.equal(0)
swapSpec(make("<div hx-swap='settle:11 swap:10'/>")).swapStyle.should.equal("innerHTML")
swapSpec(make("<div hx-swap='settle:11 swap:10'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='settle:11 swap:10'/>")).settleDelay.should.equal(11)
swapSpec(make("<div hx-swap='settle:0s swap:10'/>")).swapDelay.should.equal(10)
swapSpec(make("<div hx-swap='settle:0s swap:10'/>")).settleDelay.should.equal(0)
swapSpec(make("<div hx-swap='customstyle settle:11 swap:10'/>")).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 hx-get='/test' hx-swap='innerHTML swap:0ms'></div>"
);
div.click();
this.server.respond();
div.innerText.should.equal("Clicked!");
done();
});
it('works with a settle delay', function(done) {
this.server.respondWith("GET", "/test", "<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML settle:10ms'></div>");
var div = make("<div id='d1' hx-get='/test' hx-swap='outerHTML settle:10ms'></div>");
@ -246,6 +273,24 @@ describe("hx-swap attribute", function(){
}, 30);
});
it("works with no settle delay", function (done) {
this.server.respondWith(
"GET",
"/test",
"<div id='d1' class='foo' hx-get='/test' hx-swap='outerHTML settle:0ms'></div>"
);
var div = make(
"<div id='d1' hx-get='/test' hx-swap='outerHTML settle:0ms'></div>"
);
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", '<a id="a1" data-hx-get="/test2">Click Me</a>');

View File

@ -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'}],
@ -678,6 +684,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 hx-trigger='click throttle:0ms' hx-get='/test'></div>"
);
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 +751,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 hx-trigger='click delay:0ms' hx-get='/test'></div>"
);
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;

View File

@ -66,9 +66,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

View File

@ -0,0 +1,8 @@
<head>
<title>Index content</title>
</head>
<h1># Index</h1>
<ul>
<li><code>&lt;title&gt;</code> <b>should not</b> spawn inside <code>&lt;main&gt;</code></li>
<li><code>&lt;title&gt;</code> <b>should be</b> <em>index content</em></li>
</ul>

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" hx-preserve="true">
<meta name="htmx-config" content='{"useTemplateFragments": true}' hx-preserve="true">
<title>Index content</title>
<style hx-preserve="true">
* {
font-family: Arial, sans-serif;
font-size: 24px;
}
code {
font-family: monospace;
font-size: 20px;
}
hr { border: 1px solid black; }
</style>
<script src="../../../src/htmx.js" hx-preserve="true"></script>
<script src="../../../src/ext/head-support.js" hx-preserve="true"></script>
</head>
<body hx-ext="head-support" hx-boost="true">
<header hx-push-url="false" hx-target="main" hx-swap="innerHTML">
<nav>
<ul>
<li><a href="./index-partial.html">Index</a></li>
<li><a href="./other-content.html">See other content</a></li>
</ul>
</nav><hr>
</header>
<main>
<h1># Index</h1>
<ul>
<li><code>&lt;title&gt;</code> <b>should not</b> spawn inside <code>&lt;main&gt;</code></li>
<li><code>&lt;title&gt;</code> <b>should be</b> <em>index content</em></li>
</ul>
</main>
</body>
</html>

View File

@ -0,0 +1,16 @@
<head>
<title>Other content</title>
<style>
body {
background: lightgreen;
}
</style>
</head>
<h1># Swapped content</h1>
<ul>
<li><code>&lt;title&gt;</code> and &lt;style&gt;
<b>should not</b> spawn inside <code>&lt;main&gt;</code></li>
<li><code>&lt;title&gt;</code> <b>should be</b> <em>other content</em></li>
<li>Background <b>should be</b> green</li>
</ul>

View File

@ -41,6 +41,7 @@
<ul>
<li><a href="hxboost_relative_resources">Relative Resources</a></li>
<li><a href="hxboost_template_parsing">Template Parsing</a></li>
<li><a href="hxboost_partial_template_parsing">Partial Template Parsing</a></li>
</ul>
</li>
</ul>