summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorCarson Gross <carson@bigsky.software>2023-12-13 17:08:52 -0700
committerCarson Gross <carson@bigsky.software>2023-12-13 17:08:52 -0700
commitad871a9576ceec2ba1a9a41eb7f44a7cf824fb9f (patch)
tree0ebdc84e46171e9d16528b93eb9b75427de38054
parent69c053c13673263290f665fab18787d55a2219e7 (diff)
parentf84b332d192c357e4786937df7e35babdbad4d51 (diff)
downloadhtmx-ad871a9576ceec2ba1a9a41eb7f44a7cf824fb9f.tar.gz
htmx-ad871a9576ceec2ba1a9a41eb7f44a7cf824fb9f.zip
Merge branch 'v2.0v2.0' into disable-inheritance
# Conflicts: # src/htmx.js
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md18
-rw-r--r--CHANGELOG.md12
-rw-r--r--CONTRIBUTING.md29
-rw-r--r--README.md2
-rw-r--r--dist/ext/ws.js2
-rw-r--r--dist/htmx.d.ts7
-rw-r--r--dist/htmx.js135
-rw-r--r--dist/htmx.min.js2
-rw-r--r--dist/htmx.min.js.gzbin15236 -> 15362 bytes
-rw-r--r--package.json2
-rw-r--r--src/ext/ws.js2
-rw-r--r--src/htmx.d.ts37
-rw-r--r--src/htmx.js246
-rw-r--r--test/attributes/hx-boost.js1
-rw-r--r--test/attributes/hx-on-wildcard.js32
-rw-r--r--test/attributes/hx-trigger.js59
-rw-r--r--test/core/ajax.js30
-rw-r--r--test/core/api.js56
-rw-r--r--test/core/events.js8
-rw-r--r--test/core/extensions.js5
-rw-r--r--test/core/headers.js27
-rw-r--r--test/core/internals.js10
-rw-r--r--test/core/perf.js4
-rw-r--r--test/core/regressions.js6
-rw-r--r--test/core/security.js6
-rw-r--r--test/core/validation.js6
-rw-r--r--test/ext/hyperscript.js5
-rw-r--r--test/ext/ws.js213
-rw-r--r--test/index.html2
-rw-r--r--test/manual/hxboost_relative_resources/index.html10
-rw-r--r--test/manual/hxboost_relative_resources/nested/img.pngbin0 -> 15693 bytes
-rw-r--r--test/manual/hxboost_relative_resources/nested/page2.html12
-rw-r--r--test/manual/index.html6
-rw-r--r--test/scratch/demo.html6
-rw-r--r--test/util/util.js30
-rw-r--r--www/content/_index.md17
-rw-r--r--www/content/api.md18
-rw-r--r--www/content/attributes/hx-on.md42
-rw-r--r--www/content/attributes/hx-trigger.md1
-rw-r--r--www/content/docs.md71
-rw-r--r--www/content/essays/_index.md2
-rw-r--r--www/content/essays/does-hypermedia-scale.md2
-rw-r--r--www/content/essays/hypermedia-apis-vs-data-apis.md2
-rw-r--r--www/content/essays/template-fragments.md1
-rw-r--r--www/content/essays/why-tend-not-to-use-content-negotiation.md177
-rw-r--r--www/content/examples/_index.md2
-rw-r--r--www/content/examples/active-search.md10
-rw-r--r--www/content/reference.md51
-rw-r--r--www/content/server-examples.md9
-rw-r--r--www/static/img/blackhost-logo.svg33
-rw-r--r--www/static/js/demo.js8
-rw-r--r--www/static/src/ext/ws.js2
-rw-r--r--www/static/src/htmx.d.ts7
-rw-r--r--www/static/src/htmx.js135
-rw-r--r--www/static/test/attributes/hx-boost.js1
-rw-r--r--www/static/test/attributes/hx-trigger.js59
-rw-r--r--www/static/test/core/ajax.js2
-rw-r--r--www/static/test/core/api.js56
-rw-r--r--www/static/test/ext/ws.js201
-rw-r--r--www/static/test/manual/hxboost_relative_resources/index.html10
-rw-r--r--www/static/test/manual/hxboost_relative_resources/nested/img.pngbin0 -> 15693 bytes
-rw-r--r--www/static/test/manual/hxboost_relative_resources/nested/page2.html12
-rw-r--r--www/static/test/manual/index.html6
-rw-r--r--www/static/test/ws-sse/server.mjs244
-rw-r--r--www/themes/htmx-theme/static/js/htmx.js135
65 files changed, 1903 insertions, 441 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 00000000..c3ab23c0
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,18 @@
+## Description
+*Please describe what changes you made, and why you feel they are necessary. Make sure to include
+code examples, where applicable.*
+
+Corresponding issue:
+
+## Testing
+*Please explain how you tested this change manually, and, if applicable, what new tests you added. If
+you're making a change to just the website, you can omit this section.*
+
+## Checklist
+
+* [ ] I have read the contribution guidelines
+* [ ] I have targeted this PR against the correct branch (`master` for website changes, `dev` for
+ source changes)
+* [ ] This is either a bugfix, a documentation update, or a new feature that has been explicitly
+ approved via an issue
+* [ ] I ran the test suite locally (`npm run test`) and verified that it succeeded
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d67d568..32baa50e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## [1.9.10] - 2023-12-??
+
+## [1.9.9] - 2023-11-21
+
+* Allow CSS selectors with whitespace in attributes like `hx-target` by using parens or curly-braces
+* Properly allow users to override the `Content-Type` request header
+* Added the `select` option to `htmx.ajax()`
+* Fixed a race condition in readystate detection that lead to htmx not being initialized in some scenarios with 3rd
+ party script loaders
+* Fixed a bug that caused relative resources to resolve against the wrong base URL when a new URL is pushed
+* Fixed a UI issue that could cause indicators to briefly flash
+
## [1.9.8] - 2023-11-06
* Fixed a few npm & build related issues
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7c10ad1e..8c52947a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,26 +1,31 @@
# Contributing
-Thank you for your interest in contributing! Because we're a small team, we have a couple
-contribution guidelines that make it easier for us to triage all the incoming suggestions.
-
-tl;dr: if proposing a new feature, start with an issue; if you think your change is a bugfix or otherwise uncontroversial, feel free to PR, but know that we might close it and kick you back to an issue if more discussion is required.
-
-Want to contribute but don't know where to start? Look for issues with the "help wanted" tag.
+Thank you for your interest in contributing! Because we're a small team, we have a couple contribution guidelines that make it easier for us to triage all the incoming suggestions.
## Issues
1. Issues are the best place to propose a new feature. Keep in mind that htmx is a small library, so there are lots of great ideas that don't fit in the core; it's always best to check in about an idea before doing a bunch of work on it.
-1. If you are adding a feature, consider doing it as an [extension](https://htmx.org/extensions). Even if we don't end up supporting it officially, you can publish it yourself and we can link to it.
+1. When proposing a new features, we will often suggest that you implement it as an [extension](https://htmx.org/extensions), so try that first. Even if we don't end up supporting it officially, you can publish it yourself and we can link to it.
1. Search the issues before proposing a feature to see if it is already under discussion. Referencing existing issues is a good way to increase the priority of your own.
-1. We don't have an issue template yet, but the more detailed your explanation, the more quickly we'll be able to evaluate it.
+1. We don't have an issue template yet, but the more detailed your description of the issue, the more quickly we'll be able to evaluate it.
1. See an issue that you also have? Give it a reaction (and comment, if you have something to add). We note that!
+1. If you haven't gotten any traction on an issue, feel free to bump it in the #issues-and-pull-requests channel on our Discord.
+1. Want to contribute but don't know where to start? Look for issues with the "help wanted" tag.
## Pull Requests
-1. Open PRs represent issues that we're actively thinking working on merging (at a pace we can manage). If we think a proposal needs more discussion, or that the existing code would require a lot of back-and-forth to merge, we might close it and suggest you make an issue.
-1. All PRs should be made against the `dev` branch, except documentation PRs (`www/` directory) which can be made against `master`.
-1. Please avoid sending the `dist` files along your PR, only include the `src` ones
+### Technical Requirements
1. Code, including tests, must be written in ES5 for [IE 11 compatibility](https://stackoverflow.com/questions/39902809/support-for-es6-in-internet-explorer-11).
+1. All PRs must be made against the `dev` branch, except documentation PRs (that only modify the `www/` directory) which can be made against `master`.
+1. Please avoid sending the `dist` files along your PR, only include the `src` ones.
1. Please include test cases in [`/test`](https://github.com/bigskysoftware/htmx/tree/dev/test) and docs in [`/www`](https://github.com/bigskysoftware/htmx/tree/dev/www).
+1. We squash all PRs, so you're welcome to submit with as many commits are you like; they will be evaluated as a single, standalone change.
+
+### Review Guidelines
+1. Open PRs represent issues that we're actively thinking working on merging (at a pace we can manage). If we think a proposal needs more discussion, or that the existing code would require a lot of back-and-forth to merge, we might close it and suggest you make an issue.
+1. Smaller PRs are easier and quicker to review. If we feel that the scope of your changes is too large, we will close the PR and try to suggest ways that the change could be broken down.
+1. Please do not PR new features unless you have already made an issue proposing the feature, and had it accepted by a core maintainer. This helps us triage the features we can support before you put a lot of work into them.
+1. Correspondingly, it is fine to directly PR bugfixes for behavior that htmx already guarantees, but please check if there's an issue first, and if you're not sure whether this *is* a bug, make an issue where we can hash it out..
1. Refactors that do not make functional changes will be automatically closed, unless explicitly solicited. Imagine someone came into your house unannounced, rearranged a bunch of furniture, and left.
-1. Typo fixes in documentation are welcome, but if it's at all debatable we might just close it.
+1. Typo fixes in the documentation (not the code comments) are welcome, but formatting or debatable grammar changes will be automatically closed.
## Misc
1. If you think we closed something incorrectly, feel free to (politely) tell us why! We're human and make mistakes.
+1. There are lots of ways to improve htmx besides code changes. Sometimes a problem can be solved with better docs, usage patterns, extensions, or community support. Talk to us and we can almost always help you get to a solution.
diff --git a/README.md b/README.md
index 556a2efe..a9287d12 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,7 @@ By removing these arbitrary constraints htmx completes HTML as a
## quick start
```html
- <script src="https://unpkg.com/htmx.org@1.9.8"></script>
+ <script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- have a button POST a click via AJAX -->
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me
diff --git a/dist/ext/ws.js b/dist/ext/ws.js
index 3366e9e5..05be1eca 100644
--- a/dist/ext/ws.js
+++ b/dist/ext/ws.js
@@ -200,7 +200,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
if (!this.socket) {
api.triggerErrorEvent()
}
- if (sendElt && api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
+ if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
message: message,
socketWrapper: this.publicInterface
})) {
diff --git a/dist/htmx.d.ts b/dist/htmx.d.ts
index 2342300c..6200cd3f 100644
--- a/dist/htmx.d.ts
+++ b/dist/htmx.d.ts
@@ -48,7 +48,7 @@ export function ajax(verb: string, path: string, selector: string): Promise<void
export function ajax(
verb: string,
path: string,
- context: Partial<{ source: any; event: any; handler: any; target: any; swap: any; values: any; headers: any }>
+ context: Partial<{ source: any; event: any; handler: any; target: any; swap: any; values: any; headers: any; select: any }>
): Promise<void>;
/**
@@ -395,6 +395,11 @@ export interface HtmxConfig {
* @default false
*/
selfRequestsOnly?: boolean;
+ /**
+ * Whether or not the target of a boosted element is scrolled into the viewport.
+ * @default true
+ */
+ scrollIntoViewOnBoost?: boolean;
}
/**
diff --git a/dist/htmx.js b/dist/htmx.js
index 79a4702e..1f0709d7 100644
--- a/dist/htmx.js
+++ b/dist/htmx.js
@@ -75,6 +75,7 @@ return (function () {
globalViewTransitions: false,
methodsThatUseUrlParams: ["get"],
selfRequestsOnly: false,
+ ignoreTitle: false,
scrollIntoViewOnBoost: true
},
parseInterval:parseInterval,
@@ -87,7 +88,7 @@ return (function () {
sock.binaryType = htmx.config.wsBinaryType;
return sock;
},
- version: "1.9.8"
+ version: "1.9.9"
};
/** @type {import("./htmx").HtmxInternalApi} */
@@ -1145,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;
@@ -1233,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';
/**
@@ -1281,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 {
@@ -2906,6 +2925,7 @@ return (function () {
values : context.values,
targetOverride: resolveTarget(context.target),
swapOverride: context.swap,
+ select: context.select,
returnPromise: true
});
}
@@ -2960,6 +2980,7 @@ return (function () {
elt = getDocument().body;
}
var responseHandler = etc.handler || handleAjaxResponse;
+ var select = etc.select || null;
if (!bodyContains(elt)) {
// do not issue requests for elements removed from the DOM
@@ -3108,6 +3129,11 @@ return (function () {
var headers = getHeaders(elt, target, promptResponse);
+
+ if (verb !== 'get' && !usesFormData(elt)) {
+ headers['Content-Type'] = 'application/x-www-form-urlencoded';
+ }
+
if (etc.headers) {
headers = mergeObjects(headers, etc.headers);
}
@@ -3121,10 +3147,6 @@ return (function () {
var allParameters = mergeObjects(rawParameters, expressionVars);
var filteredParameters = filterValues(allParameters, elt);
- if (verb !== 'get' && !usesFormData(elt)) {
- headers['Content-Type'] = 'application/x-www-form-urlencoded';
- }
-
if (htmx.config.getCacheBusterParam && verb === 'get') {
filteredParameters['org.htmx.cache-buster'] = getRawAttribute(target, "id") || "true";
}
@@ -3222,7 +3244,7 @@ return (function () {
}
var responseInfo = {
- xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted,
+ xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted, select: select,
pathInfo: {
requestPath: path,
finalRequestPath: finalPath,
@@ -3393,6 +3415,7 @@ return (function () {
var target = responseInfo.target;
var etc = responseInfo.etc;
var requestConfig = responseInfo.requestConfig;
+ var select = responseInfo.select;
if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return;
@@ -3502,10 +3525,26 @@ return (function () {
}
var selectOverride;
+ if (select) {
+ selectOverride = select;
+ }
+
if (hasHeader(xhr, /HX-Reselect:/i)) {
selectOverride = xhr.getResponseHeader("HX-Reselect");
}
+ // if we need to save history, do so, before swapping so that relative resources have the correct base URL
+ if (historyUpdate.type) {
+ triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
+ if (historyUpdate.type === "push") {
+ pushUrlIntoHistory(historyUpdate.path);
+ triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
+ } else {
+ replaceUrlInHistory(historyUpdate.path);
+ triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
+ }
+ }
+
var settleInfo = makeSettleInfo(target);
selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, selectOverride);
@@ -3555,17 +3594,6 @@ return (function () {
triggerEvent(elt, 'htmx:afterSettle', responseInfo);
});
- // if we need to save history, do so
- if (historyUpdate.type) {
- triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
- if (historyUpdate.type === "push") {
- pushUrlIntoHistory(historyUpdate.path);
- triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
- } else {
- replaceUrlInHistory(historyUpdate.path);
- triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
- }
- }
if (responseInfo.pathInfo.anchor) {
var anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor);
if(anchorTarget) {
@@ -3724,25 +3752,34 @@ return (function () {
//====================================================================
// Initialization
//====================================================================
- var isReady = false
- getDocument().addEventListener('DOMContentLoaded', function() {
- isReady = true
- })
-
/**
- * Execute a function now if DOMContentLoaded has fired, otherwise listen for it.
- *
- * This function uses isReady because there is no realiable way to ask the browswer whether
- * the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded
- * firing and readystate=complete.
+ * We want to initialize the page elements after DOMContentLoaded
+ * fires, but there isn't always a good way to tell whether
+ * it has already fired when we get here or not.
*/
- function ready(fn) {
- // Checking readyState here is a failsafe in case the htmx script tag entered the DOM by
- // some means other than the initial page load.
- if (isReady || getDocument().readyState === 'complete') {
- fn();
- } else {
- getDocument().addEventListener('DOMContentLoaded', fn);
+ function ready(functionToCall) {
+ // call the function exactly once no matter how many times this is called
+ var callReadyFunction = function() {
+ if (!functionToCall) return;
+ functionToCall();
+ functionToCall = null;
+ };
+
+ if (getDocument().readyState === "complete") {
+ // DOMContentLoaded definitely fired, we can initialize the page
+ callReadyFunction();
+ }
+ else {
+ /* DOMContentLoaded *maybe* already fired, wait for
+ * the next DOMContentLoaded or readystatechange event
+ */
+ getDocument().addEventListener("DOMContentLoaded", function() {
+ callReadyFunction();
+ });
+ getDocument().addEventListener("readystatechange", function() {
+ if (getDocument().readyState !== "complete") return;
+ callReadyFunction();
+ });
}
}
@@ -3750,9 +3787,9 @@ return (function () {
if (htmx.config.includeIndicatorStyles !== false) {
getDocument().head.insertAdjacentHTML("beforeend",
"<style>\
- ." + htmx.config.indicatorClass + "{opacity:0;transition: opacity 200ms ease-in;}\
- ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1}\
- ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1}\
+ ." + htmx.config.indicatorClass + "{opacity:0}\
+ ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\
+ ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\
</style>");
}
}
diff --git a/dist/htmx.min.js b/dist/htmx.min.js
index 4091536a..6c0a0f27 100644
--- a/dist/htmx.min.js
+++ b/dist/htmx.min.js
@@ -1 +1 @@
-(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Y={onLoad:t,process:Dt,on:Z,off:K,trigger:fe,ajax:Cr,find:E,findAll:f,closest:d,values:function(e,t){var r=or(e,t||"post");return r.values},remove:B,addClass:F,removeClass:n,toggleClass:V,takeClass:j,defineExtension:Ar,removeExtension:Nr,logAll:X,logNone:U,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,scrollIntoViewOnBoost:true},parseInterval:v,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Y.config.wsBinaryType;return t},version:"1.9.8"};var r={addTriggerHandler:St,bodyContains:oe,canAccessLocalStorage:M,findThisElement:ve,filterValues:cr,hasAttribute:o,getAttributeValue:ee,getClosestAttributeValue:re,getClosestMatch:c,getExpressionVars:wr,getHeaders:fr,getInputValues:or,getInternalData:ie,getSwapSpecification:dr,getTriggerSpecs:Ze,getTarget:ge,makeFragment:l,mergeObjects:se,makeSettleInfo:T,oobSwap:ye,querySelectorExt:le,selectAndSwap:Ue,settleImmediately:Jt,shouldCancel:tt,triggerEvent:fe,triggerErrorEvent:ue,withExtensions:C};var b=["get","post","put","delete","patch"];var w=b.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function v(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function Q(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function ee(e,t){return Q(e,t)||Q(e,"data-"+t)}function u(e){return e.parentElement}function te(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function R(e,t,r){var n=ee(t,r);var i=ee(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function re(t,r){var n=null;c(t,function(e){return n=R(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function q(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=te().createDocumentFragment()}return i}function H(e){return e.match(/<body/)}function l(e){var t=!H(e);if(Y.config.useTemplateFragments&&t){var r=i("<body><template>"+e+"</template></body>",0);return r.querySelector("template").content}else{var n=q(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("<table>"+e+"</table>",1);case"col":return i("<table><colgroup>"+e+"</colgroup></table>",2);case"tr":return i("<table><tbody>"+e+"</tbody></table>",2);case"td":case"th":return i("<table><tbody><tr>"+e+"</tr></tbody></table>",3);case"script":case"style":return i("<div>"+e+"</div>",1);default:return i(e,0)}}}function ne(e){if(e){e()}}function L(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function A(e){return L(e,"Function")}function N(e){return L(e,"Object")}function ie(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function I(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function ae(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function k(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function oe(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return te().body.contains(e.getRootNode().host)}else{return te().body.contains(e)}}function P(e){return e.trim().split(/\s+/)}function se(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function S(e){try{return JSON.parse(e)}catch(e){y(e);return null}}function M(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function D(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return xr(te().body,function(){return eval(e)})}function t(t){var e=Y.on("htmx:load",function(e){t(e.detail.elt)});return e}function X(){Y.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function U(){Y.logger=null}function E(e,t){if(t){return e.querySelector(t)}else{return E(te(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(te(),e)}}function B(e,t){e=s(e);if(t){setTimeout(function(){B(e);e=null},t)}else{e.parentElement.removeChild(e)}}function F(e,t,r){e=s(e);if(r){setTimeout(function(){F(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function V(e,t){e=s(e);e.classList.toggle(t)}function j(e,t){e=s(e);ae(e.parentElement.children,function(e){n(e,t)});F(e,t)}function d(e,t){e=s(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function _(e,t){return e.substring(e.length-t.length)===t}function z(e){var t=e.trim();if(g(t,"<")&&_(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function W(e,t){if(t.indexOf("closest ")===0){return[d(e,z(t.substr(8)))]}else if(t.indexOf("find ")===0){return[E(e,z(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[$(e,z(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[G(e,z(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return te().querySelectorAll(z(t))}}var $=function(e,t){var r=te().querySelectorAll(t);for(var n=0;n<r.length;n++){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING){return i}}};var G=function(e,t){var r=te().querySelectorAll(t);for(var n=r.length-1;n>=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function le(e,t){if(t){return W(e,t)[0]}else{return W(te().body,e)[0]}}function s(e){if(L(e,"String")){return E(e)}else{return e}}function J(e,t,r){if(A(t)){return{target:te().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function Z(t,r,n){Pr(function(){var e=J(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=A(r);return e?r:n}function K(t,r,n){Pr(function(){var e=J(t,r,n);e.target.removeEventListener(e.event,e.listener)});return A(r)?r:n}var he=te().createElement("output");function de(e,t){var r=re(e,t);if(r){if(r==="this"){return[ve(e,t)]}else{var n=W(e,r);if(n.length===0){y('The selector "'+r+'" on '+t+" returned no matches!");return[he]}else{return n}}}}function ve(e,t){return c(e,function(e){return ee(e,t)!=null})}function ge(e){var t=re(e,"hx-target");if(t){if(t==="this"){return ve(e,"hx-target")}else{return le(e,t)}}else{var r=ie(e);if(r.boosted){return te().body}else{return e}}}function me(e){var t=Y.config.attributesToSettle;for(var r=0;r<t.length;r++){if(e===t[r]){return true}}return false}function pe(t,r){ae(t.attributes,function(e){if(!r.hasAttribute(e.name)&&me(e.name)){t.removeAttribute(e.name)}});ae(r.attributes,function(e){if(me(e.name)){t.setAttribute(e.name,e.value)}})}function xe(e,t){var r=Ir(t);for(var n=0;n<r.length;n++){var i=r[n];try{if(i.isInlineSwap(e)){return true}}catch(e){y(e)}}return e==="outerHTML"}function ye(e,i,a){var t="#"+Q(i,"id");var o="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=te().querySelectorAll(t);if(r){ae(r,function(e){var t;var r=i.cloneNode(true);t=te().createDocumentFragment();t.appendChild(r);if(!xe(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!fe(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){De(o,e,e,t,a)}ae(a.elts,function(e){fe(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);ue(te().body,"htmx:oobErrorNoTarget",{content:i})}return e}function be(e,t,r){var n=re(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e<i.length;e++){var a=i[e].split(":",2);var o=a[0].trim();if(o.indexOf("#")===0){o=o.substring(1)}var s=a[1]||"true";var l=t.querySelector("#"+o);if(l){ye(s,l,r)}}}ae(f(t,"[hx-swap-oob], [data-hx-swap-oob]"),function(e){var t=ee(e,"hx-swap-oob");if(t!=null){ye(t,e,r)}})}function we(e){ae(f(e,"[hx-preserve], [data-hx-preserve]"),function(e){var t=ee(e,"id");var r=te().getElementById(t);if(r!=null){e.parentNode.replaceChild(r,e)}})}function Se(o,e,s){ae(e.querySelectorAll("[id]"),function(e){var t=Q(e,"id");if(t&&t.length>0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();pe(e,i);s.tasks.push(function(){pe(e,a)})}}})}function Ee(e){return function(){n(e,Y.config.addedClass);Dt(e);Ct(e);Ce(e);fe(e,"htmx:load")}}function Ce(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Se(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;F(i,Y.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Ee(i))}}}function Te(e,t){var r=0;while(r<e.length){t=(t<<5)-t+e.charCodeAt(r++)|0}return t}function Oe(e){var t=0;if(e.attributes){for(var r=0;r<e.attributes.length;r++){var n=e.attributes[r];if(n.value){t=Te(n.name,t);t=Te(n.value,t)}}}return t}function Re(t){var r=ie(t);if(r.onHandlers){for(let e=0;e<r.onHandlers.length;e++){const n=r.onHandlers[e];t.removeEventListener(n.event,n.listener)}delete r.onHandlers}}function qe(e){var t=ie(e);if(t.timeout){clearTimeout(t.timeout)}if(t.webSocket){t.webSocket.close()}if(t.sseEventSource){t.sseEventSource.close()}if(t.listenerInfos){ae(t.listenerInfos,function(e){if(e.on){e.on.removeEventListener(e.trigger,e.listener)}})}if(t.initHash){t.initHash=null}Re(e)}function m(e){fe(e,"htmx:beforeCleanupElement");qe(e);if(e.children){ae(e.children,function(e){m(e)})}}function He(t,e,r){if(t.tagName==="BODY"){return Pe(t,e,r)}else{var n;var i=t.previousSibling;a(u(t),t,e,r);if(i==null){n=u(t).firstChild}else{n=i.nextSibling}ie(t).replacedWith=n;r.elts=r.elts.filter(function(e){return e!=t});while(n&&n!==t){if(n.nodeType===Node.ELEMENT_NODE){r.elts.push(n)}n=n.nextElementSibling}m(t);u(t).removeChild(t)}}function Le(e,t,r){return a(e,e.firstChild,t,r)}function Ae(e,t,r){return a(u(e),e,t,r)}function Ne(e,t,r){return a(e,null,t,r)}function Ie(e,t,r){return a(u(e),e.nextSibling,t,r)}function ke(e,t,r){m(e);return u(e).removeChild(e)}function Pe(e,t,r){var n=e.firstChild;a(e,n,t,r);if(n){while(n.nextSibling){m(n.nextSibling);e.removeChild(n.nextSibling)}m(n);e.removeChild(n)}}function Me(e,t,r){var n=r||re(e,"hx-select");if(n){var i=te().createDocumentFragment();ae(t.querySelectorAll(n),function(e){i.appendChild(e)});t=i}return t}function De(e,t,r,n,i){switch(e){case"none":return;case"outerHTML":He(r,n,i);return;case"afterbegin":Le(r,n,i);return;case"beforebegin":Ae(r,n,i);return;case"beforeend":Ne(r,n,i);return;case"afterend":Ie(r,n,i);return;case"delete":ke(r,n,i);return;default:var a=Ir(t);for(var o=0;o<a.length;o++){var s=a[o];try{var l=s.handleSwap(e,r,n,i);if(l){if(typeof l.length!=="undefined"){for(var u=0;u<l.length;u++){var f=l[u];if(f.nodeType!==Node.TEXT_NODE&&f.nodeType!==Node.COMMENT_NODE){i.tasks.push(Ee(f))}}}return}}catch(e){y(e)}}if(e==="innerHTML"){Pe(r,n,i)}else{De(Y.config.defaultSwapStyle,t,r,n,i)}}}function Xe(e){if(e.indexOf("<title")>-1){var t=e.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/<title(\s[^>]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function Ue(e,t,r,n,i,a){i.title=Xe(n);var o=l(n);if(o){be(r,o,i);o=Me(r,o,a);we(o);return De(e,r,t,o,i)}}function Be(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=S(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!N(o)){o={value:o}}fe(r,a,o)}}}else{var s=n.split(",");for(var l=0;l<s.length;l++){fe(r,s[l].trim(),[])}}}var Fe=/\s/;var p=/[\s,]/;var Ve=/[_$a-zA-Z]/;var je=/[_$a-zA-Z0-9]/;var _e=['"',"'","/"];var ze=/[^\s]/;function We(e){var t=[];var r=0;while(r<e.length){if(Ve.exec(e.charAt(r))){var n=r;while(je.exec(e.charAt(r+1))){r++}t.push(e.substr(n,r-n+1))}else if(_e.indexOf(e.charAt(r))!==-1){var i=e.charAt(r);var n=r;r++;while(r<e.length&&e.charAt(r)!==i){if(e.charAt(r)==="\\"){r++}r++}t.push(e.substr(n,r-n+1))}else{var a=e.charAt(r);t.push(a)}r++}return t}function $e(e,t,r){return Ve.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==r&&t!=="."}function Ge(e,t,r){if(t[0]==="["){t.shift();var n=1;var i=" return (function("+r+"){ return (";var a=null;while(t.length>0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=xr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){ue(te().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if($e(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function x(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}var Je="input, textarea, select";function Ze(e){var t=ee(e,"hx-trigger");var r=[];if(t){var n=We(t);do{x(n,ze);var i=n.length;var a=x(n,/[,\[\s]/);if(a!==""){if(a==="every"){var o={trigger:"every"};x(n,ze);o.pollInterval=v(x(n,/[,\[\s]/));x(n,ze);var s=Ge(e,n,"event");if(s){o.eventFilter=s}r.push(o)}else if(a.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:a.substr(4)})}else{var l={trigger:a};var s=Ge(e,n,"event");if(s){l.eventFilter=s}while(n.length>0&&n[0]!==","){x(n,ze);var u=n.shift();if(u==="changed"){l.changed=true}else if(u==="once"){l.once=true}else if(u==="consume"){l.consume=true}else if(u==="delay"&&n[0]===":"){n.shift();l.delay=v(x(n,p))}else if(u==="from"&&n[0]===":"){n.shift();var f=x(n,p);if(f==="closest"||f==="find"||f==="next"||f==="previous"){n.shift();var c=x(n,p);if(c.length>0){f+=" "+c}}l.from=f}else if(u==="target"&&n[0]===":"){n.shift();l.target=x(n,p)}else if(u==="throttle"&&n[0]===":"){n.shift();l.throttle=v(x(n,p))}else if(u==="queue"&&n[0]===":"){n.shift();l.queue=x(n,p)}else if((u==="root"||u==="threshold")&&n[0]===":"){n.shift();l[u]=x(n,p)}else{ue(e,"htmx:syntax:error",{token:n.shift()})}}r.push(l)}}if(n.length===i){ue(e,"htmx:syntax:error",{token:n.shift()})}x(n,ze)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,Je)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function Ke(e){ie(e).cancelled=true}function Ye(e,t,r){var n=ie(e);n.timeout=setTimeout(function(){if(oe(e)&&n.cancelled!==true){if(!nt(r,e,Ut("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}Ye(e,t,r)}},r.pollInterval)}function Qe(e){return location.hostname===e.hostname&&Q(e,"href")&&Q(e,"href").indexOf("#")!==0}function et(t,r,e){if(t.tagName==="A"&&Qe(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=Q(t,"href")}else{var a=Q(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=Q(t,"action")}e.forEach(function(e){it(t,function(e,t){if(d(e,Y.config.disableSelector)){m(e);return}ce(n,i,e,t)},r,e,true)})}}function tt(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&d(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function rt(e,t){return ie(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function nt(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){ue(te().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function it(a,o,e,s,l){var u=ie(a);var t;if(s.from){t=W(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ie(e);t.lastValue=e.value})}ae(t,function(n){var i=function(e){if(!oe(a)){n.removeEventListener(s.trigger,i);return}if(rt(a,e)){return}if(l||tt(e,a)){e.preventDefault()}if(nt(s,a,e)){return}var t=ie(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ie(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{fe(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var at=false;var ot=null;function st(){if(!ot){ot=function(){at=true};window.addEventListener("scroll",ot);setInterval(function(){if(at){at=false;ae(te().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){lt(e)})}},200)}}function lt(t){if(!o(t,"data-hx-revealed")&&k(t)){t.setAttribute("data-hx-revealed","true");var e=ie(t);if(e.initHash){fe(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){fe(t,"revealed")},{once:true})}}}function ut(e,t,r){var n=P(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){ft(e,a[1],0)}if(a[0]==="send"){ht(e)}}}function ft(s,r,n){if(!oe(s)){return}if(r.indexOf("/")==0){var e=location.hostname+(location.port?":"+location.port:"");if(location.protocol=="https:"){r="wss://"+e+r}else if(location.protocol=="http:"){r="ws://"+e+r}}var t=Y.createWebSocket(r);t.onerror=function(e){ue(s,"htmx:wsError",{error:e,socket:t});ct(s)};t.onclose=function(e){if([1006,1012,1013].indexOf(e.code)>=0){var t=dt(n);setTimeout(function(){ft(s,r,n+1)},t)}};t.onopen=function(e){n=0};ie(s).webSocket=t;t.addEventListener("message",function(e){if(ct(s)){return}var t=e.data;C(s,function(e){t=e.transformResponse(t,null,s)});var r=T(s);var n=l(t);var i=I(n.children);for(var a=0;a<i.length;a++){var o=i[a];ye(ee(o,"hx-swap-oob")||"true",o,r)}Jt(r.tasks)})}function ct(e){if(!oe(e)){ie(e).webSocket.close();return true}}function ht(u){var f=c(u,function(e){return ie(e).webSocket!=null});if(f){u.addEventListener(Ze(u)[0].trigger,function(e){var t=ie(f).webSocket;var r=fr(u,f);var n=or(u,"post");var i=n.errors;var a=n.values;var o=wr(u);var s=se(a,o);var l=cr(s,u);l["HEADERS"]=r;if(i&&i.length>0){fe(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(tt(e,u)){e.preventDefault()}})}else{ue(u,"htmx:noWebSocketSourceError")}}function dt(e){var t=Y.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}y('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function vt(e,t,r){var n=P(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){gt(e,a[1])}if(a[0]==="swap"){mt(e,a[1])}}}function gt(t,e){var r=Y.createEventSource(e);r.onerror=function(e){ue(t,"htmx:sseError",{error:e,source:r});xt(t)};ie(t).sseEventSource=r}function mt(a,o){var s=c(a,yt);if(s){var l=ie(s).sseEventSource;var u=function(e){if(xt(s)){return}if(!oe(a)){l.removeEventListener(o,u);return}var t=e.data;C(a,function(e){t=e.transformResponse(t,null,a)});var r=dr(a);var n=ge(a);var i=T(a);Ue(r.swapStyle,n,a,t,i);Jt(i.tasks);fe(a,"htmx:sseMessage",e)};ie(a).sseListener=u;l.addEventListener(o,u)}else{ue(a,"htmx:noSSESourceError")}}function pt(e,t,r){var n=c(e,yt);if(n){var i=ie(n).sseEventSource;var a=function(){if(!xt(n)){if(oe(e)){t(e)}else{i.removeEventListener(r,a)}}};ie(e).sseListener=a;i.addEventListener(r,a)}else{ue(e,"htmx:noSSESourceError")}}function xt(e){if(!oe(e)){ie(e).sseEventSource.close();return true}}function yt(e){return ie(e).sseEventSource!=null}function bt(e,t,r,n){var i=function(){if(!r.loaded){r.loaded=true;t(e)}};if(n){setTimeout(i,n)}else{i()}}function wt(t,i,e){var a=false;ae(b,function(r){if(o(t,"hx-"+r)){var n=ee(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){St(t,e,i,function(e,t){if(d(e,Y.config.disableSelector)){m(e);return}ce(r,n,e,t)})})}});return a}function St(n,e,t,r){if(e.sseEvent){pt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){st();it(n,r,t,e);lt(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=le(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t<e.length;t++){var r=e[t];if(r.isIntersecting){fe(n,"intersect");break}}},i);a.observe(n);it(n,r,t,e)}else if(e.trigger==="load"){if(!nt(e,n,Ut("load",{elt:n}))){bt(n,r,t,e.delay)}}else if(e.pollInterval){t.polling=true;Ye(n,r,e)}else{it(n,r,t,e)}}function Et(e){if(Y.config.allowScriptTags&&(e.type==="text/javascript"||e.type==="module"||e.type==="")){var t=te().createElement("script");ae(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Y.config.inlineScriptNonce){t.nonce=Y.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){y(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Ct(e){if(h(e,"script")){Et(e)}ae(f(e,"script"),function(e){Et(e)})}function Tt(){return document.querySelector("[hx-boost], [data-hx-boost]")}function Ot(e){var t=null;var r=[];if(document.evaluate){var n=document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]',e);while(t=n.iterateNext())r.push(t)}else{var i=document.getElementsByTagName("*");for(var a=0;a<i.length;a++){var o=i[a].attributes;for(var s=0;s<o.length;s++){var l=o[s].name;if(g(l,"hx-on:")||g(l,"data-hx-on:")){r.push(i[a])}}}}return r}function Rt(e){if(e.querySelectorAll){var t=Tt()?", a":"";var r=e.querySelectorAll(w+t+", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws],"+" [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");return r}else{return[]}}function qt(e){var t=d(e.target,"button, input[type='submit']");var r=Lt(e);if(r){r.lastButtonClicked=t}}function Ht(e){var t=Lt(e);if(t){t.lastButtonClicked=null}}function Lt(e){var t=d(e.target,"button, input[type='submit']");if(!t){return}var r=s("#"+Q(t,"form"))||d(t,"form");if(!r){return}return ie(r)}function At(e){e.addEventListener("click",qt);e.addEventListener("focusin",qt);e.addEventListener("focusout",Ht)}function Nt(e){var t=We(e);var r=0;for(let e=0;e<t.length;e++){const n=t[e];if(n==="{"){r++}else if(n==="}"){r--}}return r}function It(t,e,r){var n=ie(t);if(!Array.isArray(n.onHandlers)){n.onHandlers=[]}var i;var a=function(e){return xr(t,function(){if(!i){i=new Function("event",r)}i.call(t,e)})};t.addEventListener(e,a);n.onHandlers.push({event:e,listener:a})}function kt(e){var t=ee(e,"hx-on");if(t){var r={};var n=t.split("\n");var i=null;var a=0;while(n.length>0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Nt(o)}for(var l in r){It(e,l,r[l])}}}function Pt(t){Re(t);for(var e=0;e<t.attributes.length;e++){var r=t.attributes[e].name;var n=t.attributes[e].value;if(g(r,"hx-on:")||g(r,"data-hx-on:")){let e=r.slice(r.indexOf(":")+1);if(g(e,":"))e="htmx"+e;It(t,e,n)}}}function Mt(t){if(d(t,Y.config.disableSelector)){m(t);return}var r=ie(t);if(r.initHash!==Oe(t)){qe(t);r.initHash=Oe(t);kt(t);fe(t,"htmx:beforeProcessNode");if(t.value){r.lastValue=t.value}var e=Ze(t);var n=wt(t,r,e);if(!n){if(re(t,"hx-boost")==="true"){et(t,r,e)}else if(o(t,"hx-trigger")){e.forEach(function(e){St(t,e,r,function(){})})}}if(t.tagName==="FORM"||Q(t,"type")==="submit"&&o(t,"form")){At(t)}var i=ee(t,"hx-sse");if(i){vt(t,r,i)}var a=ee(t,"hx-ws");if(a){ut(t,r,a)}fe(t,"htmx:afterProcessNode")}}function Dt(e){e=s(e);if(d(e,Y.config.disableSelector)){m(e);return}Mt(e);ae(Rt(e),function(e){Mt(e)});ae(Ot(e),Pt)}function Xt(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Ut(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{bubbles:true,cancelable:true,detail:t})}else{r=te().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function ue(e,t,r){fe(e,t,se({error:t},r))}function Bt(e){return e==="htmx:afterProcessNode"}function C(e,t){ae(Ir(e),function(e){try{t(e)}catch(e){y(e)}})}function y(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function fe(e,t,r){e=s(e);if(r==null){r={}}r["elt"]=e;var n=Ut(t,r);if(Y.logger&&!Bt(t)){Y.logger(e,t,r)}if(r.error){y(r.error);fe(e,"htmx:error",{errorInfo:r})}var i=e.dispatchEvent(n);var a=Xt(t);if(i&&a!==t){var o=Ut(a,n.detail);i=i&&e.dispatchEvent(o)}C(e,function(e){i=i&&(e.onEvent(t,n)!==false&&!n.defaultPrevented)});return i}var Ft=location.pathname+location.search;function Vt(){var e=te().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||te().body}function jt(e,t,r,n){if(!M()){return}if(Y.config.historyCacheSize<=0){localStorage.removeItem("htmx-history-cache");return}e=D(e);var i=S(localStorage.getItem("htmx-history-cache"))||[];for(var a=0;a<i.length;a++){if(i[a].url===e){i.splice(a,1);break}}var o={url:e,content:t,title:r,scroll:n};fe(te().body,"htmx:historyItemCreated",{item:o,cache:i});i.push(o);while(i.length>Y.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){ue(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function _t(e){if(!M()){return null}e=D(e);var t=S(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r<t.length;r++){if(t[r].url===e){return t[r]}}return null}function zt(e){var t=Y.config.requestClass;var r=e.cloneNode(true);ae(f(r,"."+t),function(e){n(e,t)});return r.innerHTML}function Wt(){var e=Vt();var t=Ft||location.pathname+location.search;var r;try{r=te().querySelector('[hx-history="false" i],[data-hx-history="false" i]')}catch(e){r=te().querySelector('[hx-history="false"],[data-hx-history="false"]')}if(!r){fe(te().body,"htmx:beforeHistorySave",{path:t,historyElt:e});jt(t,zt(e),te().title,window.scrollY)}if(Y.config.historyEnabled)history.replaceState({htmx:true},te().title,window.location.href)}function $t(e){if(Y.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(_(e,"&")||_(e,"?")){e=e.slice(0,-1)}}if(Y.config.historyEnabled){history.pushState({htmx:true},"",e)}Ft=e}function Gt(e){if(Y.config.historyEnabled)history.replaceState({htmx:true},"",e);Ft=e}function Jt(e){ae(e,function(e){e.call()})}function Zt(a){var e=new XMLHttpRequest;var o={path:a,xhr:e};fe(te().body,"htmx:historyCacheMiss",o);e.open("GET",a,true);e.setRequestHeader("HX-History-Restore-Request","true");e.onload=function(){if(this.status>=200&&this.status<400){fe(te().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=Vt();var r=T(t);var n=Xe(this.response);if(n){var i=E("title");if(i){i.innerHTML=n}else{window.document.title=n}}Pe(t,e,r);Jt(r.tasks);Ft=a;fe(te().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{ue(te().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function Kt(e){Wt();e=e||location.pathname+location.search;var t=_t(e);if(t){var r=l(t.content);var n=Vt();var i=T(n);Pe(n,r,i);Jt(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);Ft=e;fe(te().body,"htmx:historyRestore",{path:e,item:t})}else{if(Y.config.refreshOnHistoryMiss){window.location.reload(true)}else{Zt(e)}}}function Yt(e){var t=de(e,"hx-indicator");if(t==null){t=[e]}ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Y.config.requestClass)});return t}function Qt(e){var t=de(e,"hx-disabled-elt");if(t==null){t=[]}ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function er(e,t){ae(e,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Y.config.requestClass)}});ae(t,function(e){var t=ie(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function tr(e,t){for(var r=0;r<e.length;r++){var n=e[r];if(n.isSameNode(t)){return true}}return false}function rr(e){if(e.name===""||e.name==null||e.disabled){return false}if(e.type==="button"||e.type==="submit"||e.tagName==="image"||e.tagName==="reset"||e.tagName==="file"){return false}if(e.type==="checkbox"||e.type==="radio"){return e.checked}return true}function nr(e,t,r){if(e!=null&&t!=null){var n=r[e];if(n===undefined){r[e]=t}else if(Array.isArray(n)){if(Array.isArray(t)){r[e]=n.concat(t)}else{n.push(t)}}else{if(Array.isArray(t)){r[e]=[n].concat(t)}else{r[e]=[n,t]}}}}function ir(t,r,n,e,i){if(e==null||tr(t,e)){return}else{t.push(e)}if(rr(e)){var a=Q(e,"name");var o=e.value;if(e.multiple&&e.tagName==="SELECT"){o=I(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e.files){o=I(e.files)}nr(a,o,r);if(i){ar(e,n)}}if(h(e,"form")){var s=e.elements;ae(s,function(e){ir(t,r,n,e,i)})}}function ar(e,t){if(e.willValidate){fe(e,"htmx:validation:validate");if(!e.checkValidity()){t.push({elt:e,message:e.validationMessage,validity:e.validity});fe(e,"htmx:validation:failed",{message:e.validationMessage,validity:e.validity})}}}function or(e,t){var r=[];var n={};var i={};var a=[];var o=ie(e);if(o.lastButtonClicked&&!oe(o.lastButtonClicked)){o.lastButtonClicked=null}var s=h(e,"form")&&e.noValidate!==true||ee(e,"hx-validate")==="true";if(o.lastButtonClicked){s=s&&o.lastButtonClicked.formNoValidate!==true}if(t!=="get"){ir(r,i,a,d(e,"form"),s)}ir(r,n,a,e,s);if(o.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&Q(e,"type")==="submit"){var l=o.lastButtonClicked||e;var u=Q(l,"name");nr(u,l.value,i)}var f=de(e,"hx-include");ae(f,function(e){ir(r,n,a,e,s);if(!h(e,"form")){ae(e.querySelectorAll(Je),function(e){ir(r,n,a,e,s)})}});n=se(n,i);return{errors:a,values:n}}function sr(e,t,r){if(e!==""){e+="&"}if(String(r)==="[object Object]"){r=JSON.stringify(r)}var n=encodeURIComponent(r);e+=encodeURIComponent(t)+"="+n;return e}function lr(e){var t="";for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){ae(n,function(e){t=sr(t,r,e)})}else{t=sr(t,r,n)}}}return t}function ur(e){var t=new FormData;for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){ae(n,function(e){t.append(r,e)})}else{t.append(r,n)}}}return t}function fr(e,t,r){var n={"HX-Request":"true","HX-Trigger":Q(e,"id"),"HX-Trigger-Name":Q(e,"name"),"HX-Target":ee(t,"id"),"HX-Current-URL":te().location.href};pr(e,"hx-headers",false,n);if(r!==undefined){n["HX-Prompt"]=r}if(ie(e).boosted){n["HX-Boosted"]="true"}return n}function cr(t,e){var r=re(e,"hx-params");if(r){if(r==="none"){return{}}else if(r==="*"){return t}else if(r.indexOf("not ")===0){ae(r.substr(4).split(","),function(e){e=e.trim();delete t[e]});return t}else{var n={};ae(r.split(","),function(e){e=e.trim();n[e]=t[e]});return n}}else{return t}}function hr(e){return Q(e,"href")&&Q(e,"href").indexOf("#")>=0}function dr(e,t){var r=t?t:re(e,"hx-swap");var n={swapStyle:ie(e).boosted?"innerHTML":Y.config.defaultSwapStyle,swapDelay:Y.config.defaultSwapDelay,settleDelay:Y.config.defaultSettleDelay};if(Y.config.scrollIntoViewOnBoost&&ie(e).boosted&&!hr(e)){n["show"]="top"}if(r){var i=P(r);if(i.length>0){for(var a=0;a<i.length;a++){var o=i[a];if(o.indexOf("swap:")===0){n["swapDelay"]=v(o.substr(5))}else if(o.indexOf("settle:")===0){n["settleDelay"]=v(o.substr(7))}else if(o.indexOf("transition:")===0){n["transition"]=o.substr(11)==="true"}else if(o.indexOf("ignoreTitle:")===0){n["ignoreTitle"]=o.substr(12)==="true"}else if(o.indexOf("scroll:")===0){var s=o.substr(7);var l=s.split(":");var u=l.pop();var f=l.length>0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var d=o.substr("focus-scroll:".length);n["focusScroll"]=d=="true"}else if(a==0){n["swapStyle"]=o}else{y("Unknown modifier in hx-swap: "+o)}}}}return n}function vr(e){return re(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&Q(e,"enctype")==="multipart/form-data"}function gr(t,r,n){var i=null;C(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(vr(r)){return ur(n)}else{return lr(n)}}}function T(e){return{tasks:[],elts:[e]}}function mr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=le(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=le(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Y.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Y.config.scrollBehavior})}}}function pr(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=ee(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=xr(e,function(){return Function("return ("+a+")")()},{})}else{s=S(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return pr(u(e),t,r,n)}function xr(e,t,r){if(Y.config.allowEval){return t()}else{ue(e,"htmx:evalDisallowedError");return r}}function yr(e,t){return pr(e,"hx-vars",true,t)}function br(e,t){return pr(e,"hx-vals",false,t)}function wr(e){return se(yr(e),br(e))}function Sr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Er(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){ue(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return e.getAllResponseHeaders().match(t)}function Cr(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||L(r,"String")){return ce(e,t,null,null,{targetOverride:s(r),returnPromise:true})}else{return ce(e,t,s(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:s(r.target),swapOverride:r.swap,returnPromise:true})}}else{return ce(e,t,null,null,{returnPromise:true})}}function Tr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function Or(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=g(t,document.location.origin)}if(Y.config.selfRequestsOnly){if(!n){return false}}return fe(e,"htmx:validateUrl",se({url:i,sameHost:n},r))}function ce(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=te().body}var M=a.handler||qr;if(!oe(n)){ne(o);return l}var u=a.targetOverride||ge(n);if(u==null||u==he){ue(n,"htmx:targetError",{target:ee(n,"hx-target")});ne(s);return l}var f=ie(n);var c=f.lastButtonClicked;if(c){var h=Q(c,"formaction");if(h!=null){r=h}var d=Q(c,"formmethod");if(d!=null){if(d.toLowerCase()!=="dialog"){t=d}}}var v=re(n,"hx-confirm");if(e===undefined){var D=function(e){return ce(t,r,n,i,a,!!e)};var X={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:D,question:v};if(fe(n,"htmx:confirm",X)===false){ne(o);return l}}var g=n;var m=re(n,"hx-sync");var p=null;var x=false;if(m){var U=m.split(":");var B=U[0].trim();if(B==="this"){g=ve(n,"hx-sync")}else{g=le(n,B)}m=(U[1]||"drop").trim();f=ie(g);if(m==="drop"&&f.xhr&&f.abortable!==true){ne(o);return l}else if(m==="abort"){if(f.xhr){ne(o);return l}else{x=true}}else if(m==="replace"){fe(g,"htmx:abort")}else if(m.indexOf("queue")===0){var F=m.split(" ");p=(F[1]||"last").trim()}}if(f.xhr){if(f.abortable){fe(g,"htmx:abort")}else{if(p==null){if(i){var y=ie(i);if(y&&y.triggerSpec&&y.triggerSpec.queue){p=y.triggerSpec.queue}}if(p==null){p="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(p==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){ce(t,r,n,i,a)})}else if(p==="all"){f.queuedRequests.push(function(){ce(t,r,n,i,a)})}else if(p==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){ce(t,r,n,i,a)})}ne(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=x;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var V=re(n,"hx-prompt");if(V){var S=prompt(V);if(S===null||!fe(n,"htmx:prompt",{prompt:S,target:u})){ne(o);w();return l}}if(v&&!e){if(!confirm(v)){ne(o);w();return l}}var E=fr(n,u,S);if(a.headers){E=se(E,a.headers)}var j=or(n,t);var C=j.errors;var T=j.values;if(a.values){T=se(T,a.values)}var _=wr(n);var z=se(T,_);var O=cr(z,n);if(t!=="get"&&!vr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(Y.config.getCacheBusterParam&&t==="get"){O["org.htmx.cache-buster"]=Q(u,"id")||"true"}if(r==null||r===""){r=te().location.href}var R=pr(n,"hx-request");var W=ie(n).boosted;var q=Y.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:W,useUrlParams:q,parameters:O,unfilteredParameters:z,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||R.credentials||Y.config.withCredentials,timeout:a.timeout||R.timeout||Y.config.timeout,path:r,triggeringEvent:i};if(!fe(n,"htmx:configRequest",H)){ne(o);w();return l}r=H.path;t=H.verb;E=H.headers;O=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){fe(n,"htmx:validation:halted",H);ne(o);w();return l}var $=r.split("#");var G=$[0];var L=$[1];var A=r;if(q){A=G;var J=Object.keys(O).length!==0;if(J){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=lr(O);if(L){A+="#"+L}}}if(!Or(n,A,H)){ue(n,"htmx:invalidPath",H);ne(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(R.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var Z=E[N];Sr(b,N,Z)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:W,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=Tr(n);I.pathInfo.responsePath=Er(b);M(n,I);er(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:afterOnLoad",I);if(!oe(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(oe(r)){t=r}}if(t){fe(t,"htmx:afterRequest",I);fe(t,"htmx:afterOnLoad",I)}}ne(o);w()}catch(e){ue(n,"htmx:onLoadError",se({error:e},I));throw e}};b.onerror=function(){er(k,P);ue(n,"htmx:afterRequest",I);ue(n,"htmx:sendError",I);ne(s);w()};b.onabort=function(){er(k,P);ue(n,"htmx:afterRequest",I);ue(n,"htmx:sendAbort",I);ne(s);w()};b.ontimeout=function(){er(k,P);ue(n,"htmx:afterRequest",I);ue(n,"htmx:timeout",I);ne(s);w()};if(!fe(n,"htmx:beforeRequest",I)){ne(o);w();return l}var k=Yt(n);var P=Qt(n);ae(["loadstart","loadend","progress","abort"],function(t){ae([b,b.upload],function(e){e.addEventListener(t,function(e){fe(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});fe(n,"htmx:beforeSend",I);var K=q?null:gr(b,n,O);b.send(K);return l}function Rr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=re(e,"hx-push-url");var l=re(e,"hx-replace-url");var u=ie(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function qr(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;if(!fe(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){Be(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){Wt();var r=f.getResponseHeader("HX-Location");var h;if(r.indexOf("{")===0){h=S(r);r=h["path"];delete h["path"]}Cr("GET",r,h).then(function(){$t(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){u.target=te().querySelector(f.getResponseHeader("HX-Retarget"))}var d=Rr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var v=f.response;var a=f.status>=400;var g=Y.config.ignoreTitle;var o=se({shouldSwap:i,serverResponse:v,isError:a,ignoreTitle:g},u);if(!fe(c,"htmx:beforeSwap",o))return;c=o.target;v=o.serverResponse;a=o.isError;g=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){Ke(l)}C(l,function(e){v=e.transformResponse(v,f,l)});if(d.type){Wt()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var h=dr(l,s);if(h.hasOwnProperty("ignoreTitle")){g=h.ignoreTitle}c.classList.add(Y.config.swappingClass);var m=null;var p=null;var x=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}var n=T(c);Ue(h.swapStyle,c,l,v,n,r);if(t.elt&&!oe(t.elt)&&Q(t.elt,"id")){var i=document.getElementById(Q(t.elt,"id"));var a={preventScroll:h.focusScroll!==undefined?!h.focusScroll:!Y.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Y.config.swappingClass);ae(n.elts,function(e){if(e.classList){e.classList.add(Y.config.settlingClass)}fe(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!oe(l)){o=te().body}Be(f,"HX-Trigger-After-Swap",o)}var s=function(){ae(n.tasks,function(e){e.call()});ae(n.elts,function(e){if(e.classList){e.classList.remove(Y.config.settlingClass)}fe(e,"htmx:afterSettle",u)});if(d.type){fe(te().body,"htmx:beforeHistoryUpdate",se({history:d},u));if(d.type==="push"){$t(d.path);fe(te().body,"htmx:pushedIntoHistory",{path:d.path})}else{Gt(d.path);fe(te().body,"htmx:replacedInHistory",{path:d.path})}}if(u.pathInfo.anchor){var e=te().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!g){var t=E("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}mr(n.elts,h);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!oe(l)){r=te().body}Be(f,"HX-Trigger-After-Settle",r)}ne(m)};if(h.settleDelay>0){setTimeout(s,h.settleDelay)}else{s()}}catch(e){ue(l,"htmx:swapError",u);ne(p);throw e}};var y=Y.config.globalViewTransitions;if(h.hasOwnProperty("transition")){y=h.transition}if(y&&fe(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var b=new Promise(function(e,t){m=e;p=t});var w=x;x=function(){document.startViewTransition(function(){w();return b})}}if(h.swapDelay>0){setTimeout(x,h.swapDelay)}else{x()}}if(a){ue(l,"htmx:responseError",se({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Hr={};function Lr(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function Ar(e,t){if(t.init){t.init(r)}Hr[e]=se(Lr(),t)}function Nr(e){delete Hr[e]}function Ir(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=ee(e,"hx-ext");if(t){ae(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Hr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Ir(u(e),r,n)}var kr=false;te().addEventListener("DOMContentLoaded",function(){kr=true});function Pr(e){if(kr||te().readyState==="complete"){e()}else{te().addEventListener("DOMContentLoaded",e)}}function Mr(){if(Y.config.includeIndicatorStyles!==false){te().head.insertAdjacentHTML("beforeend","<style> ."+Y.config.indicatorClass+"{opacity:0;transition: opacity 200ms ease-in;} ."+Y.config.requestClass+" ."+Y.config.indicatorClass+"{opacity:1} ."+Y.config.requestClass+"."+Y.config.indicatorClass+"{opacity:1} </style>")}}function Dr(){var e=te().querySelector('meta[name="htmx-config"]');if(e){return S(e.content)}else{return null}}function Xr(){var e=Dr();if(e){Y.config=se(Y.config,e)}}Pr(function(){Xr();Mr();var e=te().body;Dt(e);var t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ie(t);if(r&&r.xhr){r.xhr.abort()}});var r=window.onpopstate;window.onpopstate=function(e){if(e.state&&e.state.htmx){Kt();ae(t,function(e){fe(e,"htmx:restored",{document:te(),triggerEvent:fe})})}else{if(r){r(e)}}};setTimeout(function(){fe(e,"htmx:load",{});e=null},0)});return Y}()}); \ No newline at end of file
+(function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:t,process:Bt,on:Z,off:K,trigger:ce,ajax:Or,find:C,findAll:f,closest:v,values:function(e,t){var r=ur(e,t||"post");return r.values},remove:B,addClass:F,removeClass:n,toggleClass:V,takeClass:j,defineExtension:kr,removeExtension:Pr,logAll:X,logNone:U,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true},parseInterval:d,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.9"};var r={addTriggerHandler:Tt,bodyContains:se,canAccessLocalStorage:M,findThisElement:de,filterValues:dr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Cr,getHeaders:vr,getInputValues:ur,getInternalData:ae,getSwapSpecification:mr,getTriggerSpecs:Qe,getTarget:ge,makeFragment:l,mergeObjects:le,makeSettleInfo:R,oobSwap:xe,querySelectorExt:ue,selectAndSwap:Ue,settleImmediately:Yt,shouldCancel:it,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:T};var b=["get","post","put","delete","patch"];var w=b.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function S(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=S(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function q(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function H(e){return e.match(/<body/)}function l(e){var t=!H(e);if(Q.config.useTemplateFragments&&t){var r=i("<body><template>"+e+"</template></body>",0);return r.querySelector("template").content}else{var n=q(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("<table>"+e+"</table>",1);case"col":return i("<table><colgroup>"+e+"</colgroup></table>",2);case"tr":return i("<table><tbody>"+e+"</tbody></table>",2);case"td":case"th":return i("<table><tbody><tr>"+e+"</tr></tbody></table>",3);case"script":case"style":return i("<div>"+e+"</div>",1);default:return i(e,0)}}}function ie(e){if(e){e()}}function L(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function A(e){return L(e,"Function")}function N(e){return L(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function I(e){var t=[];if(e){for(var r=0;r<e.length;r++){t.push(e[r])}}return t}function oe(e,t){if(e){for(var r=0;r<e.length;r++){t(e[r])}}}function k(e){var t=e.getBoundingClientRect();var r=t.top;var n=t.bottom;return r<window.innerHeight&&n>=0}function se(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return re().body.contains(e.getRootNode().host)}else{return re().body.contains(e)}}function P(e){return e.trim().split(/\s+/)}function le(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function E(e){try{return JSON.parse(e)}catch(e){x(e);return null}}function M(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function D(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return wr(re().body,function(){return eval(e)})}function t(t){var e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function X(){Q.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function U(){Q.logger=null}function C(e,t){if(t){return e.querySelector(t)}else{return C(re(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(re(),e)}}function B(e,t){e=s(e);if(t){setTimeout(function(){B(e);e=null},t)}else{e.parentElement.removeChild(e)}}function F(e,t,r){e=s(e);if(r){setTimeout(function(){F(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function V(e,t){e=s(e);e.classList.toggle(t)}function j(e,t){e=s(e);oe(e.parentElement.children,function(e){n(e,t)});F(e,t)}function v(e,t){e=s(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function _(e,t){return e.substring(e.length-t.length)===t}function z(e){var t=e.trim();if(g(t,"<")&&_(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function W(e,t){if(t.indexOf("closest ")===0){return[v(e,z(t.substr(8)))]}else if(t.indexOf("find ")===0){return[C(e,z(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[$(e,z(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[G(e,z(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return re().querySelectorAll(z(t))}}var $=function(e,t){var r=re().querySelectorAll(t);for(var n=0;n<r.length;n++){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_PRECEDING){return i}}};var G=function(e,t){var r=re().querySelectorAll(t);for(var n=r.length-1;n>=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ue(e,t){if(t){return W(e,t)[0]}else{return W(re().body,e)[0]}}function s(e){if(L(e,"String")){return C(e)}else{return e}}function J(e,t,r){if(A(t)){return{target:re().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function Z(t,r,n){Dr(function(){var e=J(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=A(r);return e?r:n}function K(t,r,n){Dr(function(){var e=J(t,r,n);e.target.removeEventListener(e.event,e.listener)});return A(r)?r:n}var ve=re().createElement("output");function Y(e,t){var r=ne(e,t);if(r){if(r==="this"){return[de(e,t)]}else{var n=W(e,r);if(n.length===0){x('The selector "'+r+'" on '+t+" returned no matches!");return[ve]}else{return n}}}}function de(e,t){return c(e,function(e){return te(e,t)!=null})}function ge(e){var t=ne(e,"hx-target");if(t){if(t==="this"){return de(e,"hx-target")}else{return ue(e,t)}}else{var r=ae(e);if(r.boosted){return re().body}else{return e}}}function me(e){var t=Q.config.attributesToSettle;for(var r=0;r<t.length;r++){if(e===t[r]){return true}}return false}function pe(t,r){oe(t.attributes,function(e){if(!r.hasAttribute(e.name)&&me(e.name)){t.removeAttribute(e.name)}});oe(r.attributes,function(e){if(me(e.name)){t.setAttribute(e.name,e.value)}})}function ye(e,t){var r=Mr(t);for(var n=0;n<r.length;n++){var i=r[n];try{if(i.isInlineSwap(e)){return true}}catch(e){x(e)}}return e==="outerHTML"}function xe(e,i,a){var t="#"+ee(i,"id");var o="outerHTML";if(e==="true"){}else if(e.indexOf(":")>0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=re().querySelectorAll(t);if(r){oe(r,function(e){var t;var r=i.cloneNode(true);t=re().createDocumentFragment();t.appendChild(r);if(!ye(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ce(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){De(o,e,e,t,a)}oe(a.elts,function(e){ce(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);fe(re().body,"htmx:oobErrorNoTarget",{content:i})}return e}function be(e,t,r){var n=ne(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e<i.length;e++){var a=i[e].split(":",2);var o=a[0].trim();if(o.indexOf("#")===0){o=o.substring(1)}var s=a[1]||"true";var l=t.querySelector("#"+o);if(l){xe(s,l,r)}}}oe(f(t,"[hx-swap-oob], [data-hx-swap-oob]"),function(e){var t=te(e,"hx-swap-oob");if(t!=null){xe(t,e,r)}})}function we(e){oe(f(e,"[hx-preserve], [data-hx-preserve]"),function(e){var t=te(e,"id");var r=re().getElementById(t);if(r!=null){e.parentNode.replaceChild(r,e)}})}function Se(o,e,s){oe(e.querySelectorAll("[id]"),function(e){var t=ee(e,"id");if(t&&t.length>0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();pe(e,i);s.tasks.push(function(){pe(e,a)})}}})}function Ee(e){return function(){n(e,Q.config.addedClass);Bt(e);Ot(e);Ce(e);ce(e,"htmx:load")}}function Ce(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Se(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;F(i,Q.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Ee(i))}}}function Te(e,t){var r=0;while(r<e.length){t=(t<<5)-t+e.charCodeAt(r++)|0}return t}function Re(e){var t=0;if(e.attributes){for(var r=0;r<e.attributes.length;r++){var n=e.attributes[r];if(n.value){t=Te(n.name,t);t=Te(n.value,t)}}}return t}function Oe(t){var r=ae(t);if(r.onHandlers){for(let e=0;e<r.onHandlers.length;e++){const n=r.onHandlers[e];t.removeEventListener(n.event,n.listener)}delete r.onHandlers}}function qe(e){var t=ae(e);if(t.timeout){clearTimeout(t.timeout)}if(t.webSocket){t.webSocket.close()}if(t.sseEventSource){t.sseEventSource.close()}if(t.listenerInfos){oe(t.listenerInfos,function(e){if(e.on){e.on.removeEventListener(e.trigger,e.listener)}})}if(t.initHash){t.initHash=null}Oe(e)}function m(e){ce(e,"htmx:beforeCleanupElement");qe(e);if(e.children){oe(e.children,function(e){m(e)})}}function He(t,e,r){if(t.tagName==="BODY"){return Pe(t,e,r)}else{var n;var i=t.previousSibling;a(u(t),t,e,r);if(i==null){n=u(t).firstChild}else{n=i.nextSibling}ae(t).replacedWith=n;r.elts=r.elts.filter(function(e){return e!=t});while(n&&n!==t){if(n.nodeType===Node.ELEMENT_NODE){r.elts.push(n)}n=n.nextElementSibling}m(t);u(t).removeChild(t)}}function Le(e,t,r){return a(e,e.firstChild,t,r)}function Ae(e,t,r){return a(u(e),e,t,r)}function Ne(e,t,r){return a(e,null,t,r)}function Ie(e,t,r){return a(u(e),e.nextSibling,t,r)}function ke(e,t,r){m(e);return u(e).removeChild(e)}function Pe(e,t,r){var n=e.firstChild;a(e,n,t,r);if(n){while(n.nextSibling){m(n.nextSibling);e.removeChild(n.nextSibling)}m(n);e.removeChild(n)}}function Me(e,t,r){var n=r||ne(e,"hx-select");if(n){var i=re().createDocumentFragment();oe(t.querySelectorAll(n),function(e){i.appendChild(e)});t=i}return t}function De(e,t,r,n,i){switch(e){case"none":return;case"outerHTML":He(r,n,i);return;case"afterbegin":Le(r,n,i);return;case"beforebegin":Ae(r,n,i);return;case"beforeend":Ne(r,n,i);return;case"afterend":Ie(r,n,i);return;case"delete":ke(r,n,i);return;default:var a=Mr(t);for(var o=0;o<a.length;o++){var s=a[o];try{var l=s.handleSwap(e,r,n,i);if(l){if(typeof l.length!=="undefined"){for(var u=0;u<l.length;u++){var f=l[u];if(f.nodeType!==Node.TEXT_NODE&&f.nodeType!==Node.COMMENT_NODE){i.tasks.push(Ee(f))}}}return}}catch(e){x(e)}}if(e==="innerHTML"){Pe(r,n,i)}else{De(Q.config.defaultSwapStyle,t,r,n,i)}}}function Xe(e){if(e.indexOf("<title")>-1){var t=e.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");var r=t.match(/<title(\s[^>]*>|>)([\s\S]*?)<\/title>/im);if(r){return r[2]}}}function Ue(e,t,r,n,i,a){i.title=Xe(n);var o=l(n);if(o){be(r,o,i);o=Me(r,o,a);we(o);return De(e,r,t,o,i)}}function Be(e,t,r){var n=e.getResponseHeader(t);if(n.indexOf("{")===0){var i=E(n);for(var a in i){if(i.hasOwnProperty(a)){var o=i[a];if(!N(o)){o={value:o}}ce(r,a,o)}}}else{var s=n.split(",");for(var l=0;l<s.length;l++){ce(r,s[l].trim(),[])}}}var Fe=/\s/;var p=/[\s,]/;var Ve=/[_$a-zA-Z]/;var je=/[_$a-zA-Z0-9]/;var _e=['"',"'","/"];var ze=/[^\s]/;var We=/[{(]/;var $e=/[})]/;function Ge(e){var t=[];var r=0;while(r<e.length){if(Ve.exec(e.charAt(r))){var n=r;while(je.exec(e.charAt(r+1))){r++}t.push(e.substr(n,r-n+1))}else if(_e.indexOf(e.charAt(r))!==-1){var i=e.charAt(r);var n=r;r++;while(r<e.length&&e.charAt(r)!==i){if(e.charAt(r)==="\\"){r++}r++}t.push(e.substr(n,r-n+1))}else{var a=e.charAt(r);t.push(a)}r++}return t}function Je(e,t,r){return Ve.exec(e.charAt(0))&&e!=="true"&&e!=="false"&&e!=="this"&&e!==r&&t!=="."}function Ze(e,t,r){if(t[0]==="["){t.shift();var n=1;var i=" return (function("+r+"){ return (";var a=null;while(t.length>0){var o=t[0];if(o==="]"){n--;if(n===0){if(a===null){i=i+"true"}t.shift();i+=")})";try{var s=wr(e,function(){return Function(i)()},function(){return true});s.source=i;return s}catch(e){fe(re().body,"htmx:syntax:error",{error:e,source:i});return null}}}else if(o==="["){n++}if(Je(o,a,r)){i+="(("+r+"."+o+") ? ("+r+"."+o+") : (window."+o+"))"}else{i=i+o}a=t.shift()}}}function y(e,t){var r="";while(e.length>0&&!e[0].match(t)){r+=e.shift()}return r}function Ke(e){var t;if(e.length>0&&We.test(e[0])){e.shift();t=y(e,$e).trim();e.shift()}else{t=y(e,p)}return t}var Ye="input, textarea, select";function Qe(e){var t=te(e,"hx-trigger");var r=[];if(t){var n=Ge(t);do{y(n,ze);var i=n.length;var a=y(n,/[,\[\s]/);if(a!==""){if(a==="every"){var o={trigger:"every"};y(n,ze);o.pollInterval=d(y(n,/[,\[\s]/));y(n,ze);var s=Ze(e,n,"event");if(s){o.eventFilter=s}r.push(o)}else if(a.indexOf("sse:")===0){r.push({trigger:"sse",sseEvent:a.substr(4)})}else{var l={trigger:a};var s=Ze(e,n,"event");if(s){l.eventFilter=s}while(n.length>0&&n[0]!==","){y(n,ze);var u=n.shift();if(u==="changed"){l.changed=true}else if(u==="once"){l.once=true}else if(u==="consume"){l.consume=true}else if(u==="delay"&&n[0]===":"){n.shift();l.delay=d(y(n,p))}else if(u==="from"&&n[0]===":"){n.shift();if(We.test(n[0])){var f=Ke(n)}else{var f=y(n,p);if(f==="closest"||f==="find"||f==="next"||f==="previous"){n.shift();var c=Ke(n);if(c.length>0){f+=" "+c}}}l.from=f}else if(u==="target"&&n[0]===":"){n.shift();l.target=Ke(n)}else if(u==="throttle"&&n[0]===":"){n.shift();l.throttle=d(y(n,p))}else if(u==="queue"&&n[0]===":"){n.shift();l.queue=y(n,p)}else if(u==="root"&&n[0]===":"){n.shift();l[u]=Ke(n)}else if(u==="threshold"&&n[0]===":"){n.shift();l[u]=y(n,p)}else{fe(e,"htmx:syntax:error",{token:n.shift()})}}r.push(l)}}if(n.length===i){fe(e,"htmx:syntax:error",{token:n.shift()})}y(n,ze)}while(n[0]===","&&n.shift())}if(r.length>0){return r}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,Ye)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function et(e){ae(e).cancelled=true}function tt(e,t,r){var n=ae(e);n.timeout=setTimeout(function(){if(se(e)&&n.cancelled!==true){if(!ot(r,e,Vt("hx:poll:trigger",{triggerSpec:r,target:e}))){t(e)}tt(e,t,r)}},r.pollInterval)}function rt(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function nt(t,r,e){if(t.tagName==="A"&&rt(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"){r.boosted=true;var n,i;if(t.tagName==="A"){n="get";i=ee(t,"href")}else{var a=ee(t,"method");n=a?a.toLowerCase():"get";if(n==="get"){}i=ee(t,"action")}e.forEach(function(e){st(t,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(n,i,e,t)},r,e,true)})}}function it(e,t){if(e.type==="submit"||e.type==="click"){if(t.tagName==="FORM"){return true}if(h(t,'input[type="submit"], button')&&v(t,"form")!==null){return true}if(t.tagName==="A"&&t.href&&(t.getAttribute("href")==="#"||t.getAttribute("href").indexOf("#")!==0)){return true}}return false}function at(e,t){return ae(e).boosted&&e.tagName==="A"&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function ot(e,t,r){var n=e.eventFilter;if(n){try{return n.call(t,r)!==true}catch(e){fe(re().body,"htmx:eventFilter:error",{error:e,source:n.source});return true}}return false}function st(a,o,e,s,l){var u=ae(a);var t;if(s.from){t=W(a,s.from)}else{t=[a]}if(s.changed){t.forEach(function(e){var t=ae(e);t.lastValue=e.value})}oe(t,function(n){var i=function(e){if(!se(a)){n.removeEventListener(s.trigger,i);return}if(at(a,e)){return}if(l||it(e,a)){e.preventDefault()}if(ot(s,a,e)){return}var t=ae(e);t.triggerSpec=s;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(a)<0){t.handledFor.push(a);if(s.consume){e.stopPropagation()}if(s.target&&e.target){if(!h(e.target,s.target)){return}}if(s.once){if(u.triggeredOnce){return}else{u.triggeredOnce=true}}if(s.changed){var r=ae(n);if(r.lastValue===n.value){return}r.lastValue=n.value}if(u.delayed){clearTimeout(u.delayed)}if(u.throttle){return}if(s.throttle){if(!u.throttle){o(a,e);u.throttle=setTimeout(function(){u.throttle=null},s.throttle)}}else if(s.delay){u.delayed=setTimeout(function(){o(a,e)},s.delay)}else{ce(a,"htmx:trigger");o(a,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:s.trigger,listener:i,on:n});n.addEventListener(s.trigger,i)})}var lt=false;var ut=null;function ft(){if(!ut){ut=function(){lt=true};window.addEventListener("scroll",ut);setInterval(function(){if(lt){lt=false;oe(re().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"),function(e){ct(e)})}},200)}}function ct(t){if(!o(t,"data-hx-revealed")&&k(t)){t.setAttribute("data-hx-revealed","true");var e=ae(t);if(e.initHash){ce(t,"revealed")}else{t.addEventListener("htmx:afterProcessNode",function(e){ce(t,"revealed")},{once:true})}}}function ht(e,t,r){var n=P(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){vt(e,a[1],0)}if(a[0]==="send"){gt(e)}}}function vt(s,r,n){if(!se(s)){return}if(r.indexOf("/")==0){var e=location.hostname+(location.port?":"+location.port:"");if(location.protocol=="https:"){r="wss://"+e+r}else if(location.protocol=="http:"){r="ws://"+e+r}}var t=Q.createWebSocket(r);t.onerror=function(e){fe(s,"htmx:wsError",{error:e,socket:t});dt(s)};t.onclose=function(e){if([1006,1012,1013].indexOf(e.code)>=0){var t=mt(n);setTimeout(function(){vt(s,r,n+1)},t)}};t.onopen=function(e){n=0};ae(s).webSocket=t;t.addEventListener("message",function(e){if(dt(s)){return}var t=e.data;T(s,function(e){t=e.transformResponse(t,null,s)});var r=R(s);var n=l(t);var i=I(n.children);for(var a=0;a<i.length;a++){var o=i[a];xe(te(o,"hx-swap-oob")||"true",o,r)}Yt(r.tasks)})}function dt(e){if(!se(e)){ae(e).webSocket.close();return true}}function gt(u){var f=c(u,function(e){return ae(e).webSocket!=null});if(f){u.addEventListener(Qe(u)[0].trigger,function(e){var t=ae(f).webSocket;var r=vr(u,f);var n=ur(u,"post");var i=n.errors;var a=n.values;var o=Cr(u);var s=le(a,o);var l=dr(s,u);l["HEADERS"]=r;if(i&&i.length>0){ce(u,"htmx:validation:halted",i);return}t.send(JSON.stringify(l));if(it(e,u)){e.preventDefault()}})}else{fe(u,"htmx:noWebSocketSourceError")}}function mt(e){var t=Q.config.wsReconnectDelay;if(typeof t==="function"){return t(e)}if(t==="full-jitter"){var r=Math.min(e,6);var n=1e3*Math.pow(2,r);return n*Math.random()}x('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')}function pt(e,t,r){var n=P(r);for(var i=0;i<n.length;i++){var a=n[i].split(/:(.+)/);if(a[0]==="connect"){yt(e,a[1])}if(a[0]==="swap"){xt(e,a[1])}}}function yt(t,e){var r=Q.createEventSource(e);r.onerror=function(e){fe(t,"htmx:sseError",{error:e,source:r});wt(t)};ae(t).sseEventSource=r}function xt(a,o){var s=c(a,St);if(s){var l=ae(s).sseEventSource;var u=function(e){if(wt(s)){return}if(!se(a)){l.removeEventListener(o,u);return}var t=e.data;T(a,function(e){t=e.transformResponse(t,null,a)});var r=mr(a);var n=ge(a);var i=R(a);Ue(r.swapStyle,n,a,t,i);Yt(i.tasks);ce(a,"htmx:sseMessage",e)};ae(a).sseListener=u;l.addEventListener(o,u)}else{fe(a,"htmx:noSSESourceError")}}function bt(e,t,r){var n=c(e,St);if(n){var i=ae(n).sseEventSource;var a=function(){if(!wt(n)){if(se(e)){t(e)}else{i.removeEventListener(r,a)}}};ae(e).sseListener=a;i.addEventListener(r,a)}else{fe(e,"htmx:noSSESourceError")}}function wt(e){if(!se(e)){ae(e).sseEventSource.close();return true}}function St(e){return ae(e).sseEventSource!=null}function Et(e,t,r,n){var i=function(){if(!r.loaded){r.loaded=true;t(e)}};if(n){setTimeout(i,n)}else{i()}}function Ct(t,i,e){var a=false;oe(b,function(r){if(o(t,"hx-"+r)){var n=te(t,"hx-"+r);a=true;i.path=n;i.verb=r;e.forEach(function(e){Tt(t,e,i,function(e,t){if(v(e,Q.config.disableSelector)){m(e);return}he(r,n,e,t)})})}});return a}function Tt(n,e,t,r){if(e.sseEvent){bt(n,r,e.sseEvent)}else if(e.trigger==="revealed"){ft();st(n,r,t,e);ct(n)}else if(e.trigger==="intersect"){var i={};if(e.root){i.root=ue(n,e.root)}if(e.threshold){i.threshold=parseFloat(e.threshold)}var a=new IntersectionObserver(function(e){for(var t=0;t<e.length;t++){var r=e[t];if(r.isIntersecting){ce(n,"intersect");break}}},i);a.observe(n);st(n,r,t,e)}else if(e.trigger==="load"){if(!ot(e,n,Vt("load",{elt:n}))){Et(n,r,t,e.delay)}}else if(e.pollInterval){t.polling=true;tt(n,r,e)}else{st(n,r,t,e)}}function Rt(e){if(Q.config.allowScriptTags&&(e.type==="text/javascript"||e.type==="module"||e.type==="")){var t=re().createElement("script");oe(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}var r=e.parentElement;try{r.insertBefore(t,e)}catch(e){x(e)}finally{if(e.parentElement){e.parentElement.removeChild(e)}}}}function Ot(e){if(h(e,"script")){Rt(e)}oe(f(e,"script"),function(e){Rt(e)})}function qt(){return document.querySelector("[hx-boost], [data-hx-boost]")}function Ht(e){var t=null;var r=[];if(document.evaluate){var n=document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]',e);while(t=n.iterateNext())r.push(t)}else{var i=document.getElementsByTagName("*");for(var a=0;a<i.length;a++){var o=i[a].attributes;for(var s=0;s<o.length;s++){var l=o[s].name;if(g(l,"hx-on:")||g(l,"data-hx-on:")){r.push(i[a])}}}}return r}function Lt(e){if(e.querySelectorAll){var t=qt()?", a":"";var r=e.querySelectorAll(w+t+", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws],"+" [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");return r}else{return[]}}function At(e){var t=v(e.target,"button, input[type='submit']");var r=It(e);if(r){r.lastButtonClicked=t}}function Nt(e){var t=It(e);if(t){t.lastButtonClicked=null}}function It(e){var t=v(e.target,"button, input[type='submit']");if(!t){return}var r=s("#"+ee(t,"form"))||v(t,"form");if(!r){return}return ae(r)}function kt(e){e.addEventListener("click",At);e.addEventListener("focusin",At);e.addEventListener("focusout",Nt)}function Pt(e){var t=Ge(e);var r=0;for(let e=0;e<t.length;e++){const n=t[e];if(n==="{"){r++}else if(n==="}"){r--}}return r}function Mt(t,e,r){var n=ae(t);if(!Array.isArray(n.onHandlers)){n.onHandlers=[]}var i;var a=function(e){return wr(t,function(){if(!i){i=new Function("event",r)}i.call(t,e)})};t.addEventListener(e,a);n.onHandlers.push({event:e,listener:a})}function Dt(e){var t=te(e,"hx-on");if(t){var r={};var n=t.split("\n");var i=null;var a=0;while(n.length>0){var o=n.shift();var s=o.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);if(a===0&&s){o.split(":");i=s[1].slice(0,-1);r[i]=s[2]}else{r[i]+=o}a+=Pt(o)}for(var l in r){Mt(e,l,r[l])}}}function Xt(t){Oe(t);for(var e=0;e<t.attributes.length;e++){var r=t.attributes[e].name;var n=t.attributes[e].value;if(g(r,"hx-on:")||g(r,"data-hx-on:")){let e=r.slice(r.indexOf(":")+1);if(g(e,":"))e="htmx"+e;Mt(t,e,n)}}}function Ut(t){if(v(t,Q.config.disableSelector)){m(t);return}var r=ae(t);if(r.initHash!==Re(t)){qe(t);r.initHash=Re(t);Dt(t);ce(t,"htmx:beforeProcessNode");if(t.value){r.lastValue=t.value}var e=Qe(t);var n=Ct(t,r,e);if(!n){if(ne(t,"hx-boost")==="true"){nt(t,r,e)}else if(o(t,"hx-trigger")){e.forEach(function(e){Tt(t,e,r,function(){})})}}if(t.tagName==="FORM"||ee(t,"type")==="submit"&&o(t,"form")){kt(t)}var i=te(t,"hx-sse");if(i){pt(t,r,i)}var a=te(t,"hx-ws");if(a){ht(t,r,a)}ce(t,"htmx:afterProcessNode")}}function Bt(e){e=s(e);if(v(e,Q.config.disableSelector)){m(e);return}Ut(e);oe(Lt(e),function(e){Ut(e)});oe(Ht(e),Xt)}function Ft(e){return e.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()}function Vt(e,t){var r;if(window.CustomEvent&&typeof window.CustomEvent==="function"){r=new CustomEvent(e,{bubbles:true,cancelable:true,detail:t})}else{r=re().createEvent("CustomEvent");r.initCustomEvent(e,true,true,t)}return r}function fe(e,t,r){ce(e,t,le({error:t},r))}function jt(e){return e==="htmx:afterProcessNode"}function T(e,t){oe(Mr(e),function(e){try{t(e)}catch(e){x(e)}})}function x(e){if(console.error){console.error(e)}else if(console.log){console.log("ERROR: ",e)}}function ce(e,t,r){e=s(e);if(r==null){r={}}r["elt"]=e;var n=Vt(t,r);if(Q.logger&&!jt(t)){Q.logger(e,t,r)}if(r.error){x(r.error);ce(e,"htmx:error",{errorInfo:r})}var i=e.dispatchEvent(n);var a=Ft(t);if(i&&a!==t){var o=Vt(a,n.detail);i=i&&e.dispatchEvent(o)}T(e,function(e){i=i&&(e.onEvent(t,n)!==false&&!n.defaultPrevented)});return i}var _t=location.pathname+location.search;function zt(){var e=re().querySelector("[hx-history-elt],[data-hx-history-elt]");return e||re().body}function Wt(e,t,r,n){if(!M()){return}if(Q.config.historyCacheSize<=0){localStorage.removeItem("htmx-history-cache");return}e=D(e);var i=E(localStorage.getItem("htmx-history-cache"))||[];for(var a=0;a<i.length;a++){if(i[a].url===e){i.splice(a,1);break}}var o={url:e,content:t,title:r,scroll:n};ce(re().body,"htmx:historyItemCreated",{item:o,cache:i});i.push(o);while(i.length>Q.config.historyCacheSize){i.shift()}while(i.length>0){try{localStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(re().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function $t(e){if(!M()){return null}e=D(e);var t=E(localStorage.getItem("htmx-history-cache"))||[];for(var r=0;r<t.length;r++){if(t[r].url===e){return t[r]}}return null}function Gt(e){var t=Q.config.requestClass;var r=e.cloneNode(true);oe(f(r,"."+t),function(e){n(e,t)});return r.innerHTML}function Jt(){var e=zt();var t=_t||location.pathname+location.search;var r;try{r=re().querySelector('[hx-history="false" i],[data-hx-history="false" i]')}catch(e){r=re().querySelector('[hx-history="false"],[data-hx-history="false"]')}if(!r){ce(re().body,"htmx:beforeHistorySave",{path:t,historyElt:e});Wt(t,Gt(e),re().title,window.scrollY)}if(Q.config.historyEnabled)history.replaceState({htmx:true},re().title,window.location.href)}function Zt(e){if(Q.config.getCacheBusterParam){e=e.replace(/org\.htmx\.cache-buster=[^&]*&?/,"");if(_(e,"&")||_(e,"?")){e=e.slice(0,-1)}}if(Q.config.historyEnabled){history.pushState({htmx:true},"",e)}_t=e}function Kt(e){if(Q.config.historyEnabled)history.replaceState({htmx:true},"",e);_t=e}function Yt(e){oe(e,function(e){e.call()})}function Qt(a){var e=new XMLHttpRequest;var o={path:a,xhr:e};ce(re().body,"htmx:historyCacheMiss",o);e.open("GET",a,true);e.setRequestHeader("HX-History-Restore-Request","true");e.onload=function(){if(this.status>=200&&this.status<400){ce(re().body,"htmx:historyCacheMissLoad",o);var e=l(this.response);e=e.querySelector("[hx-history-elt],[data-hx-history-elt]")||e;var t=zt();var r=R(t);var n=Xe(this.response);if(n){var i=C("title");if(i){i.innerHTML=n}else{window.document.title=n}}Pe(t,e,r);Yt(r.tasks);_t=a;ce(re().body,"htmx:historyRestore",{path:a,cacheMiss:true,serverResponse:this.response})}else{fe(re().body,"htmx:historyCacheMissLoadError",o)}};e.send()}function er(e){Jt();e=e||location.pathname+location.search;var t=$t(e);if(t){var r=l(t.content);var n=zt();var i=R(n);Pe(n,r,i);Yt(i.tasks);document.title=t.title;setTimeout(function(){window.scrollTo(0,t.scroll)},0);_t=e;ce(re().body,"htmx:historyRestore",{path:e,item:t})}else{if(Q.config.refreshOnHistoryMiss){window.location.reload(true)}else{Qt(e)}}}function tr(e){var t=Y(e,"hx-indicator");if(t==null){t=[e]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.classList["add"].call(e.classList,Q.config.requestClass)});return t}function rr(e){var t=Y(e,"hx-disabled-elt");if(t==null){t=[]}oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","")});return t}function nr(e,t){oe(e,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.classList["remove"].call(e.classList,Q.config.requestClass)}});oe(t,function(e){var t=ae(e);t.requestCount=(t.requestCount||0)-1;if(t.requestCount===0){e.removeAttribute("disabled")}})}function ir(e,t){for(var r=0;r<e.length;r++){var n=e[r];if(n.isSameNode(t)){return true}}return false}function ar(e){if(e.name===""||e.name==null||e.disabled){return false}if(e.type==="button"||e.type==="submit"||e.tagName==="image"||e.tagName==="reset"||e.tagName==="file"){return false}if(e.type==="checkbox"||e.type==="radio"){return e.checked}return true}function or(e,t,r){if(e!=null&&t!=null){var n=r[e];if(n===undefined){r[e]=t}else if(Array.isArray(n)){if(Array.isArray(t)){r[e]=n.concat(t)}else{n.push(t)}}else{if(Array.isArray(t)){r[e]=[n].concat(t)}else{r[e]=[n,t]}}}}function sr(t,r,n,e,i){if(e==null||ir(t,e)){return}else{t.push(e)}if(ar(e)){var a=ee(e,"name");var o=e.value;if(e.multiple&&e.tagName==="SELECT"){o=I(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e.files){o=I(e.files)}or(a,o,r);if(i){lr(e,n)}}if(h(e,"form")){var s=e.elements;oe(s,function(e){sr(t,r,n,e,i)})}}function lr(e,t){if(e.willValidate){ce(e,"htmx:validation:validate");if(!e.checkValidity()){t.push({elt:e,message:e.validationMessage,validity:e.validity});ce(e,"htmx:validation:failed",{message:e.validationMessage,validity:e.validity})}}}function ur(e,t){var r=[];var n={};var i={};var a=[];var o=ae(e);if(o.lastButtonClicked&&!se(o.lastButtonClicked)){o.lastButtonClicked=null}var s=h(e,"form")&&e.noValidate!==true||te(e,"hx-validate")==="true";if(o.lastButtonClicked){s=s&&o.lastButtonClicked.formNoValidate!==true}if(t!=="get"){sr(r,i,a,v(e,"form"),s)}sr(r,n,a,e,s);if(o.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){var l=o.lastButtonClicked||e;var u=ee(l,"name");or(u,l.value,i)}var f=Y(e,"hx-include");oe(f,function(e){sr(r,n,a,e,s);if(!h(e,"form")){oe(e.querySelectorAll(Ye),function(e){sr(r,n,a,e,s)})}});n=le(n,i);return{errors:a,values:n}}function fr(e,t,r){if(e!==""){e+="&"}if(String(r)==="[object Object]"){r=JSON.stringify(r)}var n=encodeURIComponent(r);e+=encodeURIComponent(t)+"="+n;return e}function cr(e){var t="";for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){oe(n,function(e){t=fr(t,r,e)})}else{t=fr(t,r,n)}}}return t}function hr(e){var t=new FormData;for(var r in e){if(e.hasOwnProperty(r)){var n=e[r];if(Array.isArray(n)){oe(n,function(e){t.append(r,e)})}else{t.append(r,n)}}}return t}function vr(e,t,r){var n={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":te(t,"id"),"HX-Current-URL":re().location.href};br(e,"hx-headers",false,n);if(r!==undefined){n["HX-Prompt"]=r}if(ae(e).boosted){n["HX-Boosted"]="true"}return n}function dr(t,e){var r=ne(e,"hx-params");if(r){if(r==="none"){return{}}else if(r==="*"){return t}else if(r.indexOf("not ")===0){oe(r.substr(4).split(","),function(e){e=e.trim();delete t[e]});return t}else{var n={};oe(r.split(","),function(e){e=e.trim();n[e]=t[e]});return n}}else{return t}}function gr(e){return ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function mr(e,t){var r=t?t:ne(e,"hx-swap");var n={swapStyle:ae(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&ae(e).boosted&&!gr(e)){n["show"]="top"}if(r){var i=P(r);if(i.length>0){for(var a=0;a<i.length;a++){var o=i[a];if(o.indexOf("swap:")===0){n["swapDelay"]=d(o.substr(5))}else if(o.indexOf("settle:")===0){n["settleDelay"]=d(o.substr(7))}else if(o.indexOf("transition:")===0){n["transition"]=o.substr(11)==="true"}else if(o.indexOf("ignoreTitle:")===0){n["ignoreTitle"]=o.substr(12)==="true"}else if(o.indexOf("scroll:")===0){var s=o.substr(7);var l=s.split(":");var u=l.pop();var f=l.length>0?l.join(":"):null;n["scroll"]=u;n["scrollTarget"]=f}else if(o.indexOf("show:")===0){var c=o.substr(5);var l=c.split(":");var h=l.pop();var f=l.length>0?l.join(":"):null;n["show"]=h;n["showTarget"]=f}else if(o.indexOf("focus-scroll:")===0){var v=o.substr("focus-scroll:".length);n["focusScroll"]=v=="true"}else if(a==0){n["swapStyle"]=o}else{x("Unknown modifier in hx-swap: "+o)}}}}return n}function pr(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function yr(t,r,n){var i=null;T(r,function(e){if(i==null){i=e.encodeParameters(t,n,r)}});if(i!=null){return i}else{if(pr(r)){return hr(n)}else{return cr(n)}}}function R(e){return{tasks:[],elts:[e]}}function xr(e,t){var r=e[0];var n=e[e.length-1];if(t.scroll){var i=null;if(t.scrollTarget){i=ue(r,t.scrollTarget)}if(t.scroll==="top"&&(r||i)){i=i||r;i.scrollTop=0}if(t.scroll==="bottom"&&(n||i)){i=i||n;i.scrollTop=i.scrollHeight}}if(t.show){var i=null;if(t.showTarget){var a=t.showTarget;if(t.showTarget==="window"){a="body"}i=ue(r,a)}if(t.show==="top"&&(r||i)){i=i||r;i.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(n||i)){i=i||n;i.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function br(e,t,r,n){if(n==null){n={}}if(e==null){return n}var i=te(e,t);if(i){var a=i.trim();var o=r;if(a==="unset"){return null}if(a.indexOf("javascript:")===0){a=a.substr(11);o=true}else if(a.indexOf("js:")===0){a=a.substr(3);o=true}if(a.indexOf("{")!==0){a="{"+a+"}"}var s;if(o){s=wr(e,function(){return Function("return ("+a+")")()},{})}else{s=E(a)}for(var l in s){if(s.hasOwnProperty(l)){if(n[l]==null){n[l]=s[l]}}}}return br(u(e),t,r,n)}function wr(e,t,r){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return r}}function Sr(e,t){return br(e,"hx-vars",true,t)}function Er(e,t){return br(e,"hx-vals",false,t)}function Cr(e){return le(Sr(e),Er(e))}function Tr(t,r,n){if(n!==null){try{t.setRequestHeader(r,n)}catch(e){t.setRequestHeader(r,encodeURIComponent(n));t.setRequestHeader(r+"-URI-AutoEncoded","true")}}}function Rr(t){if(t.responseURL&&typeof URL!=="undefined"){try{var e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(re().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function O(e,t){return e.getAllResponseHeaders().match(t)}function Or(e,t,r){e=e.toLowerCase();if(r){if(r instanceof Element||L(r,"String")){return he(e,t,null,null,{targetOverride:s(r),returnPromise:true})}else{return he(e,t,s(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:s(r.target),swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(e,t,null,null,{returnPromise:true})}}function qr(e){var t=[];while(e){t.push(e);e=e.parentElement}return t}function Hr(e,t,r){var n;var i;if(typeof URL==="function"){i=new URL(t,document.location.href);var a=document.location.origin;n=a===i.origin}else{i=t;n=g(t,document.location.origin)}if(Q.config.selfRequestsOnly){if(!n){return false}}return ce(e,"htmx:validateUrl",le({url:i,sameHost:n},r))}function he(t,r,n,i,a,e){var o=null;var s=null;a=a!=null?a:{};if(a.returnPromise&&typeof Promise!=="undefined"){var l=new Promise(function(e,t){o=e;s=t})}if(n==null){n=re().body}var M=a.handler||Ar;var D=a.select||null;if(!se(n)){ie(o);return l}var u=a.targetOverride||ge(n);if(u==null||u==ve){fe(n,"htmx:targetError",{target:te(n,"hx-target")});ie(s);return l}var f=ae(n);var c=f.lastButtonClicked;if(c){var h=ee(c,"formaction");if(h!=null){r=h}var v=ee(c,"formmethod");if(v!=null){if(v.toLowerCase()!=="dialog"){t=v}}}var d=ne(n,"hx-confirm");if(e===undefined){var X=function(e){return he(t,r,n,i,a,!!e)};var U={target:u,elt:n,path:r,verb:t,triggeringEvent:i,etc:a,issueRequest:X,question:d};if(ce(n,"htmx:confirm",U)===false){ie(o);return l}}var g=n;var m=ne(n,"hx-sync");var p=null;var y=false;if(m){var B=m.split(":");var F=B[0].trim();if(F==="this"){g=de(n,"hx-sync")}else{g=ue(n,F)}m=(B[1]||"drop").trim();f=ae(g);if(m==="drop"&&f.xhr&&f.abortable!==true){ie(o);return l}else if(m==="abort"){if(f.xhr){ie(o);return l}else{y=true}}else if(m==="replace"){ce(g,"htmx:abort")}else if(m.indexOf("queue")===0){var V=m.split(" ");p=(V[1]||"last").trim()}}if(f.xhr){if(f.abortable){ce(g,"htmx:abort")}else{if(p==null){if(i){var x=ae(i);if(x&&x.triggerSpec&&x.triggerSpec.queue){p=x.triggerSpec.queue}}if(p==null){p="last"}}if(f.queuedRequests==null){f.queuedRequests=[]}if(p==="first"&&f.queuedRequests.length===0){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(p==="all"){f.queuedRequests.push(function(){he(t,r,n,i,a)})}else if(p==="last"){f.queuedRequests=[];f.queuedRequests.push(function(){he(t,r,n,i,a)})}ie(o);return l}}var b=new XMLHttpRequest;f.xhr=b;f.abortable=y;var w=function(){f.xhr=null;f.abortable=false;if(f.queuedRequests!=null&&f.queuedRequests.length>0){var e=f.queuedRequests.shift();e()}};var j=ne(n,"hx-prompt");if(j){var S=prompt(j);if(S===null||!ce(n,"htmx:prompt",{prompt:S,target:u})){ie(o);w();return l}}if(d&&!e){if(!confirm(d)){ie(o);w();return l}}var E=vr(n,u,S);if(t!=="get"&&!pr(n)){E["Content-Type"]="application/x-www-form-urlencoded"}if(a.headers){E=le(E,a.headers)}var _=ur(n,t);var C=_.errors;var T=_.values;if(a.values){T=le(T,a.values)}var z=Cr(n);var W=le(T,z);var R=dr(W,n);if(Q.config.getCacheBusterParam&&t==="get"){R["org.htmx.cache-buster"]=ee(u,"id")||"true"}if(r==null||r===""){r=re().location.href}var O=br(n,"hx-request");var $=ae(n).boosted;var q=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;var H={boosted:$,useUrlParams:q,parameters:R,unfilteredParameters:W,headers:E,target:u,verb:t,errors:C,withCredentials:a.credentials||O.credentials||Q.config.withCredentials,timeout:a.timeout||O.timeout||Q.config.timeout,path:r,triggeringEvent:i};if(!ce(n,"htmx:configRequest",H)){ie(o);w();return l}r=H.path;t=H.verb;E=H.headers;R=H.parameters;C=H.errors;q=H.useUrlParams;if(C&&C.length>0){ce(n,"htmx:validation:halted",H);ie(o);w();return l}var G=r.split("#");var J=G[0];var L=G[1];var A=r;if(q){A=J;var Z=Object.keys(R).length!==0;if(Z){if(A.indexOf("?")<0){A+="?"}else{A+="&"}A+=cr(R);if(L){A+="#"+L}}}if(!Hr(n,A,H)){fe(n,"htmx:invalidPath",H);ie(s);return l}b.open(t.toUpperCase(),A,true);b.overrideMimeType("text/html");b.withCredentials=H.withCredentials;b.timeout=H.timeout;if(O.noHeaders){}else{for(var N in E){if(E.hasOwnProperty(N)){var K=E[N];Tr(b,N,K)}}}var I={xhr:b,target:u,requestConfig:H,etc:a,boosted:$,select:D,pathInfo:{requestPath:r,finalRequestPath:A,anchor:L}};b.onload=function(){try{var e=qr(n);I.pathInfo.responsePath=Rr(b);M(n,I);nr(k,P);ce(n,"htmx:afterRequest",I);ce(n,"htmx:afterOnLoad",I);if(!se(n)){var t=null;while(e.length>0&&t==null){var r=e.shift();if(se(r)){t=r}}if(t){ce(t,"htmx:afterRequest",I);ce(t,"htmx:afterOnLoad",I)}}ie(o);w()}catch(e){fe(n,"htmx:onLoadError",le({error:e},I));throw e}};b.onerror=function(){nr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendError",I);ie(s);w()};b.onabort=function(){nr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:sendAbort",I);ie(s);w()};b.ontimeout=function(){nr(k,P);fe(n,"htmx:afterRequest",I);fe(n,"htmx:timeout",I);ie(s);w()};if(!ce(n,"htmx:beforeRequest",I)){ie(o);w();return l}var k=tr(n);var P=rr(n);oe(["loadstart","loadend","progress","abort"],function(t){oe([b,b.upload],function(e){e.addEventListener(t,function(e){ce(n,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ce(n,"htmx:beforeSend",I);var Y=q?null:yr(b,n,R);b.send(Y);return l}function Lr(e,t){var r=t.xhr;var n=null;var i=null;if(O(r,/HX-Push:/i)){n=r.getResponseHeader("HX-Push");i="push"}else if(O(r,/HX-Push-Url:/i)){n=r.getResponseHeader("HX-Push-Url");i="push"}else if(O(r,/HX-Replace-Url:/i)){n=r.getResponseHeader("HX-Replace-Url");i="replace"}if(n){if(n==="false"){return{}}else{return{type:i,path:n}}}var a=t.pathInfo.finalRequestPath;var o=t.pathInfo.responsePath;var s=ne(e,"hx-push-url");var l=ne(e,"hx-replace-url");var u=ae(e).boosted;var f=null;var c=null;if(s){f="push";c=s}else if(l){f="replace";c=l}else if(u){f="push";c=o||a}if(c){if(c==="false"){return{}}if(c==="true"){c=o||a}if(t.pathInfo.anchor&&c.indexOf("#")===-1){c=c+"#"+t.pathInfo.anchor}return{type:f,path:c}}else{return{}}}function Ar(l,u){var f=u.xhr;var c=u.target;var e=u.etc;var t=u.requestConfig;var h=u.select;if(!ce(l,"htmx:beforeOnLoad",u))return;if(O(f,/HX-Trigger:/i)){Be(f,"HX-Trigger",l)}if(O(f,/HX-Location:/i)){Jt();var r=f.getResponseHeader("HX-Location");var v;if(r.indexOf("{")===0){v=E(r);r=v["path"];delete v["path"]}Or("GET",r,v).then(function(){Zt(r)});return}var n=O(f,/HX-Refresh:/i)&&"true"===f.getResponseHeader("HX-Refresh");if(O(f,/HX-Redirect:/i)){location.href=f.getResponseHeader("HX-Redirect");n&&location.reload();return}if(n){location.reload();return}if(O(f,/HX-Retarget:/i)){u.target=re().querySelector(f.getResponseHeader("HX-Retarget"))}var d=Lr(l,u);var i=f.status>=200&&f.status<400&&f.status!==204;var g=f.response;var a=f.status>=400;var m=Q.config.ignoreTitle;var o=le({shouldSwap:i,serverResponse:g,isError:a,ignoreTitle:m},u);if(!ce(c,"htmx:beforeSwap",o))return;c=o.target;g=o.serverResponse;a=o.isError;m=o.ignoreTitle;u.target=c;u.failed=a;u.successful=!a;if(o.shouldSwap){if(f.status===286){et(l)}T(l,function(e){g=e.transformResponse(g,f,l)});if(d.type){Jt()}var s=e.swapOverride;if(O(f,/HX-Reswap:/i)){s=f.getResponseHeader("HX-Reswap")}var v=mr(l,s);if(v.hasOwnProperty("ignoreTitle")){m=v.ignoreTitle}c.classList.add(Q.config.swappingClass);var p=null;var y=null;var x=function(){try{var e=document.activeElement;var t={};try{t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null}}catch(e){}var r;if(h){r=h}if(O(f,/HX-Reselect:/i)){r=f.getResponseHeader("HX-Reselect")}if(d.type){ce(re().body,"htmx:beforeHistoryUpdate",le({history:d},u));if(d.type==="push"){Zt(d.path);ce(re().body,"htmx:pushedIntoHistory",{path:d.path})}else{Kt(d.path);ce(re().body,"htmx:replacedInHistory",{path:d.path})}}var n=R(c);Ue(v.swapStyle,c,l,g,n,r);if(t.elt&&!se(t.elt)&&ee(t.elt,"id")){var i=document.getElementById(ee(t.elt,"id"));var a={preventScroll:v.focusScroll!==undefined?!v.focusScroll:!Q.config.defaultFocusScroll};if(i){if(t.start&&i.setSelectionRange){try{i.setSelectionRange(t.start,t.end)}catch(e){}}i.focus(a)}}c.classList.remove(Q.config.swappingClass);oe(n.elts,function(e){if(e.classList){e.classList.add(Q.config.settlingClass)}ce(e,"htmx:afterSwap",u)});if(O(f,/HX-Trigger-After-Swap:/i)){var o=l;if(!se(l)){o=re().body}Be(f,"HX-Trigger-After-Swap",o)}var s=function(){oe(n.tasks,function(e){e.call()});oe(n.elts,function(e){if(e.classList){e.classList.remove(Q.config.settlingClass)}ce(e,"htmx:afterSettle",u)});if(u.pathInfo.anchor){var e=re().getElementById(u.pathInfo.anchor);if(e){e.scrollIntoView({block:"start",behavior:"auto"})}}if(n.title&&!m){var t=C("title");if(t){t.innerHTML=n.title}else{window.document.title=n.title}}xr(n.elts,v);if(O(f,/HX-Trigger-After-Settle:/i)){var r=l;if(!se(l)){r=re().body}Be(f,"HX-Trigger-After-Settle",r)}ie(p)};if(v.settleDelay>0){setTimeout(s,v.settleDelay)}else{s()}}catch(e){fe(l,"htmx:swapError",u);ie(y);throw e}};var b=Q.config.globalViewTransitions;if(v.hasOwnProperty("transition")){b=v.transition}if(b&&ce(l,"htmx:beforeTransition",u)&&typeof Promise!=="undefined"&&document.startViewTransition){var w=new Promise(function(e,t){p=e;y=t});var S=x;x=function(){document.startViewTransition(function(){S();return w})}}if(v.swapDelay>0){setTimeout(x,v.swapDelay)}else{x()}}if(a){fe(l,"htmx:responseError",le({error:"Response Status Error Code "+f.status+" from "+u.pathInfo.requestPath},u))}}var Nr={};function Ir(){return{init:function(e){return null},onEvent:function(e,t){return true},transformResponse:function(e,t,r){return e},isInlineSwap:function(e){return false},handleSwap:function(e,t,r,n){return false},encodeParameters:function(e,t,r){return null}}}function kr(e,t){if(t.init){t.init(r)}Nr[e]=le(Ir(),t)}function Pr(e){delete Nr[e]}function Mr(e,r,n){if(e==undefined){return r}if(r==undefined){r=[]}if(n==undefined){n=[]}var t=te(e,"hx-ext");if(t){oe(t.split(","),function(e){e=e.replace(/ /g,"");if(e.slice(0,7)=="ignore:"){n.push(e.slice(7));return}if(n.indexOf(e)<0){var t=Nr[e];if(t&&r.indexOf(t)<0){r.push(t)}}})}return Mr(u(e),r,n)}function Dr(e){var t=function(){if(!e)return;e();e=null};if(re().readyState==="complete"){t()}else{re().addEventListener("DOMContentLoaded",function(){t()});re().addEventListener("readystatechange",function(){if(re().readyState!=="complete")return;t()})}}function Xr(){if(Q.config.includeIndicatorStyles!==false){re().head.insertAdjacentHTML("beforeend","<style> ."+Q.config.indicatorClass+"{opacity:0} ."+Q.config.requestClass+" ."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} ."+Q.config.requestClass+"."+Q.config.indicatorClass+"{opacity:1; transition: opacity 200ms ease-in;} </style>")}}function Ur(){var e=re().querySelector('meta[name="htmx-config"]');if(e){return E(e.content)}else{return null}}function Br(){var e=Ur();if(e){Q.config=le(Q.config,e)}}Dr(function(){Br();Xr();var e=re().body;Bt(e);var t=re().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){var t=e.target;var r=ae(t);if(r&&r.xhr){r.xhr.abort()}});var r=window.onpopstate;window.onpopstate=function(e){if(e.state&&e.state.htmx){er();oe(t,function(e){ce(e,"htmx:restored",{document:re(),triggerEvent:ce})})}else{if(r){r(e)}}};setTimeout(function(){ce(e,"htmx:load",{});e=null},0)});return Q}()}); \ No newline at end of file
diff --git a/dist/htmx.min.js.gz b/dist/htmx.min.js.gz
index 91a4a55a..f218fd5f 100644
--- a/dist/htmx.min.js.gz
+++ b/dist/htmx.min.js.gz
Binary files differ
diff --git a/package.json b/package.json
index e91ec71f..0f3e3a1a 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"AJAX",
"HTML"
],
- "version": "1.9.8",
+ "version": "1.9.10",
"homepage": "https://htmx.org/",
"bugs": {
"url": "https://github.com/bigskysoftware/htmx/issues"
diff --git a/src/ext/ws.js b/src/ext/ws.js
index 3366e9e5..05be1eca 100644
--- a/src/ext/ws.js
+++ b/src/ext/ws.js
@@ -200,7 +200,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
if (!this.socket) {
api.triggerErrorEvent()
}
- if (sendElt && api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
+ if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
message: message,
socketWrapper: this.publicInterface
})) {
diff --git a/src/htmx.d.ts b/src/htmx.d.ts
index 2342300c..d768a3ef 100644
--- a/src/htmx.d.ts
+++ b/src/htmx.d.ts
@@ -48,7 +48,7 @@ export function ajax(verb: string, path: string, selector: string): Promise<void
export function ajax(
verb: string,
path: string,
- context: Partial<{ source: any; event: any; handler: any; target: any; swap: any; values: any; headers: any }>
+ context: Partial<{ source: any; event: any; handler: any; target: any; swap: any; values: any; headers: any; select: any }>
): Promise<void>;
/**
@@ -395,6 +395,41 @@ export interface HtmxConfig {
* @default false
*/
selfRequestsOnly?: boolean;
+ /**
+ * Whether or not the target of a boosted element is scrolled into the viewport.
+ * @default true
+ */
+ scrollIntoViewOnBoost?: boolean;
+ /**
+ * If set, the nonce will be added to inline scripts.
+ * @default ''
+ */
+ inlineScriptNonce?: string;
+ /**
+ * The type of binary data being received over the WebSocket connection
+ * @default 'blob'
+ */
+ wsBinaryType?: 'blob' | 'arraybuffer';
+ /**
+ * If set to true htmx will include a cache-busting parameter in GET requests to avoid caching partial responses by the browser
+ * @default false
+ */
+ getCacheBusterParam?: boolean;
+ /**
+ * If set to true, htmx will use the View Transition API when swapping in new content.
+ * @default false
+ */
+ globalViewTransitions?: boolean;
+ /**
+ * htmx will format requests with these methods by encoding their parameters in the URL, not the request body
+ * @default ["get"]
+ */
+ methodsThatUseUrlParams?: ('get' | 'head' | 'post' | 'put' | 'delete' | 'connect' | 'options' | 'trace' | 'patch' )[];
+ /**
+ * If set to true htmx will not update the title of the document when a title tag is found in new content
+ * @default false
+ */
+ ignoreTitle:? boolean;
}
/**
diff --git a/src/htmx.js b/src/htmx.js
index f11437ca..66dcd038 100644
--- a/src/htmx.js
+++ b/src/htmx.js
@@ -69,12 +69,13 @@ return (function () {
wsBinaryType: 'blob',
disableSelector: "[hx-disable], [data-hx-disable]",
useTemplateFragments: false,
- scrollBehavior: 'smooth',
+ scrollBehavior: 'instant',
defaultFocusScroll: false,
getCacheBusterParam: false,
globalViewTransitions: false,
- methodsThatUseUrlParams: ["get"],
- selfRequestsOnly: false,
+ methodsThatUseUrlParams: ["get", "delete"],
+ selfRequestsOnly: true,
+ ignoreTitle: false,
scrollIntoViewOnBoost: true,
disableInheritance: false
},
@@ -88,7 +89,7 @@ return (function () {
sock.binaryType = htmx.config.wsBinaryType;
return sock;
},
- version: "1.9.8"
+ version: "1.9.10"
};
/** @type {import("./htmx").HtmxInternalApi} */
@@ -285,7 +286,7 @@ return (function () {
}
function aFullPageResponse(resp) {
- return resp.match(/<body/);
+ return /<body/.test(resp)
}
/**
@@ -459,7 +460,7 @@ return (function () {
path = url.pathname + url.search;
}
// remove trailing slash, unless index page
- if (!path.match('^/$')) {
+ if (!(/^\/$/.test(path))) {
path = path.replace(/\/+$/, '');
}
return path;
@@ -836,7 +837,7 @@ return (function () {
var oobSelects = getClosestAttributeValue(elt, "hx-select-oob");
if (oobSelects) {
var oobSelectValues = oobSelects.split(",");
- for (let i = 0; i < oobSelectValues.length; i++) {
+ for (var i = 0; i < oobSelectValues.length; i++) {
var oobSelectValue = oobSelectValues[i].split(":", 2);
var id = oobSelectValue[0].trim();
if (id.indexOf("#") === 0) {
@@ -943,7 +944,7 @@ return (function () {
function deInitOnHandlers(elt) {
var internalData = getInternalData(elt);
if (internalData.onHandlers) {
- for (let i = 0; i < internalData.onHandlers.length; i++) {
+ for (var i = 0; i < internalData.onHandlers.length; i++) {
const handlerInfo = internalData.onHandlers[i];
elt.removeEventListener(handlerInfo.event, handlerInfo.listener);
}
@@ -1155,6 +1156,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;
@@ -1237,12 +1240,24 @@ return (function () {
function consumeUntil(tokens, match) {
var result = "";
- while (tokens.length > 0 && !tokens[0].match(match)) {
+ while (tokens.length > 0 && !match.test(tokens[0])) {
result += tokens.shift();
}
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';
/**
@@ -1291,29 +1306,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 {
@@ -1885,26 +1904,35 @@ return (function () {
});
}
- function hasChanceOfBeingBoosted() {
- return document.querySelector("[hx-boost], [data-hx-boost]");
+ function shouldProcessHxOn(elt) {
+ var attributes = elt.attributes
+ for (var j = 0; j < attributes.length; j++) {
+ var attrName = attributes[j].name
+ if (startsWith(attrName, "hx-on:") || startsWith(attrName, "data-hx-on:") ||
+ startsWith(attrName, "hx-on-") || startsWith(attrName, "data-hx-on-")) {
+ return true
+ }
+ }
+ return false
}
function findHxOnWildcardElements(elt) {
var node = null
var elements = []
+ if (shouldProcessHxOn(elt)) {
+ elements.push(elt)
+ }
+
if (document.evaluate) {
- var iter = document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]', elt)
+ var iter = document.evaluate('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
+ ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', elt)
while (node = iter.iterateNext()) elements.push(node)
} else {
- var allElements = document.getElementsByTagName("*")
+ var allElements = elt.getElementsByTagName("*")
for (var i = 0; i < allElements.length; i++) {
- var attributes = allElements[i].attributes
- for (var j = 0; j < attributes.length; j++) {
- var attrName = attributes[j].name
- if (startsWith(attrName, "hx-on:") || startsWith(attrName, "data-hx-on:")) {
- elements.push(allElements[i])
- }
+ if (shouldProcessHxOn(allElements[i])) {
+ elements.push(allElements[i])
}
}
}
@@ -1914,9 +1942,9 @@ return (function () {
function findElementsToProcess(elt) {
if (elt.querySelectorAll) {
- var boostedElts = hasChanceOfBeingBoosted() ? ", a" : "";
- var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws]," +
- " [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]");
+ var boostedSelector = ", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]";
+ var results = elt.querySelectorAll(VERB_SELECTOR + boostedSelector + ", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws]," +
+ " [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]");
return results;
} else {
return [];
@@ -1961,7 +1989,7 @@ return (function () {
function countCurlies(line) {
var tokens = tokenizeString(line);
var netCurlies = 0;
- for (let i = 0; i < tokens.length; i++) {
+ for (var i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token === "{") {
netCurlies++;
@@ -1990,32 +2018,6 @@ return (function () {
nodeData.onHandlers.push({event:eventName, listener:listener});
}
- function processHxOn(elt) {
- var hxOnValue = getAttributeValue(elt, 'hx-on');
- if (hxOnValue) {
- var handlers = {}
- var lines = hxOnValue.split("\n");
- var currentEvent = null;
- var curlyCount = 0;
- while (lines.length > 0) {
- var line = lines.shift();
- var match = line.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/);
- if (curlyCount === 0 && match) {
- line.split(":")
- currentEvent = match[1].slice(0, -1); // strip last colon
- handlers[currentEvent] = match[2];
- } else {
- handlers[currentEvent] += line;
- }
- curlyCount += countCurlies(line);
- }
-
- for (var eventName in handlers) {
- addHxOnEventHandler(elt, eventName, handlers[eventName]);
- }
- }
- }
-
function processHxOnWildcard(elt) {
// wipe any previous on handlers so that this function takes precedence
deInitOnHandlers(elt)
@@ -2023,12 +2025,22 @@ return (function () {
for (var i = 0; i < elt.attributes.length; i++) {
var name = elt.attributes[i].name
var value = elt.attributes[i].value
- if (startsWith(name, "hx-on:") || startsWith(name, "data-hx-on:")) {
- let eventName = name.slice(name.indexOf(":") + 1)
- // if the eventName starts with a colon, prepend "htmx" for shorthand support
- if (startsWith(eventName, ":")) eventName = "htmx" + eventName
+ if (startsWith(name, "hx-on") || startsWith(name, "data-hx-on")) {
+ var afterOnPosition = name.indexOf("-on") + 3;
+ var nextChar = name.slice(afterOnPosition, afterOnPosition + 1);
+ if (nextChar === "-" || nextChar === ":") {
+ var eventName = name.slice(afterOnPosition + 1);
+ // if the eventName starts with a colon or dash, prepend "htmx" for shorthand support
+ if (startsWith(eventName, ":")) {
+ eventName = "htmx" + eventName
+ } else if (startsWith(eventName, "-")) {
+ eventName = "htmx:" + eventName.slice(1);
+ } else if (startsWith(eventName, "htmx-")) {
+ eventName = "htmx:" + eventName.slice(5);
+ }
- addHxOnEventHandler(elt, eventName, value)
+ addHxOnEventHandler(elt, eventName, value)
+ }
}
}
}
@@ -2045,8 +2057,6 @@ return (function () {
nodeData.initHash = attributeHash(elt);
- processHxOn(elt);
-
triggerEvent(elt, "htmx:beforeProcessNode")
if (elt.value) {
@@ -2095,8 +2105,6 @@ return (function () {
}
initNode(elt);
forEach(findElementsToProcess(elt), function(child) { initNode(child) });
- // Because it happens second, the new way of adding onHandlers superseeds the old one
- // i.e. if there are any hx-on:eventName attributes, the hx-on attribute will be ignored
forEach(findHxOnWildcardElements(elt), processHxOnWildcard);
}
@@ -2306,7 +2314,9 @@ return (function () {
var details = {path: path, xhr:request};
triggerEvent(getDocument().body, "htmx:historyCacheMiss", details);
request.open('GET', path, true);
+ request.setRequestHeader("HX-Request", "true");
request.setRequestHeader("HX-History-Restore-Request", "true");
+ request.setRequestHeader("HX-Current-URL", getDocument().location.href);
request.onload = function () {
if (this.status >= 200 && this.status < 400) {
triggerEvent(getDocument().body, "htmx:historyCacheMissLoad", details);
@@ -2897,7 +2907,7 @@ return (function () {
}
function hasHeader(xhr, regexp) {
- return xhr.getAllResponseHeaders().match(regexp);
+ return regexp.test(xhr.getAllResponseHeaders())
}
function ajaxHelper(verb, path, context) {
@@ -2916,6 +2926,7 @@ return (function () {
values : context.values,
targetOverride: resolveTarget(context.target),
swapOverride: context.swap,
+ select: context.select,
returnPromise: true
});
}
@@ -2970,6 +2981,7 @@ return (function () {
elt = getDocument().body;
}
var responseHandler = etc.handler || handleAjaxResponse;
+ var select = etc.select || null;
if (!bodyContains(elt)) {
// do not issue requests for elements removed from the DOM
@@ -3118,6 +3130,11 @@ return (function () {
var headers = getHeaders(elt, target, promptResponse);
+
+ if (verb !== 'get' && !usesFormData(elt)) {
+ headers['Content-Type'] = 'application/x-www-form-urlencoded';
+ }
+
if (etc.headers) {
headers = mergeObjects(headers, etc.headers);
}
@@ -3131,10 +3148,6 @@ return (function () {
var allParameters = mergeObjects(rawParameters, expressionVars);
var filteredParameters = filterValues(allParameters, elt);
- if (verb !== 'get' && !usesFormData(elt)) {
- headers['Content-Type'] = 'application/x-www-form-urlencoded';
- }
-
if (htmx.config.getCacheBusterParam && verb === 'get') {
filteredParameters['org.htmx.cache-buster'] = getRawAttribute(target, "id") || "true";
}
@@ -3232,7 +3245,7 @@ return (function () {
}
var responseInfo = {
- xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted,
+ xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted, select: select,
pathInfo: {
requestPath: path,
finalRequestPath: finalPath,
@@ -3403,6 +3416,7 @@ return (function () {
var target = responseInfo.target;
var etc = responseInfo.etc;
var requestConfig = responseInfo.requestConfig;
+ var select = responseInfo.select;
if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return;
@@ -3512,10 +3526,26 @@ return (function () {
}
var selectOverride;
+ if (select) {
+ selectOverride = select;
+ }
+
if (hasHeader(xhr, /HX-Reselect:/i)) {
selectOverride = xhr.getResponseHeader("HX-Reselect");
}
+ // if we need to save history, do so, before swapping so that relative resources have the correct base URL
+ if (historyUpdate.type) {
+ triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
+ if (historyUpdate.type === "push") {
+ pushUrlIntoHistory(historyUpdate.path);
+ triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
+ } else {
+ replaceUrlInHistory(historyUpdate.path);
+ triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
+ }
+ }
+
var settleInfo = makeSettleInfo(target);
selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, selectOverride);
@@ -3565,17 +3595,6 @@ return (function () {
triggerEvent(elt, 'htmx:afterSettle', responseInfo);
});
- // if we need to save history, do so
- if (historyUpdate.type) {
- triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
- if (historyUpdate.type === "push") {
- pushUrlIntoHistory(historyUpdate.path);
- triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
- } else {
- replaceUrlInHistory(historyUpdate.path);
- triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
- }
- }
if (responseInfo.pathInfo.anchor) {
var anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor);
if(anchorTarget) {
@@ -3734,25 +3753,34 @@ return (function () {
//====================================================================
// Initialization
//====================================================================
- var isReady = false
- getDocument().addEventListener('DOMContentLoaded', function() {
- isReady = true
- })
-
/**
- * Execute a function now if DOMContentLoaded has fired, otherwise listen for it.
- *
- * This function uses isReady because there is no realiable way to ask the browswer whether
- * the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded
- * firing and readystate=complete.
+ * We want to initialize the page elements after DOMContentLoaded
+ * fires, but there isn't always a good way to tell whether
+ * it has already fired when we get here or not.
*/
- function ready(fn) {
- // Checking readyState here is a failsafe in case the htmx script tag entered the DOM by
- // some means other than the initial page load.
- if (isReady || getDocument().readyState === 'complete') {
- fn();
- } else {
- getDocument().addEventListener('DOMContentLoaded', fn);
+ function ready(functionToCall) {
+ // call the function exactly once no matter how many times this is called
+ var callReadyFunction = function() {
+ if (!functionToCall) return;
+ functionToCall();
+ functionToCall = null;
+ };
+
+ if (getDocument().readyState === "complete") {
+ // DOMContentLoaded definitely fired, we can initialize the page
+ callReadyFunction();
+ }
+ else {
+ /* DOMContentLoaded *maybe* already fired, wait for
+ * the next DOMContentLoaded or readystatechange event
+ */
+ getDocument().addEventListener("DOMContentLoaded", function() {
+ callReadyFunction();
+ });
+ getDocument().addEventListener("readystatechange", function() {
+ if (getDocument().readyState !== "complete") return;
+ callReadyFunction();
+ });
}
}
@@ -3760,9 +3788,9 @@ return (function () {
if (htmx.config.includeIndicatorStyles !== false) {
getDocument().head.insertAdjacentHTML("beforeend",
"<style>\
- ." + htmx.config.indicatorClass + "{opacity:0;transition: opacity 200ms ease-in;}\
- ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1}\
- ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1}\
+ ." + htmx.config.indicatorClass + "{opacity:0}\
+ ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\
+ ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\
</style>");
}
}
@@ -3800,7 +3828,9 @@ return (function () {
internalData.xhr.abort();
}
});
- var originalPopstate = window.onpopstate;
+ /** @type {(ev: PopStateEvent) => any} */
+ const originalPopstate = window.onpopstate ? window.onpopstate.bind(window) : null;
+ /** @type {(ev: PopStateEvent) => any} */
window.onpopstate = function (event) {
if (event.state && event.state.htmx) {
restoreHistory();
diff --git a/test/attributes/hx-boost.js b/test/attributes/hx-boost.js
index 7866abec..f8698a33 100644
--- a/test/attributes/hx-boost.js
+++ b/test/attributes/hx-boost.js
@@ -116,4 +116,3 @@ describe("hx-boost attribute", function() {
});
});
-
diff --git a/test/attributes/hx-on-wildcard.js b/test/attributes/hx-on-wildcard.js
index b45bb085..9aa3633e 100644
--- a/test/attributes/hx-on-wildcard.js
+++ b/test/attributes/hx-on-wildcard.js
@@ -15,6 +15,14 @@ describe("hx-on:* attribute", function() {
delete window.foo;
});
+ it("can use dashes rather than colons", function () {
+ var btn = make("<button hx-on-click='window.foo = true'>Foo</button>");
+ btn.click();
+ window.foo.should.equal(true);
+ delete window.foo;
+ });
+
+
it("can modify a parameter via htmx:configRequest", function () {
this.server.respondWith("POST", "/test", function (xhr) {
var params = parseParams(xhr.requestBody);
@@ -26,6 +34,17 @@ describe("hx-on:* attribute", function() {
btn.innerText.should.equal("bar");
});
+ it("can modify a parameter via htmx:configRequest with dashes", function () {
+ this.server.respondWith("POST", "/test", function (xhr) {
+ var params = parseParams(xhr.requestBody);
+ xhr.respond(200, {}, params.foo);
+ });
+ var btn = make("<button hx-on-htmx-config-request='event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>");
+ btn.click();
+ this.server.respond();
+ btn.innerText.should.equal("bar");
+ });
+
it("expands :: shorthand into htmx:", function () {
this.server.respondWith("POST", "/test", function (xhr) {
var params = parseParams(xhr.requestBody);
@@ -37,6 +56,17 @@ describe("hx-on:* attribute", function() {
btn.innerText.should.equal("bar");
});
+ it("expands -- shorthand into htmx:", function () {
+ this.server.respondWith("POST", "/test", function (xhr) {
+ var params = parseParams(xhr.requestBody);
+ xhr.respond(200, {}, params.foo);
+ });
+ var btn = make("<button hx-on--config-request='event.detail.parameters.foo = \"bar\"' hx-post='/test'>Foo</button>");
+ btn.click();
+ this.server.respond();
+ btn.innerText.should.equal("bar");
+ });
+
it("can cancel an event via preventDefault for htmx:config-request", function () {
this.server.respondWith("POST", "/test", function (xhr) {
xhr.respond(200, {}, "<button>Bar</button>");
@@ -185,7 +215,7 @@ describe("hx-on:* attribute", function() {
// check there is just one handler against each event
htmx.trigger(div, "increment-foo");
- htmx.trigger(div, "increment-bar");
+ htmx.trigger(div, "increment-bar");
window.foo.should.equal(1);
window.bar.should.equal(1);
diff --git a/test/attributes/hx-trigger.js b/test/attributes/hx-trigger.js
index 5e2f138b..0cbb4adf 100644
--- a/test/attributes/hx-trigger.js
+++ b/test/attributes/hx-trigger.js
@@ -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>');
+ });
+
})
diff --git a/test/core/ajax.js b/test/core/ajax.js
index 7d18be73..42d733a6 100644
--- a/test/core/ajax.js
+++ b/test/core/ajax.js
@@ -868,9 +868,7 @@ describe("Core htmx AJAX Tests", function(){
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
- if (supportsSvgTitles()) { // IE 11
- btn.innerText.should.equal("Clicked!");
- }
+ btn.innerText.should.equal("Clicked!");
window.document.title.should.equal(originalTitle);
});
@@ -884,9 +882,7 @@ describe("Core htmx AJAX Tests", function(){
var btn = make('<button hx-get="/test">Click Me!</button>')
btn.click();
this.server.respond();
- if (supportsSvgTitles()) { // IE 11
- btn.innerText.should.equal("Clicked!");
- }
+ btn.innerText.should.equal("Clicked!");
window.document.title.should.equal(newTitle);
});
@@ -1089,11 +1085,6 @@ describe("Core htmx AJAX Tests", function(){
})
it('properly handles clicked submit button with a value outside a htmx form', function () {
- if (!supportsFormAttribute()) {
- this._runnable.title += " - Skipped as IE11 doesn't support form attribute"
- this.skip()
- return
- }
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
@@ -1111,11 +1102,6 @@ describe("Core htmx AJAX Tests", function(){
})
it('properly handles clicked submit input with a value outside a htmx form', function () {
- if (!supportsFormAttribute()) {
- this._runnable.title += " - Skipped as IE11 doesn't support form attribute"
- this.skip()
- return
- }
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
@@ -1187,11 +1173,6 @@ describe("Core htmx AJAX Tests", function(){
})
it('properly handles clicked submit button with a value inside a form, referencing another form', function () {
- if (!supportsFormAttribute()) {
- this._runnable.title += " - Skipped as IE11 doesn't support form attribute"
- this.skip()
- return
- }
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
@@ -1212,11 +1193,6 @@ describe("Core htmx AJAX Tests", function(){
})
it('properly handles clicked submit input with a value inside a form, referencing another form', function () {
- if (!supportsFormAttribute()) {
- this._runnable.title += " - Skipped as IE11 doesn't support form attribute"
- this.skip()
- return
- }
var values;
this.server.respondWith("Post", "/test", function (xhr) {
values = getParameters(xhr);
@@ -1284,6 +1260,8 @@ describe("Core htmx AJAX Tests", function(){
byId("submit").click();
this.server.respond();
responded.should.equal(true);
+ })
+
it("can associate submit buttons from outside a form with the current version of the form after swap", function(){
const template = '<form ' +
'id="hello" ' +
diff --git a/test/core/api.js b/test/core/api.js
index 8a1735c4..431633d6 100644
--- a/test/core/api.js
+++ b/test/core/api.js
@@ -225,6 +225,24 @@ describe("Core htmx API test", function(){
div.innerHTML.should.equal('<p class="test">foo!</p>');
});
+ it('ajax api works with select', function()
+ {
+ this.server.respondWith("GET", "/test", "<div id='d1'>foo</div><div id='d2'>bar</div>");
+ var div = make("<div id='target'></div>");
+ htmx.ajax("GET", "/test", {target: "#target", select: "#d2"});
+ this.server.respond();
+ div.innerHTML.should.equal('<div id="d2">bar</div>');
+ });
+
+ it('ajax api works with Hx-Select overrides select', function()
+ {
+ this.server.respondWith("GET", "/test", [200, {"HX-Reselect": "#d2"}, "<div id='d1'>foo</div><div id='d2'>bar</div>"]);
+ var div = make("<div id='target'></div>");
+ htmx.ajax("GET", "/test", {target: "#target", select: "#d1"});
+ this.server.respond();
+ div.innerHTML.should.equal('<div id="d2">bar</div>');
+ });
+
it('ajax returns a promise', function(done)
{
// in IE we do not return a promise
@@ -255,6 +273,44 @@ describe("Core htmx API test", function(){
div.innerHTML.should.equal("Clicked!");
});
+ it('ajax api Content-Type header is application/x-www-form-urlencoded', function(){
+
+ this.server.respondWith("POST", "/test", function (xhr) {
+ var params = getParameters(xhr);
+ xhr.requestHeaders['Content-Type'].should.equal('application/x-www-form-urlencoded;charset=utf-8');
+ params['i1'].should.equal("test");
+ xhr.respond(200, {}, "Clicked!")
+ });
+ var div = make("<div id='d1'></div>");
+ htmx.ajax("POST", "/test", {target:"#d1", values:{i1: 'test'}})
+ this.server.respond();
+ div.innerHTML.should.equal("Clicked!");
+ });
+
+ it('ajax api Content-Type header override to application/json', function(){
+
+ this.server.respondWith("POST", "/test", function (xhr) {
+ var params = getParameters(xhr);
+ xhr.requestHeaders['Content-Type'].should.equal('application/json;charset=utf-8');
+ params['i1'].should.equal("test");
+ xhr.respond(200, {}, "Clicked!");
+ });
+
+ var div = make("<div id='d1'></div>");
+ htmx.ajax('POST',"/test", {
+ target:'#d1',
+ swap:'innerHTML',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ values:{i1: 'test'}
+ })
+
+ this.server.respond();
+ div.innerHTML.should.equal("Clicked!");
+ });
+
+
it('can re-init with new attributes', function () {
this.server.respondWith("PATCH", "/test", "patch");
this.server.respondWith("DELETE", "/test", "delete");
diff --git a/test/core/events.js b/test/core/events.js
index 7c1c1596..309c3e07 100644
--- a/test/core/events.js
+++ b/test/core/events.js
@@ -389,12 +389,7 @@ describe("Core htmx Events", function() {
});
it("htmx:sendError is called after a failed request", function (done) {
- if (IsIE11()) {
- // IE will throw an exception on xhr.open with the URL below, xhr.send won't even be called
- this._runnable.title += " - Skipped on IE11 as xhr.send won't even be called with a file URL"
- this.skip()
- return
- }
+ htmx.config.selfRequestsOnly = false; // turn off self requests only
var called = false;
var handler = htmx.on("htmx:sendError", function (evt) {
called = true;
@@ -405,6 +400,7 @@ describe("Core htmx Events", function() {
setTimeout(function () {
htmx.off("htmx:sendError", handler);
should.equal(called, true);
+ htmx.config.selfRequestsOnly = true; // restore self requests only
done();
}, 30);
});
diff --git a/test/core/extensions.js b/test/core/extensions.js
index f2d3ed2a..2bcfe208 100644
--- a/test/core/extensions.js
+++ b/test/core/extensions.js
@@ -29,11 +29,6 @@ describe('Core htmx extension tests', function() {
onEvent: function(name, evt) {
if (name === 'htmx:beforeRequest') {
evt.preventDefault();
- if (IsIE11()) {
- // IE11 doesn't set defaultPrevented to true on custom events it seems, so use a
- // return false instead to cancel the event
- return false
- }
}
}
});
diff --git a/test/core/headers.js b/test/core/headers.js
index af357d37..7e127ece 100644
--- a/test/core/headers.js
+++ b/test/core/headers.js
@@ -348,4 +348,31 @@ describe("Core htmx AJAX headers", function () {
this.server.respond();
div.innerHTML.should.equal('<div>Yay! Welcome</div>');
})
+
+ it('request to restore history should include the HX-Request header', function () {
+ this.server.respondWith('GET', '/test', function (xhr) {
+ xhr.requestHeaders['HX-Request'].should.be.equal('true');
+ xhr.respond(200, {}, '');
+ });
+ htmx._('loadHistoryFromServer')('/test');
+ this.server.respond();
+ });
+
+ it('request to restore history should include the HX-History-Restore-Request header', function () {
+ this.server.respondWith('GET', '/test', function (xhr) {
+ xhr.requestHeaders['HX-History-Restore-Request'].should.be.equal('true');
+ xhr.respond(200, {}, '');
+ });
+ htmx._('loadHistoryFromServer')('/test');
+ this.server.respond();
+ });
+
+ it('request to restore history should include the HX-Current-URL header', function () {
+ this.server.respondWith('GET', '/test', function (xhr) {
+ chai.assert(typeof xhr.requestHeaders['HX-Current-URL'] !== 'undefined', 'HX-Current-URL should not be undefined');
+ xhr.respond(200, {}, '');
+ });
+ htmx._('loadHistoryFromServer')('/test');
+ this.server.respond();
+ });
});
diff --git a/test/core/internals.js b/test/core/internals.js
index 1de1fb98..f5c96356 100644
--- a/test/core/internals.js
+++ b/test/core/internals.js
@@ -22,11 +22,6 @@ describe("Core htmx internals Tests", function() {
})
it("makeFragment works with template wrapping", function(){
- if (!supportsTemplates()) {
- this._runnable.title += " - Skipped as IE11 doesn't support templates"
- this.skip()
- return
- }
try {
htmx._("makeFragment")("<html></html>").children.length.should.equal(0);
htmx._("makeFragment")("<html><body></body></html>").children.length.should.equal(0);
@@ -50,11 +45,6 @@ describe("Core htmx internals Tests", function() {
it("makeFragment works with template wrapping and funky combos", function(){
- if (!supportsTemplates()) {
- this._runnable.title += " - Skipped as IE11 doesn't support templates"
- this.skip()
- return
- }
htmx.config.useTemplateFragments = true;
try {
var fragment = htmx._("makeFragment")("<td></td><div></div>");
diff --git a/test/core/perf.js b/test/core/perf.js
index eacaa5b0..87f2c95d 100644
--- a/test/core/perf.js
+++ b/test/core/perf.js
@@ -50,10 +50,6 @@ describe("Core htmx perf Tests", function() {
it("history snapshot cleaning should be fast", function(){
var size = 5 * 1024 // ~350K in size, about the size of CNN's body tag :p
- if (IsIE11()) {
- // So slow in IE11 it freezes the browser and blocks other tests, pretty annoying
- size = 5 * 100 // Seriously this already takes ~1.5 SECOND to run, more simply makes it crash
- }
var workArea = getWorkArea();
var html = "<div class='foo bar'>Yay, really large HTML documents are fun!</div>\n";
html = stringRepeat(html, size);
diff --git a/test/core/regressions.js b/test/core/regressions.js
index a0f6724e..585ab3dc 100644
--- a/test/core/regressions.js
+++ b/test/core/regressions.js
@@ -129,12 +129,6 @@ describe("Core htmx Regression Tests", function(){
})
it('a form can reset based on the htmx:afterRequest event', function() {
- if (IsIE11()) {
- this._runnable.title += " - Skipped as hyperscript isn't IE11 compatible"
- this.skip()
- return
- }
-
this.server.respondWith("POST", "/test", "posted");
var form = make('<div id="d1"></div><form _="on htmx:afterRequest reset() me" hx-post="/test" hx-target="#d1">' +
diff --git a/test/core/security.js b/test/core/security.js
index 95f17350..733a4ef9 100644
--- a/test/core/security.js
+++ b/test/core/security.js
@@ -106,10 +106,12 @@ describe("security options", function() {
btn.innerHTML.should.equal("Clicked a second time");
})
- it("can make egress cross site requests when htmx.config.selfRequestsOnly is enabled", function(done){
+ it("can make egress cross site requests when htmx.config.selfRequestsOnly is disabled", function(done){
this.timeout(4000)
+ htmx.config.selfRequestsOnly = false;
// should trigger send error, rather than reject
var listener = htmx.on("htmx:sendError", function (){
+ htmx.config.selfRequestsOnly = true;
htmx.off("htmx:sendError", listener);
done();
});
@@ -122,9 +124,7 @@ describe("security options", function() {
it("can't make egress cross site requests when htmx.config.selfRequestsOnly is enabled", function(done){
this.timeout(4000)
// should trigger send error, rather than reject
- htmx.config.selfRequestsOnly = true;
var listener = htmx.on("htmx:invalidPath", function (){
- htmx.config.selfRequestsOnly = false;
htmx.off("htmx:invalidPath", listener);
done();
})
diff --git a/test/core/validation.js b/test/core/validation.js
index 075e4639..428a4acc 100644
--- a/test/core/validation.js
+++ b/test/core/validation.js
@@ -110,12 +110,6 @@ describe("Core htmx client side validation tests", function(){
it('hyperscript validation error prevents request', function()
{
- if (IsIE11()) {
- this._runnable.title += " - Skipped as hyperscript isn't IE11 compatible"
- this.skip()
- return
- }
-
this.server.respondWith("POST", "/test", "Clicked!");
var form = make('<form hx-post="/test" hx-trigger="click">' +
diff --git a/test/ext/hyperscript.js b/test/ext/hyperscript.js
index 67889d05..6d49a2da 100644
--- a/test/ext/hyperscript.js
+++ b/test/ext/hyperscript.js
@@ -2,11 +2,6 @@ describe("hyperscript integration", function() {
beforeEach(function () {
this.server = makeServer();
clearWorkArea();
-
- if (IsIE11()) {
- this.title += " - Skipped as hyperscript isn't IE11 compatible"
- this.skip()
- }
});
afterEach(function () {
this.server.restore();
diff --git a/test/ext/ws.js b/test/ext/ws.js
index 7e6f140d..2b90eb00 100644
--- a/test/ext/ws.js
+++ b/test/ext/ws.js
@@ -550,12 +550,6 @@ describe("web-sockets extension", function () {
})
it('sends data to the server with external non-htmx form + submit button & value', function () {
- if (!supportsFormAttribute()) {
- this._runnable.title += " - Skipped as IE11 doesn't support form attribute"
- this.skip()
- return
- }
-
make('<div hx-ext="ws" ws-connect="ws://localhost:8080">' +
'<form ws-send id="form">' +
'<input type="hidden" name="foo" value="bar">' +
@@ -583,12 +577,6 @@ describe("web-sockets extension", function () {
})
it('sends data to the server with external non-htmx form + submit input & value', function () {
- if (!supportsFormAttribute()) {
- this._runnable.title += " - Skipped as IE11 doesn't support form attribute"
- this.skip()
- return
- }
-
make('<div hx-ext="ws" ws-connect="ws://localhost:8080">' +
'<form ws-send id="form">' +
'<input type="hidden" name="foo" value="bar">' +
@@ -614,4 +602,205 @@ describe("web-sockets extension", function () {
this.messages[1].should.contains('"foo":"bar"')
this.messages[1].should.contains('"action":"B"')
})
+
+ describe("Send immediately", function() {
+ function checkCallForWsBeforeSend(spy, wrapper, message, target) {
+ // Utility function to always check the same for htmx:wsBeforeSend caught by a spy
+ spy.calledOnce.should.be.true;
+ var call = spy.getCall(0);
+ call.args.length.should.equal(1);
+ var arg = call.args[0];
+ arg.target.should.equal(target);
+ arg.detail.socketWrapper.should.equal(wrapper);
+ arg.detail.message.should.equal(message);
+ }
+ it('triggers wsBeforeSend on body if provided to sendImmediately', function (done) {
+ var myEventCalled = sinon.spy();
+ var message = '{"foo":"bar"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ window.document.body.addEventListener("htmx:wsBeforeSend", myEventCalled)
+ try {
+ socketWrapper.sendImmediately(message, window.document.body)
+ checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, window.document.body)
+ } finally {
+ window.document.body.removeEventListener("htmx:wsBeforeSend", myEventCalled)
+ }
+ done()
+ }
+ try {
+ window.document.addEventListener("htmx:wsOpen", handler)
+
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+
+ })
+ it('triggers wsBeforeSend on any send element provided to sendImmediately', function (done) {
+ var myEventCalled = sinon.spy();
+ var message = '{"a":"b"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ var id1 = byId("d1");
+ id1.addEventListener("htmx:wsBeforeSend", myEventCalled)
+ try {
+ socketWrapper.sendImmediately(message, d1)
+ checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, d1)
+ } finally {
+ id1.removeEventListener("htmx:wsBeforeSend", myEventCalled)
+ }
+ done()
+ }
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+
+ })
+ it('triggers wsAfterSend on body if provided to sendImmediately', function (done) {
+ var myEventCalled = sinon.spy();
+ var message = '{"foo":"bar"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ window.document.body.addEventListener("htmx:wsAfterSend", myEventCalled)
+ try {
+ socketWrapper.sendImmediately(message, window.document.body)
+ checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, window.document.body)
+ } finally {
+ window.document.body.removeEventListener("htmx:wsAfterSend", myEventCalled)
+ }
+ done()
+ }
+ try {
+ window.document.addEventListener("htmx:wsOpen", handler)
+
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+
+ })
+ it('triggers wsAfterSend on any send element provided to sendImmediately', function (done) {
+ var myEventCalled = sinon.spy();
+ var message = '{"a":"b"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ var id1 = byId("d1");
+ id1.addEventListener("htmx:wsAfterSend", myEventCalled)
+ try {
+ socketWrapper.sendImmediately(message, d1)
+ checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, d1)
+ } finally {
+ id1.removeEventListener("htmx:wsAfterSend", myEventCalled)
+ }
+ done()
+ }
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+
+ })
+ it('sends message if event is not prevented', function (done) {
+ var message = '{"a":"b"}';
+ var noop = function() {}
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ var id1 = byId("d1");
+ id1.addEventListener("htmx:wsBeforeSend", noop)
+ try {
+ socketWrapper.sendImmediately(message, d1)
+ this.tickMock();
+ this.messages.should.eql([message])
+ } finally {
+ id1.removeEventListener("htmx:wsBeforeSend", noop)
+ }
+ done()
+ }.bind(this)
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+ })
+ it('sends message if no sending element is provided', function (done) {
+ var message = '{"a":"b"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ socketWrapper.sendImmediately(message)
+ this.tickMock();
+ this.messages.should.eql([message])
+ done()
+ }.bind(this)
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+ })
+ it('sends message if sending element has no event listener for beforeSend', function (done) {
+ var message = '{"a":"b"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ var d1 = byId("d1");
+ socketWrapper.sendImmediately(message, d1)
+ this.tickMock();
+ this.messages.should.eql([message])
+ done()
+ }.bind(this)
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+ })
+ it('does not send message if beforeSend is prevented', function (done) {
+ var message = '{"a":"b"}';
+ var eventPrevented = function(e) {e.preventDefault()}
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ var id1 = byId("d1");
+ id1.addEventListener("htmx:wsBeforeSend", eventPrevented)
+ try {
+ socketWrapper.sendImmediately(message, d1)
+ this.tickMock();
+ this.messages.should.eql([])
+ } finally {
+ id1.removeEventListener("htmx:wsBeforeSend", eventPrevented)
+ }
+ done()
+ }.bind(this)
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+
+ })
+ })
+
+
});
diff --git a/test/index.html b/test/index.html
index 45ec50aa..98d9981a 100644
--- a/test/index.html
+++ b/test/index.html
@@ -81,7 +81,6 @@
<script src="attributes/hx-disabled-elt.js"></script>
<script src="attributes/hx-inherit.js"></script>
<script src="attributes/hx-disinherit.js"></script>
-<script src="attributes/hx-on.js"></script>
<script src="attributes/hx-on-wildcard.js"></script>
<script src="attributes/hx-params.js"></script>
<script src="attributes/hx-patch.js"></script>
@@ -169,7 +168,6 @@
<script class="mocha-exec">
document.addEventListener("DOMContentLoaded", function () {
- mocha.setup({globals: ['$0', '$1', '$2', '$3', '$4', 'performance', 'requestAnimationFrame', 'cancelAnimationFrame', 'confirm']}); <!-- IE11 -->
mocha.run();
})
</script>
diff --git a/test/manual/hxboost_relative_resources/index.html b/test/manual/hxboost_relative_resources/index.html
new file mode 100644
index 00000000..9bf741eb
--- /dev/null
+++ b/test/manual/hxboost_relative_resources/index.html
@@ -0,0 +1,10 @@
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <script type="application/javascript" src="../../../src/htmx.js"></script>
+ <title>hx-boost - Relative Resources Page 1</title>
+</head>
+<body style="padding:20px;font-family: sans-serif" hx-boost="true">
+<a href="nested/page2.html">To Page 2</a>
+</body>
+</html>
diff --git a/test/manual/hxboost_relative_resources/nested/img.png b/test/manual/hxboost_relative_resources/nested/img.png
new file mode 100644
index 00000000..533a968a
--- /dev/null
+++ b/test/manual/hxboost_relative_resources/nested/img.png
Binary files differ
diff --git a/test/manual/hxboost_relative_resources/nested/page2.html b/test/manual/hxboost_relative_resources/nested/page2.html
new file mode 100644
index 00000000..12cdd25d
--- /dev/null
+++ b/test/manual/hxboost_relative_resources/nested/page2.html
@@ -0,0 +1,12 @@
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <script type="application/javascript" src="../../../../src/htmx.js"></script>
+ <title>hx-boost - Relative Resources Page 2</title>
+</head>
+<body style="padding:20px;font-family: sans-serif" hx-boost="true">
+<a href="../index.html">Back To Page 1</a>
+<p>Image should be displayed below</p>
+<img src="img.png" />
+</body>
+</html>
diff --git a/test/manual/index.html b/test/manual/index.html
index 1097e0fd..99b25d65 100644
--- a/test/manual/index.html
+++ b/test/manual/index.html
@@ -37,6 +37,12 @@
<li><a href="history_style">History Style</a></li>
</ul>
</li>
+ <li>Boost Tests
+ <ul>
+ <li><a href="hxboost_relative_resources">Relative Resources</a></li>
+ <li><a href="hxboost_template_parsing">Template Parsing</a></li>
+ </ul>
+ </li>
</ul>
<h2>Perf</h2>
<li><a href="manual-perf.html">Manual Perf Test</a></li>
diff --git a/test/scratch/demo.html b/test/scratch/demo.html
index 19102815..379e718a 100644
--- a/test/scratch/demo.html
+++ b/test/scratch/demo.html
@@ -1 +1,5 @@
-foo \ No newline at end of file
+<html>
+<body>
+<div id="foo" fooBar="10">asdfasdf</div>
+</body>
+</html>
diff --git a/test/util/util.js b/test/util/util.js
index 2986487d..699c4d22 100644
--- a/test/util/util.js
+++ b/test/util/util.js
@@ -112,33 +112,3 @@ function log(val) {
console.log(val);
return val;
}
-
-// region IE11
-function supportsTemplates() {
- return typeof document.createElement("template").content !== "undefined"
-}
-
-function supportsSvgTitles() {
- // Need to append the element to the body, otherwise innerText will add the svg title to the returned value...
- var tempButton = document.createElement("button")
- tempButton.innerHTML = '<svg><title>Svg title</title></svg>Text';
- document.body.appendChild(tempButton)
- var titleOk = tempButton.innerText === "Text"
- document.body.removeChild(tempButton)
- return titleOk
-}
-
-function supportsFormAttribute() {
- var parser = new DOMParser()
- return !!parser.parseFromString('<button form="form"></button><form id="form"></form>', "text/html").body.firstChild.form
-}
-
-function supportsXPath() {
- return typeof document.evaluate !== "undefined"
-}
-
-function IsIE11() {
- return !supportsTemplates() && !supportsSvgTitles() && !supportsFormAttribute() && !supportsXPath()
-}
-
-// endregion \ No newline at end of file
diff --git a/www/content/_index.md b/www/content/_index.md
index c062fdcf..e504e2ef 100644
--- a/www/content/_index.md
+++ b/www/content/_index.md
@@ -35,7 +35,7 @@ By removing these arbitrary constraints, htmx completes HTML as a [hypertext](ht
<h2>quick start</h2>
```html
- <script src="https://unpkg.com/htmx.org@1.9.8"></script>
+ <script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- have a button POST a click via AJAX -->
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me
@@ -72,6 +72,7 @@ Thank you to all our generous <a href="https://github.com/sponsors/bigskysoftwar
#sponsor-table td {
text-align: center;
padding: 16px;
+ min-height: 100px;
}
@media only screen and (max-width: 760px) {
@@ -91,25 +92,27 @@ Thank you to all our generous <a href="https://github.com/sponsors/bigskysoftwar
<a href="https://www.jetbrains.com//"><img src="/img/jetbrains.png" style="max-width:30%;min-width:200px;"></a>
</td>
<td>
- <a href="https://www.nuclei.ai/"><img src="/img/nuclei_logo_with_text.svg" style="max-width:50%;min-width:200px;"></a>
+ <a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next//"><img src="/img/Github_Logo.png" style="max-width:30%;min-width:200px;"></a>
</td>
<tr>
<td>
- <a href="https://github.blog/2023-04-12-github-accelerator-our-first-cohort-and-whats-next//"><img src="/img/Github_Logo.png" style="max-width:30%;min-width:200px;"></a>
+ <a href="https://www.commspace.co.za/"><img src="/img/commspace.svg" style="width:100%;max-width:400px"></a>
</td>
<td>
- <a href="https://www.commspace.co.za/"><img src="/img/commspace.svg" style="width:100%;max-width:600px"></a>
+ <a href="https://craftcms.com"><img src="/img/logo-craft-cms.svg" style="width:90%;max-width:200px"></a>
</td>
</tr>
<tr>
<td>
- <a href="https://craftcms.com"><img src="/img/logo-craft-cms.svg" style="width:90%;max-width:200px"></a>
-</td>
-<td>
<a href="https://buttercms.com/?utm_campaign=sponsorship&utm_medium=banner&utm_source=htmxhome">
<img src="/img/butter-cms.svg" style="width:100%;max-width:200px">
</a>
</td>
+<td>
+ <a href="https://black.host/">
+ <img src="/img/blackhost-logo.svg" style="width:100%;max-width:200px">
+ </a>
+</td>
</tr>
<tr>
<td>
diff --git a/www/content/api.md b/www/content/api.md
index e19d6ab8..6f67146e 100644
--- a/www/content/api.md
+++ b/www/content/api.md
@@ -60,6 +60,7 @@ or
* `swap` - how the response will be swapped in relative to the target
* `values` - values to submit with the request
* `headers` - headers to submit with the request
+ * `select` - allows you to select the content you want swapped from a response
##### Example
@@ -104,9 +105,10 @@ Note that using a [meta tag](@/docs.md#config) is the preferred mechanism for se
##### Properties
* `attributesToSettle:["class", "style", "width", "height"]` - array of strings: the attributes to settle during the settling phase
+* `refreshOnHistoryMiss:false` - boolean: if set to `true` htmx will issue a full page refresh on history misses rather than use an AJAX request
* `defaultSettleDelay:20` - int: the default delay between completing the content swap and settling attributes
* `defaultSwapDelay:0` - int: the default delay between receiving a response from the server and doing the swap
-* `defaultSwapStyle:'innerHtml'` - string: the default swap style to use if [`hx-swap`](@/attributes/hx-swap.md) is omitted
+* `defaultSwapStyle:'innerHTML'` - string: the default swap style to use if [`hx-swap`](@/attributes/hx-swap.md) is omitted
* `historyCacheSize:10` - int: the number of pages to keep in `localStorage` for history support
* `historyEnabled:true` - boolean: whether or not to use history
* `includeIndicatorStyles:true` - boolean: if true, htmx will inject a small amount of CSS into the page to make indicators invisible unless the `htmx-indicator` class is present
@@ -117,10 +119,20 @@ Note that using a [meta tag](@/docs.md#config) is the preferred mechanism for se
* `swappingClass:'htmx-swapping'` - string: the class to place on target elements when htmx is in the swapping phase
* `allowEval:true` - boolean: allows the use of eval-like functionality in htmx, to enable `hx-vars`, trigger conditions & script tag evaluation. Can be set to `false` for CSP compatibility.
* `allowScriptTags:true` - boolean: allows script tags to be evaluated in new content
+* `inlineScriptNonce:''` - string: the [nonce](https://developer.mozilla.org/docs/Web/HTML/Global_attributes/nonce) to add to inline scripts
* `useTemplateFragments:false` - boolean: use HTML template tags for parsing content from the server. This allows you to use Out of Band content when returning things like table rows, but it is *not* IE11 compatible.
* `withCredentials:false` - boolean: allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates
-* `wsReconnectDelay:full-jitter` - string/function: the default implementation of `getWebSocketReconnectDelay` for reconnecting after unexpected connection loss by the event code `Abnormal Closure`, `Service Restart` or `Try Again Later`
-* `scrollBehavior:smooth` - string: the behavior for a boosted link on page transitions. The allowed values are `auto` and `smooth`. Smooth will smoothscroll to the top of the page while auto will behave like a vanilla link.
+* `timeout:0` - int: the number of milliseconds a request can take before automatically being terminated
+* `wsReconnectDelay:'full-jitter'` - string/function: the default implementation of `getWebSocketReconnectDelay` for reconnecting after unexpected connection loss by the event code `Abnormal Closure`, `Service Restart` or `Try Again Later`
+* `wsBinaryType:'blob'` - string: the [the type of binary data](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) being received over the WebSocket connection
+* `disableSelector:"[hx-disable], [data-hx-disable]"` - array of strings: htmx will not process elements with this attribute on it or a parent
+* `scrollBehavior:'smooth'` - string: the behavior for a boosted link on page transitions. The allowed values are `auto` and `smooth`. Smooth will smoothscroll to the top of the page while auto will behave like a vanilla link.
+* `defaultFocusScroll:false` - boolean: if the focused element should be scrolled into view, can be overridden using the [focus-scroll](@/attributes/hx-swap.md#focus-scroll) swap modifier
+* `getCacheBusterParam:false` - boolean: if set to true htmx will include a cache-busting parameter in `GET` requests to avoid caching partial responses by the browser
+* `globalViewTransitions:false` - boolean: if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content.
+* `methodsThatUseUrlParams:["get"]` - array of strings: htmx will format requests with these methods by encoding their parameters in the URL, not the request body
+* `selfRequestsOnly:false` - boolean: if set to `true` will only allow AJAX requests to the same domain as the current document
+* `ignoreTitle:false` - boolean: if set to `true` htmx will not update the title of the document when a `title` tag is found in new content
* `scrollIntoViewOnBoost:true` - boolean: whether or not the target of a boosted element is scrolled into the viewport. If `hx-target` is omitted on a boosted element, the target defaults to `body`, causing the page to scroll to the top.
##### Example
diff --git a/www/content/attributes/hx-on.md b/www/content/attributes/hx-on.md
index 57bc03b3..2934fd4d 100644
--- a/www/content/attributes/hx-on.md
+++ b/www/content/attributes/hx-on.md
@@ -2,26 +2,28 @@
title = "hx-on"
+++
-The `hx-on` attribute allows you to embed scripts inline to respond to events directly on an element; similar to the [`onevent` properties](https://developer.mozilla.org/en-US/docs/Web/Events/Event_handlers#using_onevent_properties) found in HTML, such as `onClick`.
+The `hx-on*` attributes allow you to embed scripts inline to respond to events directly on an element; similar to the
+[`onevent` properties](https://developer.mozilla.org/en-US/docs/Web/Events/Event_handlers#using_onevent_properties) found in HTML, such as `onClick`.
-`hx-on` improves upon `onevent` by enabling the handling of any event for enhanced [Locality of Behaviour (LoB)](/essays/locality-of-behaviour/). This also enables you to handle any htmx event.
+The `hx-on*` attributes improve upon `onevent` by enabling the handling of any arbitrary JavaScript event,
+for enhanced [Locality of Behaviour (LoB)](/essays/locality-of-behaviour/) even when dealing with non-standard DOM events. For example, these
+attributes allow you to handle [htmx events](/reference#events).
-There are two forms of this attribute, one in which you specify the event as part of the attribute name
-after a colon (`hx-on:click`, for example), and a deprecated form that uses the `hx-on` attribute directly. The
-latter should only be used if IE11 support is required.
-
-### hx-on:* (recommended)
-The event name follows a colon `:` in the attribute, and the attribute value is the script to be executed:
+With `hx-on` attributes, you specify the event name as part of the attribute name, after a colon. So, for example, if
+you want to respond to a `click` event, you would use the attribute `hx-on:click`:
```html
<div hx-on:click="alert('Clicked!')">Click</div>
```
-All htmx events can be captured, too! Make sure to use the [kebab-case event name](@/docs.md#events),
-because DOM attributes do not preserve casing. For instance, `hx-on::beforeRequest` **will not work:**
-use `hx-on::before-request` instead.
+Note that this syntax can be used to capture all htmx events, as well as most other custom events, in addition to the
+standard DOM events.
+
+One gotcha to note is that DOM attributes do not preserve case. This means, unfortunately, an attribute like
+`hx-on:htmx:beforeRequest` **will not work**, because the DOM lowercases the attribute names. Fortunately, htmx supports
+both camel case event names and also [kebab-case event names](@/docs.md#events), so you can use `hx-on:htmx:before-request` instead.
-To make writing these a little easier, you can use the shorthand double-colon `hx-on::` for htmx
+In order to make writing htmx-based event handlers a little easier, you can use the shorthand double-colon `hx-on::` for htmx
events, and omit the "htmx" part:
```html
@@ -36,7 +38,7 @@ events, and omit the "htmx" part:
```
-Adding multiple handlers is easy, you just specify additional attributes:
+If you wish to handle multiple different events, you can simply add multiple attributes to an element:
```html
<button hx-get="/info"
hx-on::before-request="alert('Making a request!')"
@@ -45,6 +47,20 @@ Adding multiple handlers is easy, you just specify additional attributes:
</button>
```
+Finally, in order to make this feature compatible with some templating languages (e.g. [JSX](https://react.dev/learn/writing-markup-with-jsx)) that do not like having a colon (`:`)
+in HTML attributes, you may use dashes in the place of colons for both the long form and the shorthand form:
+
+```html
+<!-- These two are equivalent -->
+<button hx-get="/info" hx-on-htmx-before-request="alert('Making a request!')">
+ Get Info!
+</button>
+
+<button hx-get="/info" hx-on--before-request="alert('Making a request!')">
+ Get Info!
+</button>
+
+```
### hx-on (deprecated)
The value is an event name, followed by a colon `:`, followed by the script:
diff --git a/www/content/attributes/hx-trigger.md b/www/content/attributes/hx-trigger.md
index 14b6e367..ec5f1762 100644
--- a/www/content/attributes/hx-trigger.md
+++ b/www/content/attributes/hx-trigger.md
@@ -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)`)
diff --git a/www/content/docs.md b/www/content/docs.md
index 9ab4f795..1ec6486d 100644
--- a/www/content/docs.md
+++ b/www/content/docs.md
@@ -114,7 +114,7 @@ The fastest way to get going with htmx is to load it via a CDN. You can simply a
and get going:
```html
-<script src="https://unpkg.com/htmx.org@1.9.8" integrity="sha384-EAzY246d6BpbWR7sQ8+WEm40J8c3dHFsqC58IgPlh4kMbRRI6P6WA+LA/qGAyAu8" crossorigin="anonymous"></script>
+<script src="https://unpkg.com/htmx.org@1.9.10" integrity="sha384-TODO" crossorigin="anonymous"></script>
```
While the CDN approach is extremely simple, you may want to consider [not using CDNs in production](https://blog.wesleyac.com/posts/why-not-javascript-cdn).
@@ -171,6 +171,43 @@ window.htmx = require('htmx.org');
* Finally, rebuild your bundle
+### htmx 1.x to 2.x Upgrade Guide
+
+To upgrade to htmx 2.0 from htmx 1.0, you will need to do the following:
+
+* If you are still using the legacy `hx-ws` and `hx-sse` attributes, please upgrade to the extension versions (available in 1.x)
+ of those features
+* Default Changes
+ * If you want to retain the 1.0 behavior of "smooth scrolling" by default, revert `htmx.config.scrollBehavior` to `'smooth'`
+ * If you want `DELETE` requests to use a form-encoded body rather than parameters, revert
+ `htmx.config.methodsThatUseUrlParams` to `["get"]` (it's a little crazy, but `DELETE`, according to the spec, should
+ use request parameters.)
+ * If you want to make cross-domain requests with htmx, revert `htmx.config.selfRequestsOnly` to `false`
+* Convert any `hx-on` attributes to their `hx-on:` equivalent:
+ ```html
+ <button hx-get="/info" hx-on="htmx:beforeRequest: alert('Making a request!')
+ htmx:afterRequest: alert('Done making a request!')">
+ Get Info!
+ </button>
+ ```
+ becomes:
+ ```html
+ <button hx-get="/info" hx-on:htmx:before-request="alert('Making a request!')"
+ hx-on:htmx:after-request="alert('Done making a request!')">
+ Get Info!
+ </button>
+ Note that you must use the kebab-case of the event name due to the fact that attributes are case-insensitive in HTML.
+ ```
+
+here is a meta tag to revert to htmx 1.x defaults:
+
+```html
+<meta name="htmx-config" content='{"scrollBehavior":"smooth", "methodsThatUseUrlParams":["get"], "selfRequestsOnly": false}'>
+```
+
+IE is no longer supported in htmx 2.0, but htmx 1.x continues to support IE and will be supported for the foreseeable
+future.
+
## AJAX
The core of htmx is a set of attributes that allow you to issue AJAX requests directly from HTML:
@@ -1258,7 +1295,7 @@ Scripting solutions that pair well with htmx include:
team that created htmx. It is designed to embed well in HTML and both respond to and create events, and pairs very well
with htmx.
-### <a name="hx-on"></a>[The `hx-on` Attribute](#hyperscript)
+### <a name="hx-on"></a>[The `hx-on*` Attributes](#hx-on)
HTML allows the embedding of inline scripts via the [`onevent` properties](https://developer.mozilla.org/en-US/docs/Web/Events/Event_handlers#using_onevent_properties),
such as `onClick`:
@@ -1271,35 +1308,45 @@ such as `onClick`:
This feature allows scripting logic to be co-located with the HTML elements the logic applies to, giving good
[Locality of Behaviour (LoB)](/essays/locality-of-behaviour). Unfortunately, HTML only allows `on*` attributes for a fixed
-number of specific DOM events (e.g. `onclick`) and doesn't offer a way to respond generally to events in this embedded
-manner.
+number of [specific DOM events](https://www.w3schools.com/tags/ref_eventattributes.asp) (e.g. `onclick`) and
+doesn't provide a generalized mechanism for responding to arbitrary events on elements.
-In order to address this shortcoming, htmx offers the [`hx-on`](/attributes/hx-on) attribute. This attribute allows
-you to respond to any event in a manner that preserves the LoB of the `on*` properties:
+In order to address this shortcoming, htmx offers [`hx-on*`](/attributes/hx-on) attributes. These attributes allow
+you to respond to any event in a manner that preserves the LoB of the standard `on*` properties.
+
+If we wanted to respond to the `click` event using an `hx-on` attribute, we would write this:
```html
-<button hx-on="click: alert('You clicked me!')">
+<button hx-on:click="alert('You clicked me!')">
Click Me!
</button>
```
-For a `click` event, we would recommend sticking with the standard `onclick` attribute. However, consider an htmx-powered
-button that wishes to add an attribute to a request using the `htmx:configRequest` event. This would not be possible
-with an `on*` property, but can be done using the `hx-on` attribute:
+So, the string `hx-on`, followed by a colon (or a dahs), then by the name of the event.
+
+For a `click` event, of course, we would recommend sticking with the standard `onclick` attribute. However, consider an
+htmx-powered button that wishes to add a parameter to a request using the `htmx:config-request` event. This would not
+be possible using a standard `on*` property, but it can be done using the `hx-on:htmx:config-request` attribute:
```html
<button hx-post="/example"
- hx-on="htmx:configRequest: event.detail.parameters.example = 'Hello Scripting!'">
+ hx-on:htmx:config-request=": event.detail.parameters.example = 'Hello Scripting!'">
Post Me!
</button>
```
Here the `example` parameter is added to the `POST` request before it is issued, with the value 'Hello Scripting!'.
-The `hx-on` attribute is a very simple mechanism for generalized embedded scripting. It is _not_ a replacement for more
+The `hx-on*` attributes are a very simple mechanism for generalized embedded scripting. It is _not_ a replacement for more
fully developed front-end scripting solutions such as AlpineJS or hyperscript. It can, however, augment a VanillaJS-based
approach to scripting in your htmx-powered application.
+Note that HTML attributes are *case insensitive*. This means that, unfortunately, events that rely on capitalization/
+camel casing, cannot be responded to. If you need to support camel case events we recommend using a more fully
+functional scripting solution such as AlpineJS or hyperscript. htmx dispatches all its events in both camelCase and in
+kebab-case for this very reason.
+
+
### hyperscript
Hyperscript is an experimental front end scripting language designed to be expressive and easily embeddable directly in HTML
diff --git a/www/content/essays/_index.md b/www/content/essays/_index.md
index 7fa7b556..aeab75ab 100644
--- a/www/content/essays/_index.md
+++ b/www/content/essays/_index.md
@@ -22,6 +22,7 @@ page_template = "essay.html"
* [A Response To "Have SPAs Ruined The Web"](@/essays/a-response-to-rich-harris.md)
* [When To Use Hypermedia?](@/essays/when-to-use-hypermedia.md)
* [The API Churn/Security Trade-off](https://intercoolerjs.org/2016/02/17/api-churn-vs-security.html)
+* [Does Hypermedia Scale?](@/essays/does-hypermedia-scale.md)
* [SPA Alternative](@/essays/spa-alternative.md)
### Building Hypermedia Applications
@@ -30,6 +31,7 @@ page_template = "essay.html"
* [Hypermedia-Driven Applications (HDAs)](@/essays/hypermedia-driven-applications.md)
* [Hypermedia Friendly Scripting](@/essays/hypermedia-friendly-scripting.md)
* [10 Tips For Building SSR/HDA applications](@/essays/10-tips-for-SSR-HDA-apps.md)
+* [Why I Tend Not To Use Content Negotiation](@/essays/why-tend-not-to-use-content-negotiation.md)
* [Template Fragments](@/essays/template-fragments.md)
* [View Transitions](@/essays/view-transitions.md)
diff --git a/www/content/essays/does-hypermedia-scale.md b/www/content/essays/does-hypermedia-scale.md
index de737db2..304db83c 100644
--- a/www/content/essays/does-hypermedia-scale.md
+++ b/www/content/essays/does-hypermedia-scale.md
@@ -12,7 +12,7 @@ One objection that we sometimes hear to htmx and hypermedia is some variation of
> Well, it might work well for something small, but it won't scale.
It is always dangerous to provoke us with essay-fodder and so lets dig into this claim a bit and see if we can
-shed some light on whether [Hypermedia-Driven Applications]((@/essays/hypermedia-driven-applications.md)) (HDAs) can scale.
+shed some light on whether [Hypermedia-Driven Applications](@/essays/hypermedia-driven-applications.md) (HDAs) can scale.
## Scaling
diff --git a/www/content/essays/hypermedia-apis-vs-data-apis.md b/www/content/essays/hypermedia-apis-vs-data-apis.md
index b066d8dd..6d31233e 100644
--- a/www/content/essays/hypermedia-apis-vs-data-apis.md
+++ b/www/content/essays/hypermedia-apis-vs-data-apis.md
@@ -24,7 +24,7 @@ Hypermedia APIs:
Data APIs, on the other hand:
* Will not benefit dramatically from REST-fulness, beyond perhaps [Level 2 of the Richardson Maturity Model](https://en.wikipedia.org/wiki/Richardson_Maturity_Model)
-* Should strive for both regularity and expressivity due to the arbitrary data needs of consumers
+* Should strive for both regularity and expressiveness due to the arbitrary data needs of consumers
* Should be versioned and should be very stable within a particular version of the API
* Should be consumed by code, processed and then potentially presented to a human
diff --git a/www/content/essays/template-fragments.md b/www/content/essays/template-fragments.md
index bdb23f8e..9699edb3 100644
--- a/www/content/essays/template-fragments.md
+++ b/www/content/essays/template-fragments.md
@@ -143,6 +143,7 @@ Here are some known implementations of the fragment concept:
* PHP
* [Latte](https://latte.nette.org/en/template-inheritance#toc-blocks) - Use the 3rd parameter to only render 1 block from the template - `$Latte_Engine->render('path/to/template.latte', [ 'foo' => 'bar' ], 'content');`
* [Laravel Blade](https://laravel.com/docs/10.x/blade#rendering-blade-fragments) - includes built-in support for template fragments as of v9.x
+ * [Twig](https://twig.symfony.com/doc/3.x/api.html#rendering-templates) - `$template->renderBlock('block_name', ['the' => 'variables', 'go' => 'here']);`
* Python
* [Django Render Block Extension](https://pypi.org/project/django-render-block/) - see [example code for htmx](https://github.com/spookylukey/django-htmx-patterns/blob/master/inline_partials.rst)
* [jinja2-fragments package](https://github.com/sponsfreixes/jinja2-fragments)
diff --git a/www/content/essays/why-tend-not-to-use-content-negotiation.md b/www/content/essays/why-tend-not-to-use-content-negotiation.md
new file mode 100644
index 00000000..952ec6cb
--- /dev/null
+++ b/www/content/essays/why-tend-not-to-use-content-negotiation.md
@@ -0,0 +1,177 @@
++++
+title = "Why I Tend Not To Use Content Negotiation"
+date = 2023-11-18
+updated = 2023-11-18
+[taxonomies]
+author = ["Carson Gross"]
+tag = ["posts"]
++++
+
+I have written a lot about Hypermedia APIs vs. Data (JSON) APIs, including [the differences between the two](@/essays/hypermedia-apis-vs-data-apis.md),
+what [REST "really" means](@/essays/how-did-rest-come-to-mean-the-opposite-of-rest.md) and why [HATEOAS](@/essays/hateoas.md)
+isn't so bad as long as your API is interacting with a [Hypermedia Client](@/essays/hypermedia-clients.md).
+
+Often when I am engaged in discussions with people coming from the "REST is JSON over HTTP" world (that is, the normal
+world) I have to navigate a lot of language and conceptual issues:
+
+* No, I am not advocating you return HTML as a general purpose API, hypermedia makes for a bad general purpose API
+* Yes, I am advocating [tightly coupling](@/essays/two-approaches-to-decoupling.md) your web application to your hypermedia API
+* No, I do not think that we will ever fix how the industry [uses the term REST](@/essays/how-did-rest-come-to-mean-the-opposite-of-rest.md)
+* Yes, I am advocating you [split your data API and your hypermedia API up](@/essays/splitting-your-apis.md)
+
+The last point often strikes people who are used to a single, general purpose JSON API as dumb: why have two APIs when you
+can have a single API that can satisfy any number of types of clients? I tried to answer that question as best I can in the essay
+above, but it is certainly a reasonable one to ask.
+
+It seems like (and it is) extra work in some ways when compared to having one general API.
+
+At this point in a conversation, someone who agrees broadly with my take on REST, [Hypermedia-Driven Applications](@/essays/hypermedia-driven-applications.md),
+etc. will often jump in and say something like
+
+> "Oh, it's easy, you just use _content negotiation_, it's baked into HTTP!"
+
+Not being content with alienating only the general purpose JSON API enthusiasts, let me now proceed to also alienate
+my erstwhile hypermedia enthusiast allies by saying:
+
+*I don't think content negotiation is typically the right approach to
+returning both JSON and HTML for most applications.*
+
+## What Is Content Negotiation?
+
+First things first, what is "content negotiation"?
+
+[Content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) is a feature of HTTP that
+allows a client to negotiate the content type of the response from a server. A full treatment of the implementation
+in HTTP is beyond the scope of this essay, but let us consider the most well known mechanism for content negotiation
+in HTTP, the [`Accept` Request Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#the_accept_header).
+
+The `Accept` request header allows a client, such as a browser, to indicate the `MIME` types that it is willing to accept
+from the server in a response.
+
+An example value of this header is:
+
+```http request
+Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8
+```
+
+This `Accept` header tells the server what formats the client is willing to accept. Preferences are expressed via the
+`q` weighting factor. Wildcards are expressed with asterisks `*`.
+
+In this case, the client is saying:
+
+> I would most like to receive text/html, application/xhtml+xml or image/webp. Next I would prefer application/xml. Finally, I will accept whatever you give me.
+
+The server then can take this information and determine the best content type to provide to the client.
+
+This is the act of "content negotiation" and it is certainly an interesting feature of HTTP.
+
+## Using Content Negotiation In APIs
+
+As far as I am aware, it was the [Ruby On Rails](https://rubyonrails.org/) community that first went in in a big way
+using content negotiation to provide both HTML and JSON (and other) formats from the same URL.
+
+In Rails, this is accomplished via the [`respond_to`](https://apidock.com/rails/ActionController/MimeResponds/respond_to) helper method available in
+controllers.
+
+Leaving the gory details of Rails aside, you might have a request like an HTTP `GET` to `/contacts` that ends up invoking
+a function in a `ContactsController` class that looks like this:
+
+```ruby
+def index
+ @contacts = Contacts.all
+
+ respond_to do |format|
+ format.html # default rendering logic
+ format.json { render json: @contacts }
+ end
+end
+```
+
+By making use of the `respond_to` helper method, if a client makes a request with the `Accept` header above, the controller
+will render an HTML response using the Rails templating systems.
+
+However, if the `Accept` header from the client has the value `application/json` instead, Rails will render the contacts
+as a JSON array for the client.
+
+A pretty neat trick: you can keep all your controller logic, like looking up the contacts, the same and just use a
+bit of ruby/Rails magic to render two different response types using content negotiation. Barely any additional work on
+top of the normal Model/View/Controller logic.
+
+You can see why people like the idea!
+
+## So What's The Problem?
+
+So why don't I think this is a good approach to splitting your JSON and HTML APIs up?
+
+It boils down to the [differences between JSON APIs and Hypermedia (HTML) APIs](hypermedia-apis-vs-data-apis.md) I hinted
+at earlier. In particular:
+
+* Data APIs should be versioned and should be very stable within a particular version of the API
+* Data APIs should strive for both regularity and expressiveness due to the arbitrary data needs of consumers
+* Data APIs typically use some sort of token-based authentication
+* Data APIs should be rate limited
+* Hypermedia APIs typically use some sort of session-cookie based authentication
+* Hypermedia APIs should be driven by the needs of the underlying hypermedia application
+
+While all of these differences matter and have an effect on your controller code, pulling it in two different directions,
+it is really the first and last items that make me often choose not to use content negotiation in my applications.
+
+Your JSON API needs to be a stable set of endpoint that client code can rely on.
+
+Your hypermedia API, on the other hand, can change dramatically based on the user interface needs of your applications.
+
+These two things don't mix well.
+
+To give you a concrete example, consider an end point that renders a detail view of a contact, at, say `/contacts/:id`
+(where `:id` is a parameter containing the id of the contact to render). Let's say that this page has a "related contacts"
+section of the UI and, further, computing these related contacts is expensive for some reason.
+
+In this situation you might choose to use the [Lazy Loading](https://htmx.org/examples/lazy-load/) pattern to defer
+loading the related contacts until after the initial contact detail screen has been rendered. This improves perceived
+performance of the page for your users.
+
+If you did this, you might put the lazy loaded content at the end-point `/contacts/:id/related`.
+
+Now, later on, maybe you are able to optimize the computation of related contacts. At this point you might choose to
+rip the `/contacts/:id/related` end-point out and just render the related contacts information in the initial page render.
+
+All of this is fine for your hypermedia API: hypermedia, through [the uniform interface & HATEOAS](@/essays/hateoas.md)
+is _designed_ to handle these sorts of changes.
+
+However, your JSON API... not so much.
+
+Your JSON API should remain stable. You can't be adding and removing end-points
+willy-nilly. Yes, you can have _some_ end-points respond with either JSON or HTML and others only respond with HTML, but
+it gets messy. What if you accidentally copy-and-paste in the wrong code somewhere, for example.
+
+Taking all of this into account, as well as things like rate-limiting and so on, I think you can make a strong argument
+that there should be a [Separation Of Concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) between the JSON
+API and the hypermedia API.
+
+(Yes, I am aware of the irony that the person who coined the term [Locality of Behaviour](@/essays/locality-of-behaviour.md)
+is making a SoC argument.)
+
+## So What's The Alternative?
+
+The alternative is to, as I advocate in [Splitting Your APIs](@/essays/splitting-your-apis.md), erm, splitting your
+APIs. This means providing different paths (or sub-domains, or whatever) for your JSON API and your hypermedia (HTML)
+API.
+
+Going back to our contacts API, we might have the following:
+
+* The JSON API to get all contacts is found at `/api/v1/contacts`
+* The Hypermedia API to get all contacts is found at `/contacts`
+
+This layout implies two different controllers and, I say, that's a good thing: the JSON API controller can implement the
+requirements of a JSON API: rate limiting, stability, maybe an expressive query mechanism like GraphQL.
+
+Meanwhile, your
+hypermedia API (really, just your Hypermedia Driven Application endpoints) can change dramatically as your user interface
+needs change, with highly tuned database queries, end-points to support special UI needs, etc.
+
+By separating these two concerns, your JSON API can be stable, regular and low-maintenance, and your hypermedia API can
+be chaotic, specialized and flexible. Each gets its own controller environment to thrive in, without conflicting with
+one another.
+
+And this is why I prefer to split my JSON and hypermedia APIs up into separate controllers, rather than use HTTP content
+negotiation to attempt to reuse controllers for both.
diff --git a/www/content/examples/_index.md b/www/content/examples/_index.md
index a9311317..394f1e9c 100644
--- a/www/content/examples/_index.md
+++ b/www/content/examples/_index.md
@@ -31,7 +31,7 @@ You can copy and paste them and then adjust them for your needs.
| [File Upload](@/examples/file-upload.md) | Demonstrates how to upload a file via ajax with a progress bar
| [Preserving File Inputs after Form Errors](@/examples/file-upload-input.md) | Demonstrates how to preserve file inputs after form errors
| [Dialogs - Browser](@/examples/dialogs.md) | Demonstrates the prompt and confirm dialogs
-| [Dialogs - UIKIt](@/examples/modal-uikit.md) | Demonstrates modal dialogs using UIKit
+| [Dialogs - UIKit](@/examples/modal-uikit.md) | Demonstrates modal dialogs using UIKit
| [Dialogs - Bootstrap](@/examples/modal-bootstrap.md) | Demonstrates modal dialogs using Bootstrap
| [Dialogs - Custom](@/examples/modal-custom.md) | Demonstrates modal dialogs from scratch
| [Tabs (Using HATEOAS)](@/examples/tabs-hateoas.md) | Demonstrates how to display and select tabs using HATEOAS principles
diff --git a/www/content/examples/active-search.md b/www/content/examples/active-search.md
index 1f48acd1..242a4c21 100644
--- a/www/content/examples/active-search.md
+++ b/www/content/examples/active-search.md
@@ -17,7 +17,7 @@ We start with a search input and an empty table:
<input class="form-control" type="search"
name="search" placeholder="Begin Typing To Search Users..."
hx-post="/search"
- hx-trigger="keyup changed delay:500ms, search"
+ hx-trigger="input changed delay:500ms, search"
hx-target="#search-results"
hx-indicator=".htmx-indicator">
@@ -34,16 +34,16 @@ We start with a search input and an empty table:
</table>
```
-The input issues a `POST` to `/search` on the `keyup` event and sets the body of the table to be the resulting content.
+The input issues a `POST` to `/search` on the [`input`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/input_event) event and sets the body of the table to be the resulting content. Note that the `keyup` event could be used as well, but would not fire if the user pasted text with their mouse (or any other non-keyboard method).
We add the `delay:500ms` modifier to the trigger to delay sending the query until the user stops typing. Additionally,
we add the `changed` modifier to the trigger to ensure we don't send new queries when the user doesn't change the
-value of the input (e.g. they hit an arrow key).
+value of the input (e.g. they hit an arrow key, or pasted the same value).
Since we use a `search` type input we will get an `x` in the input field to clear the input.
To make this trigger a new `POST` we have to specify another trigger. We specify another trigger by using a comma to
separate them. The `search` trigger will be run when the field is cleared but it also makes it possible to override
-the 500 ms delay on `keyup` by just pressing enter.
+the 500 ms `input` event delay by just pressing enter.
Finally, we show an indicator when the search is in flight with the `hx-indicator` attribute.
@@ -78,7 +78,7 @@ Search Contacts
<input class="form-control" type="search"
name="search" placeholder="Begin Typing To Search Users..."
hx-post="/search"
- hx-trigger="keyup changed delay:500ms, search"
+ hx-trigger="input changed delay:500ms, search"
hx-target="#search-results"
hx-indicator=".htmx-indicator">
diff --git a/www/content/reference.md b/www/content/reference.md
index 2e3b05c3..98b31bce 100644
--- a/www/content/reference.md
+++ b/www/content/reference.md
@@ -12,6 +12,7 @@ title = "Reference"
* [htmx Events](#events)
* [htmx Extensions](/extensions#included)
* [JavaScript API](#api)
+* [Configuration Options](#config)
## Core Attribute Reference {#attributes}
@@ -24,7 +25,7 @@ The following are the most common attributes when using htmx.
| [`hx-boost`](@/attributes/hx-boost.md) | add or remove [progressive enhancement](https://en.wikipedia.org/wiki/Progressive_enhancement) for links and forms |
| [`hx-get`](@/attributes/hx-get.md) | issues a `GET` to the specified URL |
| [`hx-post`](@/attributes/hx-post.md) | issues a `POST` to the specified URL |
-| [`hx-on`](@/attributes/hx-on.md) | handle any event with a script inline |
+| [`hx-on*`](@/attributes/hx-on.md) | handle events with a inline scripts on elements |
| [`hx-push-url`](@/attributes/hx-push-url.md) | pushes the URL into the browser location bar, creating a new history entry |
| [`hx-select`](@/attributes/hx-select.md) | select content to swap in from a response |
| [`hx-select-oob`](@/attributes/hx-select-oob.md) | select content to swap in from a response, out of band (somewhere other than the target) |
@@ -207,3 +208,51 @@ The table below lists all other attributes available in htmx.
</div>
+
+## Configuration Reference {#config}
+
+Htmx has some configuration options that can be accessed either programmatically or declaratively. They are
+listed below:
+
+<div class="info-table">
+
+| Config Variable | Info |
+|---------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `htmx.config.historyEnabled` | defaults to `true`, really only useful for testing |
+| `htmx.config.historyCacheSize` | defaults to 10 |
+| `htmx.config.refreshOnHistoryMiss` | defaults to `false`, if set to `true` htmx will issue a full page refresh on history misses rather than use an AJAX request |
+| `htmx.config.defaultSwapStyle` | defaults to `innerHTML` |
+| `htmx.config.defaultSwapDelay` | defaults to 0 |
+| `htmx.config.defaultSettleDelay` | defaults to 20 |
+| `htmx.config.includeIndicatorStyles` | defaults to `true` (determines if the indicator styles are loaded) |
+| `htmx.config.indicatorClass` | defaults to `htmx-indicator` |
+| `htmx.config.requestClass` | defaults to `htmx-request` |
+| `htmx.config.addedClass` | defaults to `htmx-added` |
+| `htmx.config.settlingClass` | defaults to `htmx-settling` |
+| `htmx.config.swappingClass` | defaults to `htmx-swapping` |
+| `htmx.config.allowEval` | defaults to `true`, can be used to disable htmx's use of eval for certain features (e.g. trigger filters) |
+| `htmx.config.allowScriptTags` | defaults to `true`, determines if htmx will process script tags found in new content |
+| `htmx.config.inlineScriptNonce` | defaults to `''`, meaning that no nonce will be added to inline scripts |
+| `htmx.config.attributesToSettle` | defaults to `["class", "style", "width", "height"]`, the attributes to settle during the settling phase |
+| `htmx.config.useTemplateFragments` | defaults to `false`, HTML template tags for parsing content from the server (not IE11 compatible!) |
+| `htmx.config.wsReconnectDelay` | defaults to `full-jitter` |
+| `htmx.config.wsBinaryType` | defaults to `blob`, the [the type of binary data](https://developer.mozilla.org/docs/Web/API/WebSocket/binaryType) being received over the WebSocket connection |
+| `htmx.config.disableSelector` | defaults to `[hx-disable], [data-hx-disable]`, htmx will not process elements with this attribute on it or a parent |
+| `htmx.config.withCredentials` | defaults to `false`, allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates |
+| `htmx.config.timeout` | defaults to 0, the number of milliseconds a request can take before automatically being terminated |
+| `htmx.config.scrollBehavior` | defaults to 'smooth', the behavior for a boosted link on page transitions. The allowed values are `auto` and `smooth`. Smooth will smoothscroll to the top of the page while auto will behave like a vanilla link. |
+| `htmx.config.defaultFocusScroll` | if the focused element should be scrolled into view, defaults to false and can be overridden using the [focus-scroll](@/attributes/hx-swap.md#focus-scroll) swap modifier. |
+| `htmx.config.getCacheBusterParam` | defaults to false, if set to true htmx will include a cache-busting parameter in `GET` requests to avoid caching partial responses by the browser |
+| `htmx.config.globalViewTransitions` | if set to `true`, htmx will use the [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) API when swapping in new content. |
+| `htmx.config.methodsThatUseUrlParams` | defaults to `["get"]`, htmx will format requests with these methods by encoding their parameters in the URL, not the request body |
+| `htmx.config.selfRequestsOnly` | defaults to `false`, if set to `true` will only allow AJAX requests to the same domain as the current document |
+| `htmx.config.ignoreTitle` | defaults to `false`, if set to `true` htmx will not update the title of the document when a `title` tag is found in new content |
+| `htmx.config.scrollIntoViewOnBoost` | defaults to `true`, whether or not the target of a boosted element is scrolled into the viewport. If `hx-target` is omitted on a boosted element, the target defaults to `body`, causing the page to scroll to the top. |
+
+</div>
+
+You can set them directly in javascript, or you can use a `meta` tag:
+
+```html
+<meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML"}'>
+```
diff --git a/www/content/server-examples.md b/www/content/server-examples.md
index 21da2c3e..5f6b3f54 100644
--- a/www/content/server-examples.md
+++ b/www/content/server-examples.md
@@ -138,6 +138,15 @@ These examples may make it a bit easier to get started using htmx with your plat
- <https://github.com/michalsn/codeigniter-htmx-demo>
+
+### Laravel
+
+- <https://spirofloropoulos.com/laravel-htmx-hard-mode-1.html>
+
+### Symfony
+
+- <https://github.com/tomcri/htmxfony>
+
## Elixir
### Phoenix
diff --git a/www/static/img/blackhost-logo.svg b/www/static/img/blackhost-logo.svg
new file mode 100644
index 00000000..9b96afc4
--- /dev/null
+++ b/www/static/img/blackhost-logo.svg
@@ -0,0 +1,33 @@
+<!-- Generator: Adobe Illustrator 25.1.0, SVG Export Plug-In -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="189.51px" height="22.2px" viewBox="0 0 189.51 22.2" style="overflow:visible;enable-background:new 0 0 189.51 22.2;"
+ xml:space="preserve">
+<style type="text/css">
+ .st0{fill:#00;}
+</style>
+<defs>
+</defs>
+<g>
+ <path class="st0" d="M16.59,16.11c0,4.11-2.34,5.94-6.63,5.94H0V0.15h8.04c4.56,0,6.9,2.07,6.9,5.97c0,3.15-2.04,4.14-2.73,4.35
+ C14.64,11.01,16.59,12.66,16.59,16.11z M3.15,3.3v6.27h4.71c2.67,0,3.93-0.99,3.93-3.09c0-2.01-1.2-3.18-4.14-3.18H3.15z M9.6,18.9
+ c2.55,0,3.84-0.84,3.84-3.03c0-2.1-1.26-3.39-4.14-3.39H3.15v6.42H9.6z"/>
+ <path class="st0" d="M32.73,18.9v3.15h-13.5V0.15h3.15V18.9H32.73z"/>
+ <path class="st0" d="M50.67,22.05c-0.84-2.37-1.65-4.62-2.4-6.63h-9.48c-0.72,2.01-1.53,4.26-2.37,6.63h-3.3
+ c4.89-13.89,8.46-21.9,8.46-21.9h3.9c0,0,3.57,8.01,8.49,21.9H50.67z M47.19,12.48C45,6.6,43.53,3.03,43.53,3.03
+ s-1.44,3.57-3.63,9.45H47.19z"/>
+ <path class="st0" d="M65.22,0c6,0,8.19,2.85,8.85,3.72c-0.54,1.8-1.74,2.91-1.74,2.91c-1.05-1.5-2.76-3.51-7.11-3.51
+ c-4.29,0-7.89,2.55-7.89,7.98c0,5.76,3.48,7.98,7.89,7.98c4.26,0,6.36-1.86,7.35-3.45c0,0,1.08,0.99,1.71,2.67
+ c-0.63,0.96-3.03,3.9-9.03,3.9c-6.27,0-11.07-3.63-11.07-11.1C54.18,3.87,58.95,0,65.22,0z"/>
+ <path class="st0" d="M91.68,22.05c-2.82-4.41-5.28-7.68-6.33-9.03c-1.56,0.96-3.36,1.71-5.37,2.28v6.75h-3.15V0.15h3.15v12
+ c5.16-1.41,8.91-4.77,10.8-12h3.15c-1.44,5.07-3.39,8.55-6.03,10.98c1.08,1.41,4.05,5.4,7.47,10.92H91.68z"/>
+ <path class="st0" d="M125.61,0.15v21.9h-4.95V14.1h-11.1v7.95h-4.95V0.15h4.95v9h11.1v-9H125.61z"/>
+ <path class="st0" d="M139.26,0c6.3,0,10.95,3.75,10.95,11.1c0,7.35-4.65,11.1-10.95,11.1s-10.95-3.75-10.95-11.1
+ C128.31,3.75,132.96,0,139.26,0z M139.26,17.25c3.75,0,6-2.4,6-6.15c0-3.75-2.25-6.15-6-6.15s-6,2.4-6,6.15
+ C133.26,14.85,135.51,17.25,139.26,17.25z"/>
+ <path class="st0" d="M164.91,7.95c0,0,0.3-0.45,0.3-1.35c0-1.35-0.75-2.37-3.9-2.37c-3,0-4.05,1.02-4.05,2.37
+ c0,4.5,13.2,0.9,13.2,9c0,4.2-2.55,6.6-9,6.6c-6.75,0-8.85-2.7-9.3-6.45c0,0,1.8-0.75,4.65-1.05c0.3,2.25,1.5,3.45,4.95,3.45
+ c3,0,4.05-0.75,4.05-2.25c0-4.2-13.35-0.75-13.35-9.45c0-3.75,2.55-6.45,9-6.45c6.75,0,8.7,2.7,8.7,5.55c0,1.8-0.45,2.4-0.45,2.4
+ H164.91z"/>
+ <path class="st0" d="M189.51,5.1h-6.6v16.95h-4.95V5.1h-6.6V0.15h18.15V5.1z"/>
+</g>
+</svg>
diff --git a/www/static/js/demo.js b/www/static/js/demo.js
index 037006de..1228b4e3 100644
--- a/www/static/js/demo.js
+++ b/www/static/js/demo.js
@@ -28,8 +28,8 @@ function parseParams(str) {
str = str.substr(1);
}
while (e = re.exec(str)) {
- var k = decode(e[1]);
- var v = decode(e[2]);
+ var k = encodeHTML(decode(e[1]));
+ var v = encodeHTML(decode(e[2]));
if (params[k] !== undefined) {
if (!Array.isArray(params[k])) {
params[k] = [params[k]];
@@ -52,6 +52,10 @@ function getQuery(url) {
url.substring(question + 1, hash);
}
+function encodeHTML(s) {
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/"/g, '&quot;');
+}
+
function params(request) {
if (server.getHTTPMethod(request) == "GET") {
return parseParams(getQuery(request.url));
diff --git a/www/static/src/ext/ws.js b/www/static/src/ext/ws.js
index 3366e9e5..05be1eca 100644
--- a/www/static/src/ext/ws.js
+++ b/www/static/src/ext/ws.js
@@ -200,7 +200,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
if (!this.socket) {
api.triggerErrorEvent()
}
- if (sendElt && api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
+ if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
message: message,
socketWrapper: this.publicInterface
})) {
diff --git a/www/static/src/htmx.d.ts b/www/static/src/htmx.d.ts
index 2342300c..6200cd3f 100644
--- a/www/static/src/htmx.d.ts
+++ b/www/static/src/htmx.d.ts
@@ -48,7 +48,7 @@ export function ajax(verb: string, path: string, selector: string): Promise<void
export function ajax(
verb: string,
path: string,
- context: Partial<{ source: any; event: any; handler: any; target: any; swap: any; values: any; headers: any }>
+ context: Partial<{ source: any; event: any; handler: any; target: any; swap: any; values: any; headers: any; select: any }>
): Promise<void>;
/**
@@ -395,6 +395,11 @@ export interface HtmxConfig {
* @default false
*/
selfRequestsOnly?: boolean;
+ /**
+ * Whether or not the target of a boosted element is scrolled into the viewport.
+ * @default true
+ */
+ scrollIntoViewOnBoost?: boolean;
}
/**
diff --git a/www/static/src/htmx.js b/www/static/src/htmx.js
index 79a4702e..1f0709d7 100644
--- a/www/static/src/htmx.js
+++ b/www/static/src/htmx.js
@@ -75,6 +75,7 @@ return (function () {
globalViewTransitions: false,
methodsThatUseUrlParams: ["get"],
selfRequestsOnly: false,
+ ignoreTitle: false,
scrollIntoViewOnBoost: true
},
parseInterval:parseInterval,
@@ -87,7 +88,7 @@ return (function () {
sock.binaryType = htmx.config.wsBinaryType;
return sock;
},
- version: "1.9.8"
+ version: "1.9.9"
};
/** @type {import("./htmx").HtmxInternalApi} */
@@ -1145,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;
@@ -1233,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';
/**
@@ -1281,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 {
@@ -2906,6 +2925,7 @@ return (function () {
values : context.values,
targetOverride: resolveTarget(context.target),
swapOverride: context.swap,
+ select: context.select,
returnPromise: true
});
}
@@ -2960,6 +2980,7 @@ return (function () {
elt = getDocument().body;
}
var responseHandler = etc.handler || handleAjaxResponse;
+ var select = etc.select || null;
if (!bodyContains(elt)) {
// do not issue requests for elements removed from the DOM
@@ -3108,6 +3129,11 @@ return (function () {
var headers = getHeaders(elt, target, promptResponse);
+
+ if (verb !== 'get' && !usesFormData(elt)) {
+ headers['Content-Type'] = 'application/x-www-form-urlencoded';
+ }
+
if (etc.headers) {
headers = mergeObjects(headers, etc.headers);
}
@@ -3121,10 +3147,6 @@ return (function () {
var allParameters = mergeObjects(rawParameters, expressionVars);
var filteredParameters = filterValues(allParameters, elt);
- if (verb !== 'get' && !usesFormData(elt)) {
- headers['Content-Type'] = 'application/x-www-form-urlencoded';
- }
-
if (htmx.config.getCacheBusterParam && verb === 'get') {
filteredParameters['org.htmx.cache-buster'] = getRawAttribute(target, "id") || "true";
}
@@ -3222,7 +3244,7 @@ return (function () {
}
var responseInfo = {
- xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted,
+ xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted, select: select,
pathInfo: {
requestPath: path,
finalRequestPath: finalPath,
@@ -3393,6 +3415,7 @@ return (function () {
var target = responseInfo.target;
var etc = responseInfo.etc;
var requestConfig = responseInfo.requestConfig;
+ var select = responseInfo.select;
if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return;
@@ -3502,10 +3525,26 @@ return (function () {
}
var selectOverride;
+ if (select) {
+ selectOverride = select;
+ }
+
if (hasHeader(xhr, /HX-Reselect:/i)) {
selectOverride = xhr.getResponseHeader("HX-Reselect");
}
+ // if we need to save history, do so, before swapping so that relative resources have the correct base URL
+ if (historyUpdate.type) {
+ triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
+ if (historyUpdate.type === "push") {
+ pushUrlIntoHistory(historyUpdate.path);
+ triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
+ } else {
+ replaceUrlInHistory(historyUpdate.path);
+ triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
+ }
+ }
+
var settleInfo = makeSettleInfo(target);
selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, selectOverride);
@@ -3555,17 +3594,6 @@ return (function () {
triggerEvent(elt, 'htmx:afterSettle', responseInfo);
});
- // if we need to save history, do so
- if (historyUpdate.type) {
- triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
- if (historyUpdate.type === "push") {
- pushUrlIntoHistory(historyUpdate.path);
- triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
- } else {
- replaceUrlInHistory(historyUpdate.path);
- triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
- }
- }
if (responseInfo.pathInfo.anchor) {
var anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor);
if(anchorTarget) {
@@ -3724,25 +3752,34 @@ return (function () {
//====================================================================
// Initialization
//====================================================================
- var isReady = false
- getDocument().addEventListener('DOMContentLoaded', function() {
- isReady = true
- })
-
/**
- * Execute a function now if DOMContentLoaded has fired, otherwise listen for it.
- *
- * This function uses isReady because there is no realiable way to ask the browswer whether
- * the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded
- * firing and readystate=complete.
+ * We want to initialize the page elements after DOMContentLoaded
+ * fires, but there isn't always a good way to tell whether
+ * it has already fired when we get here or not.
*/
- function ready(fn) {
- // Checking readyState here is a failsafe in case the htmx script tag entered the DOM by
- // some means other than the initial page load.
- if (isReady || getDocument().readyState === 'complete') {
- fn();
- } else {
- getDocument().addEventListener('DOMContentLoaded', fn);
+ function ready(functionToCall) {
+ // call the function exactly once no matter how many times this is called
+ var callReadyFunction = function() {
+ if (!functionToCall) return;
+ functionToCall();
+ functionToCall = null;
+ };
+
+ if (getDocument().readyState === "complete") {
+ // DOMContentLoaded definitely fired, we can initialize the page
+ callReadyFunction();
+ }
+ else {
+ /* DOMContentLoaded *maybe* already fired, wait for
+ * the next DOMContentLoaded or readystatechange event
+ */
+ getDocument().addEventListener("DOMContentLoaded", function() {
+ callReadyFunction();
+ });
+ getDocument().addEventListener("readystatechange", function() {
+ if (getDocument().readyState !== "complete") return;
+ callReadyFunction();
+ });
}
}
@@ -3750,9 +3787,9 @@ return (function () {
if (htmx.config.includeIndicatorStyles !== false) {
getDocument().head.insertAdjacentHTML("beforeend",
"<style>\
- ." + htmx.config.indicatorClass + "{opacity:0;transition: opacity 200ms ease-in;}\
- ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1}\
- ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1}\
+ ." + htmx.config.indicatorClass + "{opacity:0}\
+ ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\
+ ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\
</style>");
}
}
diff --git a/www/static/test/attributes/hx-boost.js b/www/static/test/attributes/hx-boost.js
index 7866abec..f8698a33 100644
--- a/www/static/test/attributes/hx-boost.js
+++ b/www/static/test/attributes/hx-boost.js
@@ -116,4 +116,3 @@ describe("hx-boost attribute", function() {
});
});
-
diff --git a/www/static/test/attributes/hx-trigger.js b/www/static/test/attributes/hx-trigger.js
index 5e2f138b..0cbb4adf 100644
--- a/www/static/test/attributes/hx-trigger.js
+++ b/www/static/test/attributes/hx-trigger.js
@@ -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>');
+ });
+
})
diff --git a/www/static/test/core/ajax.js b/www/static/test/core/ajax.js
index 7d18be73..00e677df 100644
--- a/www/static/test/core/ajax.js
+++ b/www/static/test/core/ajax.js
@@ -1284,6 +1284,8 @@ describe("Core htmx AJAX Tests", function(){
byId("submit").click();
this.server.respond();
responded.should.equal(true);
+ })
+
it("can associate submit buttons from outside a form with the current version of the form after swap", function(){
const template = '<form ' +
'id="hello" ' +
diff --git a/www/static/test/core/api.js b/www/static/test/core/api.js
index 8a1735c4..431633d6 100644
--- a/www/static/test/core/api.js
+++ b/www/static/test/core/api.js
@@ -225,6 +225,24 @@ describe("Core htmx API test", function(){
div.innerHTML.should.equal('<p class="test">foo!</p>');
});
+ it('ajax api works with select', function()
+ {
+ this.server.respondWith("GET", "/test", "<div id='d1'>foo</div><div id='d2'>bar</div>");
+ var div = make("<div id='target'></div>");
+ htmx.ajax("GET", "/test", {target: "#target", select: "#d2"});
+ this.server.respond();
+ div.innerHTML.should.equal('<div id="d2">bar</div>');
+ });
+
+ it('ajax api works with Hx-Select overrides select', function()
+ {
+ this.server.respondWith("GET", "/test", [200, {"HX-Reselect": "#d2"}, "<div id='d1'>foo</div><div id='d2'>bar</div>"]);
+ var div = make("<div id='target'></div>");
+ htmx.ajax("GET", "/test", {target: "#target", select: "#d1"});
+ this.server.respond();
+ div.innerHTML.should.equal('<div id="d2">bar</div>');
+ });
+
it('ajax returns a promise', function(done)
{
// in IE we do not return a promise
@@ -255,6 +273,44 @@ describe("Core htmx API test", function(){
div.innerHTML.should.equal("Clicked!");
});
+ it('ajax api Content-Type header is application/x-www-form-urlencoded', function(){
+
+ this.server.respondWith("POST", "/test", function (xhr) {
+ var params = getParameters(xhr);
+ xhr.requestHeaders['Content-Type'].should.equal('application/x-www-form-urlencoded;charset=utf-8');
+ params['i1'].should.equal("test");
+ xhr.respond(200, {}, "Clicked!")
+ });
+ var div = make("<div id='d1'></div>");
+ htmx.ajax("POST", "/test", {target:"#d1", values:{i1: 'test'}})
+ this.server.respond();
+ div.innerHTML.should.equal("Clicked!");
+ });
+
+ it('ajax api Content-Type header override to application/json', function(){
+
+ this.server.respondWith("POST", "/test", function (xhr) {
+ var params = getParameters(xhr);
+ xhr.requestHeaders['Content-Type'].should.equal('application/json;charset=utf-8');
+ params['i1'].should.equal("test");
+ xhr.respond(200, {}, "Clicked!");
+ });
+
+ var div = make("<div id='d1'></div>");
+ htmx.ajax('POST',"/test", {
+ target:'#d1',
+ swap:'innerHTML',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ values:{i1: 'test'}
+ })
+
+ this.server.respond();
+ div.innerHTML.should.equal("Clicked!");
+ });
+
+
it('can re-init with new attributes', function () {
this.server.respondWith("PATCH", "/test", "patch");
this.server.respondWith("DELETE", "/test", "delete");
diff --git a/www/static/test/ext/ws.js b/www/static/test/ext/ws.js
index 7e6f140d..ce17782d 100644
--- a/www/static/test/ext/ws.js
+++ b/www/static/test/ext/ws.js
@@ -614,4 +614,205 @@ describe("web-sockets extension", function () {
this.messages[1].should.contains('"foo":"bar"')
this.messages[1].should.contains('"action":"B"')
})
+
+ describe("Send immediately", function() {
+ function checkCallForWsBeforeSend(spy, wrapper, message, target) {
+ // Utility function to always check the same for htmx:wsBeforeSend caught by a spy
+ spy.calledOnce.should.be.true;
+ var call = spy.getCall(0);
+ call.args.length.should.equal(1);
+ var arg = call.args[0];
+ arg.target.should.equal(target);
+ arg.detail.socketWrapper.should.equal(wrapper);
+ arg.detail.message.should.equal(message);
+ }
+ it('triggers wsBeforeSend on body if provided to sendImmediately', function (done) {
+ var myEventCalled = sinon.spy();
+ var message = '{"foo":"bar"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ window.document.body.addEventListener("htmx:wsBeforeSend", myEventCalled)
+ try {
+ socketWrapper.sendImmediately(message, window.document.body)
+ checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, window.document.body)
+ } finally {
+ window.document.body.removeEventListener("htmx:wsBeforeSend", myEventCalled)
+ }
+ done()
+ }
+ try {
+ window.document.addEventListener("htmx:wsOpen", handler)
+
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+
+ })
+ it('triggers wsBeforeSend on any send element provided to sendImmediately', function (done) {
+ var myEventCalled = sinon.spy();
+ var message = '{"a":"b"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ var id1 = byId("d1");
+ id1.addEventListener("htmx:wsBeforeSend", myEventCalled)
+ try {
+ socketWrapper.sendImmediately(message, d1)
+ checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, d1)
+ } finally {
+ id1.removeEventListener("htmx:wsBeforeSend", myEventCalled)
+ }
+ done()
+ }
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+
+ })
+ it('triggers wsAfterSend on body if provided to sendImmediately', function (done) {
+ var myEventCalled = sinon.spy();
+ var message = '{"foo":"bar"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ window.document.body.addEventListener("htmx:wsAfterSend", myEventCalled)
+ try {
+ socketWrapper.sendImmediately(message, window.document.body)
+ checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, window.document.body)
+ } finally {
+ window.document.body.removeEventListener("htmx:wsAfterSend", myEventCalled)
+ }
+ done()
+ }
+ try {
+ window.document.addEventListener("htmx:wsOpen", handler)
+
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+
+ })
+ it('triggers wsAfterSend on any send element provided to sendImmediately', function (done) {
+ var myEventCalled = sinon.spy();
+ var message = '{"a":"b"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ var id1 = byId("d1");
+ id1.addEventListener("htmx:wsAfterSend", myEventCalled)
+ try {
+ socketWrapper.sendImmediately(message, d1)
+ checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, d1)
+ } finally {
+ id1.removeEventListener("htmx:wsAfterSend", myEventCalled)
+ }
+ done()
+ }
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+
+ })
+ it('sends message if event is not prevented', function (done) {
+ var message = '{"a":"b"}';
+ var noop = function() {}
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ var id1 = byId("d1");
+ id1.addEventListener("htmx:wsBeforeSend", noop)
+ try {
+ socketWrapper.sendImmediately(message, d1)
+ this.tickMock();
+ this.messages.should.eql([message])
+ } finally {
+ id1.removeEventListener("htmx:wsBeforeSend", noop)
+ }
+ done()
+ }.bind(this)
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+ })
+ it('sends message if no sending element is provided', function (done) {
+ var message = '{"a":"b"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ socketWrapper.sendImmediately(message)
+ this.tickMock();
+ this.messages.should.eql([message])
+ done()
+ }.bind(this)
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+ })
+ it('sends message if sending element has no event listener for beforeSend', function (done) {
+ var message = '{"a":"b"}';
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ var d1 = byId("d1");
+ socketWrapper.sendImmediately(message, d1)
+ this.tickMock();
+ this.messages.should.eql([message])
+ done()
+ }.bind(this)
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+ })
+ it('does not send message if beforeSend is prevented', function (done) {
+ var message = '{"a":"b"}';
+ var eventPrevented = function(e) {e.preventDefault()}
+ var handler = function(e){
+ var socketWrapper = e.detail.socketWrapper;
+ var id1 = byId("d1");
+ id1.addEventListener("htmx:wsBeforeSend", eventPrevented)
+ try {
+ socketWrapper.sendImmediately(message, d1)
+ this.tickMock();
+ this.messages.should.eql([])
+ } finally {
+ id1.removeEventListener("htmx:wsBeforeSend", eventPrevented)
+ }
+ done()
+ }.bind(this)
+
+ window.document.addEventListener("htmx:wsOpen", handler)
+ try {
+ var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div></div>');
+ this.tickMock();
+ } finally {
+ window.document.removeEventListener("htmx:wsOpen", handler)
+ }
+
+ })
+ })
+
+
});
diff --git a/www/static/test/manual/hxboost_relative_resources/index.html b/www/static/test/manual/hxboost_relative_resources/index.html
new file mode 100644
index 00000000..9bf741eb
--- /dev/null
+++ b/www/static/test/manual/hxboost_relative_resources/index.html
@@ -0,0 +1,10 @@
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <script type="application/javascript" src="../../../src/htmx.js"></script>
+ <title>hx-boost - Relative Resources Page 1</title>
+</head>
+<body style="padding:20px;font-family: sans-serif" hx-boost="true">
+<a href="nested/page2.html">To Page 2</a>
+</body>
+</html>
diff --git a/www/static/test/manual/hxboost_relative_resources/nested/img.png b/www/static/test/manual/hxboost_relative_resources/nested/img.png
new file mode 100644
index 00000000..533a968a
--- /dev/null
+++ b/www/static/test/manual/hxboost_relative_resources/nested/img.png
Binary files differ
diff --git a/www/static/test/manual/hxboost_relative_resources/nested/page2.html b/www/static/test/manual/hxboost_relative_resources/nested/page2.html
new file mode 100644
index 00000000..12cdd25d
--- /dev/null
+++ b/www/static/test/manual/hxboost_relative_resources/nested/page2.html
@@ -0,0 +1,12 @@
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <script type="application/javascript" src="../../../../src/htmx.js"></script>
+ <title>hx-boost - Relative Resources Page 2</title>
+</head>
+<body style="padding:20px;font-family: sans-serif" hx-boost="true">
+<a href="../index.html">Back To Page 1</a>
+<p>Image should be displayed below</p>
+<img src="img.png" />
+</body>
+</html>
diff --git a/www/static/test/manual/index.html b/www/static/test/manual/index.html
index 1097e0fd..99b25d65 100644
--- a/www/static/test/manual/index.html
+++ b/www/static/test/manual/index.html
@@ -37,6 +37,12 @@
<li><a href="history_style">History Style</a></li>
</ul>
</li>
+ <li>Boost Tests
+ <ul>
+ <li><a href="hxboost_relative_resources">Relative Resources</a></li>
+ <li><a href="hxboost_template_parsing">Template Parsing</a></li>
+ </ul>
+ </li>
</ul>
<h2>Perf</h2>
<li><a href="manual-perf.html">Manual Perf Test</a></li>
diff --git a/www/static/test/ws-sse/server.mjs b/www/static/test/ws-sse/server.mjs
new file mode 100644
index 00000000..0cbe53c3
--- /dev/null
+++ b/www/static/test/ws-sse/server.mjs
@@ -0,0 +1,244 @@
+import * as http from 'node:http'
+import * as path from 'node:path'
+import * as fs from 'node:fs/promises'
+
+import { WebSocketServer } from 'ws'
+
+// Define some string and number constants
+const HOSTNAME = '127.0.0.1';
+const PORT = 8080;
+const DATA = JSON.parse(await fs.readFile('./static/data.json'))
+const SITE_BASE = (await fs.readFile('./static/site-base.html')).toString()
+
+// Define the websockets
+const ECHO_WS = createWebSocket((ws) => {
+ ws.on('message', (message) => {
+ const data = JSON.parse(message.toString())
+ ws.send(`<div id=idMessage>${data.message}</div>`)
+ })
+})
+const HEARTBEAT_WS = createWebSocket((ws) => {
+ ws.interval = setInterval(() => {
+ const num = Math.trunc(Math.random() * 10**10)
+ ws.send(`<div id=idMessage>${num}</div>`)
+ }, 1000)
+}, (ws) => clearInterval(ws.interval))
+
+
+// Define the server
+const server = http.createServer(async (req, res) => {
+ try {
+ await handleRequest(req, res)
+ } catch (error) {
+ console.error(`Error serving ${req.url}`)
+ console.error(req.body)
+ console.error(error)
+ }
+})
+
+// This handles all the non-websocket requests
+async function handleRequest (req, res) {
+ // If the URL starts with htmx, serve the src/ root version of htmx
+ if (req.url.startsWith('/htmx')) {
+ const resource = req.url.substring(6)
+ res.setHeader('Content-Type', 'text/javascript')
+ const fp = path.join('../../src', resource)
+ return serveFile(res, fp)
+ }
+
+ // If the URL matches one of these, it's an event stream
+ if (req.url.startsWith("/posts.html")) return servePosts(req, res)
+ if (req.url === "/comments.html") return makeStream(req, res, DATA.comments, formatComment)
+ if (req.url === "/albums.html") return makeStream(req, res, DATA.albums, formatAlbum)
+ if (req.url === "/todos.html") return makeStream(req, res, DATA.todos, formatTodo)
+ if (req.url === "/users.html") return makeStream(req, res, DATA.users, formatUser)
+
+ // Randomly-generated HTML
+ if (req.url === "/page/random") return serveRandomHtml(req, res)
+
+ // Otherwise, attempt to serve the file from ./static and return a 404 on failure
+ try {
+ await serveFileFromStatic(req, res)
+ } catch (error) {
+ sendNotFound(res)
+ }
+}
+
+// Attach the websockets
+server.on('upgrade', (request, socket, head) => {
+ if (request.url === '/echo') ECHO_WS.handle(request, socket, head)
+ if (request.url === '/heartbeat') HEARTBEAT_WS.handle(request, socket, head)
+})
+
+// Start listening
+server.listen(PORT, HOSTNAME, () => {
+ console.log('Loading the WebSocket / Server-Side Event Tests...');
+ console.log(`You can run them at http://${HOSTNAME}:${PORT}/`);
+})
+
+function createWebSocket (connectionFunc, closeFunc) {
+ const server = new WebSocketServer({ noServer: true })
+ server.on('connection', connectionFunc)
+ if (closeFunc) server.on('close', closeFunc)
+
+ const handle = (request, socket, head) => {
+ server.handleUpgrade(request, socket, head, (ws) => {
+ server.emit('connection', ws, request)
+ })
+ }
+ return { handle }
+}
+
+async function serveFileFromStatic (req, res) {
+ // For the root, serve the static index.html file
+ const resource = req.url === '/' ? '/index.html' : req.url
+
+ let fp = path.join('./static/', resource)
+ let lstat = await fs.lstat(fp)
+
+ // If it's a directory, re-set the fp to be the index.html of that directory
+ if (lstat.isDirectory()) {
+ fp = path.join(fp, 'index.html')
+ lstat = await fs.lstat(fp)
+ }
+
+ if (!lstat.isFile) return sendNotFound(res)
+
+ const withBase = fp.endsWith('.html')
+ return serveFile(res, fp, withBase)
+}
+
+async function serveFile (res, fp, withBase) {
+ try {
+ const file = await fs.readFile(fp)
+ let text = file.toString()
+ if (withBase) text = SITE_BASE + text
+ res.end(text)
+ } catch (error) {
+ console.error(error)
+ sendNotFound(res)
+ }
+}
+
+function servePosts (req, res) {
+ // Why do we have to specify a fake protocol here? Because WHATWG doesn't support relative URLs
+ // Maddening discussion here: https://github.com/whatwg/url/issues/531
+ const url = new URL(req.url, "thismessage:/")
+ const types = url.searchParams?.get('types')
+
+ const numEvents = types ? types.split(',').length : 0
+ makeStream(req, res, DATA.posts, formatPost, numEvents)
+}
+
+function sendNotFound(res) {
+ res.statusCode = 404
+ res.setHeader('Content-Type', 'text/plain')
+ res.end('404 NOT FOUND')
+}
+
+function makeStream(req, res, arr, formatFunc, numEvents = 0) {
+ res.writeHead(200, {
+ 'Content-Type': 'text/event-stream',
+ Connection: 'keep-alive',
+ 'Cache-Control': 'no-cache'
+ })
+
+ // Make the intervals somewhat random, between 200 and 400ms
+ // We have some tests that create multiple streams at once, so this ensures they're all visibile
+ const intervalLength = Math.floor(Math.random() * 200) + 200
+
+ let i = 0
+ const interval = setInterval(() => {
+ if (i == arr.length) i = 0
+
+ const item = arr[i]
+ try {
+ const evenNum = Math.floor(Math.random() * numEvents) + 1
+ const eventName = numEvents > 0 ? `Event${evenNum}` : '(none)'
+ item.event = eventName
+
+ const formattedData = formatFunc(item).replace(/\n/g, ' ')
+ const event = `${numEvents > 0 ? `event: ${eventName}\n` : ''}data: ${formattedData}\n\n`
+ res.write(event)
+ i++
+ } catch (error) {
+ // Stop the interval if it errors for any reason
+ clearInterval(interval)
+ }
+ }, intervalLength)
+
+ req.on('close', () => {
+ res.end('OK')
+ clearInterval(interval)
+ })
+}
+
+function serveRandomHtml(_req, res) {
+ const page_num = Math.trunc(Math.random() * 10**10)
+ const html_num = Math.trunc(Math.random() * 10**10)
+ const html = `
+ <div>
+ This is page ${page_num}
+ <br><br>
+ Randomly generated <b>HTML</b> ${html_num}
+ <br><br>
+ I wish I were a haiku.
+ </div>
+ `
+ res.end(html)
+}
+
+function formatPost (post) {
+ return `
+ <div>
+ <div class="bold">Post: ${post.title}</div>
+ <div>${post.body}</div>
+ <div>id: ${post.id}</div>
+ <div>user: ${post.userId}</div>
+ <div>event: ${post.event}</div>
+ </div>
+ `
+}
+
+function formatComment (comment) {
+ return `
+ <div>
+ <div class="bold">Comment: ${comment.name}</div>
+ <div>${comment.email}</div>
+ <div>id: ${comment.body}</div>
+ <div>event: ${comment.event}</div>
+ </div>
+ `
+}
+
+function formatAlbum (album) {
+ return `
+ <div>
+ <div class="bold">Album: ${album.title}</div>
+ <div>id: ${album.id}</div>
+ <div>event: ${album.event}</div>
+ </div>
+ `
+}
+
+function formatTodo (todo) {
+ return `
+ <div>
+ <div class="bold">To-Do: ${todo.title}</div>
+ <div>complete? ${todo.completed}</div>
+ <div>event: ${todo.event}</div>
+ </div>
+ `
+}
+
+function formatUser (user) {
+ return `
+ <div>
+ <div class="bold">User: ${user.name}</div>
+ <div>${user.email}</div>
+ <div>${user.address.street} ${user.address.suite}<br>${user.address.city}, ${user.address.zipcode}</div>
+ <div>event: ${user.event}</div>
+ </div>
+ `
+}
+
diff --git a/www/themes/htmx-theme/static/js/htmx.js b/www/themes/htmx-theme/static/js/htmx.js
index 79a4702e..1f0709d7 100644
--- a/www/themes/htmx-theme/static/js/htmx.js
+++ b/www/themes/htmx-theme/static/js/htmx.js
@@ -75,6 +75,7 @@ return (function () {
globalViewTransitions: false,
methodsThatUseUrlParams: ["get"],
selfRequestsOnly: false,
+ ignoreTitle: false,
scrollIntoViewOnBoost: true
},
parseInterval:parseInterval,
@@ -87,7 +88,7 @@ return (function () {
sock.binaryType = htmx.config.wsBinaryType;
return sock;
},
- version: "1.9.8"
+ version: "1.9.9"
};
/** @type {import("./htmx").HtmxInternalApi} */
@@ -1145,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;
@@ -1233,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';
/**
@@ -1281,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 {
@@ -2906,6 +2925,7 @@ return (function () {
values : context.values,
targetOverride: resolveTarget(context.target),
swapOverride: context.swap,
+ select: context.select,
returnPromise: true
});
}
@@ -2960,6 +2980,7 @@ return (function () {
elt = getDocument().body;
}
var responseHandler = etc.handler || handleAjaxResponse;
+ var select = etc.select || null;
if (!bodyContains(elt)) {
// do not issue requests for elements removed from the DOM
@@ -3108,6 +3129,11 @@ return (function () {
var headers = getHeaders(elt, target, promptResponse);
+
+ if (verb !== 'get' && !usesFormData(elt)) {
+ headers['Content-Type'] = 'application/x-www-form-urlencoded';
+ }
+
if (etc.headers) {
headers = mergeObjects(headers, etc.headers);
}
@@ -3121,10 +3147,6 @@ return (function () {
var allParameters = mergeObjects(rawParameters, expressionVars);
var filteredParameters = filterValues(allParameters, elt);
- if (verb !== 'get' && !usesFormData(elt)) {
- headers['Content-Type'] = 'application/x-www-form-urlencoded';
- }
-
if (htmx.config.getCacheBusterParam && verb === 'get') {
filteredParameters['org.htmx.cache-buster'] = getRawAttribute(target, "id") || "true";
}
@@ -3222,7 +3244,7 @@ return (function () {
}
var responseInfo = {
- xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted,
+ xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted, select: select,
pathInfo: {
requestPath: path,
finalRequestPath: finalPath,
@@ -3393,6 +3415,7 @@ return (function () {
var target = responseInfo.target;
var etc = responseInfo.etc;
var requestConfig = responseInfo.requestConfig;
+ var select = responseInfo.select;
if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return;
@@ -3502,10 +3525,26 @@ return (function () {
}
var selectOverride;
+ if (select) {
+ selectOverride = select;
+ }
+
if (hasHeader(xhr, /HX-Reselect:/i)) {
selectOverride = xhr.getResponseHeader("HX-Reselect");
}
+ // if we need to save history, do so, before swapping so that relative resources have the correct base URL
+ if (historyUpdate.type) {
+ triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
+ if (historyUpdate.type === "push") {
+ pushUrlIntoHistory(historyUpdate.path);
+ triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
+ } else {
+ replaceUrlInHistory(historyUpdate.path);
+ triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
+ }
+ }
+
var settleInfo = makeSettleInfo(target);
selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, selectOverride);
@@ -3555,17 +3594,6 @@ return (function () {
triggerEvent(elt, 'htmx:afterSettle', responseInfo);
});
- // if we need to save history, do so
- if (historyUpdate.type) {
- triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
- if (historyUpdate.type === "push") {
- pushUrlIntoHistory(historyUpdate.path);
- triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
- } else {
- replaceUrlInHistory(historyUpdate.path);
- triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
- }
- }
if (responseInfo.pathInfo.anchor) {
var anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor);
if(anchorTarget) {
@@ -3724,25 +3752,34 @@ return (function () {
//====================================================================
// Initialization
//====================================================================
- var isReady = false
- getDocument().addEventListener('DOMContentLoaded', function() {
- isReady = true
- })
-
/**
- * Execute a function now if DOMContentLoaded has fired, otherwise listen for it.
- *
- * This function uses isReady because there is no realiable way to ask the browswer whether
- * the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded
- * firing and readystate=complete.
+ * We want to initialize the page elements after DOMContentLoaded
+ * fires, but there isn't always a good way to tell whether
+ * it has already fired when we get here or not.
*/
- function ready(fn) {
- // Checking readyState here is a failsafe in case the htmx script tag entered the DOM by
- // some means other than the initial page load.
- if (isReady || getDocument().readyState === 'complete') {
- fn();
- } else {
- getDocument().addEventListener('DOMContentLoaded', fn);
+ function ready(functionToCall) {
+ // call the function exactly once no matter how many times this is called
+ var callReadyFunction = function() {
+ if (!functionToCall) return;
+ functionToCall();
+ functionToCall = null;
+ };
+
+ if (getDocument().readyState === "complete") {
+ // DOMContentLoaded definitely fired, we can initialize the page
+ callReadyFunction();
+ }
+ else {
+ /* DOMContentLoaded *maybe* already fired, wait for
+ * the next DOMContentLoaded or readystatechange event
+ */
+ getDocument().addEventListener("DOMContentLoaded", function() {
+ callReadyFunction();
+ });
+ getDocument().addEventListener("readystatechange", function() {
+ if (getDocument().readyState !== "complete") return;
+ callReadyFunction();
+ });
}
}
@@ -3750,9 +3787,9 @@ return (function () {
if (htmx.config.includeIndicatorStyles !== false) {
getDocument().head.insertAdjacentHTML("beforeend",
"<style>\
- ." + htmx.config.indicatorClass + "{opacity:0;transition: opacity 200ms ease-in;}\
- ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1}\
- ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1}\
+ ." + htmx.config.indicatorClass + "{opacity:0}\
+ ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\
+ ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\
</style>");
}
}