mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-09-27 04:50:43 +00:00
prep 1.9.7 release
This commit is contained in:
parent
5cd5f927ba
commit
3912e3c2c2
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [1.9.7] - 2023-11-03
|
||||
|
||||
* Fixed a bug where a button associated with a form that is swapped out of the DOM caused errors
|
||||
* The `hx-target-error` attribute was added to the `response-targets` extension, allowing you to capture all 400 & 500
|
||||
responses with a single attribute
|
||||
* `hx-on` now properly supports multiple listeners
|
||||
* The `hx-confirm` prompt is now passed into custom confirmation handlers
|
||||
* `next` and `previous` are now valid _extended CSS_ symbols in htmx
|
||||
* The `htmx:beforeHistoryUpdate` event was added
|
||||
* Properly ignore the `dialog` formmethod on buttons when resolving the HTTP method to use
|
||||
|
||||
## [1.9.6] - 2023-09-22
|
||||
|
||||
* IE support has been restored (thank you @telroshan!)
|
||||
|
@ -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.6"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.7"></script>
|
||||
<!-- have a button POST a click via AJAX -->
|
||||
<button hx-post="/clicked" hx-swap="outerHTML">
|
||||
Click Me
|
||||
|
22
dist/ext/client-side-templates.js
vendored
22
dist/ext/client-side-templates.js
vendored
@ -28,15 +28,27 @@ htmx.defineExtension('client-side-templates', {
|
||||
var handlebarsTemplate = htmx.closest(elt, "[handlebars-template]");
|
||||
if (handlebarsTemplate) {
|
||||
var data = JSON.parse(text);
|
||||
var templateName = handlebarsTemplate.getAttribute('handlebars-template');
|
||||
return Handlebars.partials[templateName](data);
|
||||
var templateId = handlebarsTemplate.getAttribute('handlebars-template');
|
||||
var templateElement = htmx.find('#' + templateId).innerHTML;
|
||||
var renderTemplate = Handlebars.compile(templateElement);
|
||||
if (renderTemplate) {
|
||||
return renderTemplate(data);
|
||||
} else {
|
||||
throw "Unknown handlebars template: " + templateId;
|
||||
}
|
||||
}
|
||||
|
||||
var handlebarsArrayTemplate = htmx.closest(elt, "[handlebars-array-template]");
|
||||
if (handlebarsArrayTemplate) {
|
||||
var data = JSON.parse(text);
|
||||
var templateName = handlebarsArrayTemplate.getAttribute('handlebars-array-template');
|
||||
return Handlebars.partials[templateName]({"data": data});
|
||||
var templateId = handlebarsArrayTemplate.getAttribute('handlebars-array-template');
|
||||
var templateElement = htmx.find('#' + templateId).innerHTML;
|
||||
var renderTemplate = Handlebars.compile(templateElement);
|
||||
if (renderTemplate) {
|
||||
return renderTemplate(data);
|
||||
} else {
|
||||
throw "Unknown handlebars template: " + templateId;
|
||||
}
|
||||
}
|
||||
|
||||
var nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
|
||||
@ -50,7 +62,7 @@ htmx.defineExtension('client-side-templates', {
|
||||
return nunjucks.render(templateName, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var xsltTemplate = htmx.closest(elt, "[xslt-template]");
|
||||
if (xsltTemplate) {
|
||||
var templateId = xsltTemplate.getAttribute('xslt-template');
|
||||
|
3
dist/ext/response-targets.js
vendored
3
dist/ext/response-targets.js
vendored
@ -38,6 +38,9 @@
|
||||
'***',
|
||||
'xxx',
|
||||
];
|
||||
if (respCode.startsWith('4') || respCode.startsWith('5')) {
|
||||
attrPossibilities.push('error');
|
||||
}
|
||||
|
||||
for (var i = 0; i < attrPossibilities.length; i++) {
|
||||
var attr = attrPrefix + attrPossibilities[i];
|
||||
|
2
dist/ext/ws.js
vendored
2
dist/ext/ws.js
vendored
@ -379,7 +379,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
|
||||
socketWrapper.send(body, elt);
|
||||
|
||||
if (api.shouldCancel(evt, elt)) {
|
||||
if (evt && api.shouldCancel(evt, elt)) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
});
|
||||
|
10
dist/htmx.d.ts
vendored
10
dist/htmx.d.ts
vendored
@ -385,16 +385,6 @@ export interface HtmxConfig {
|
||||
disableSelector?: "[hx-disable], [data-hx-disable]" | string;
|
||||
/** @default "smooth" */
|
||||
scrollBehavior?: "smooth" | "auto";
|
||||
/**
|
||||
* If set to false, disables the interpretation of script tags.
|
||||
* @default true
|
||||
*/
|
||||
allowScriptTags?: boolean;
|
||||
/**
|
||||
* If set to true, disables htmx-based requests to non-origin hosts.
|
||||
* @default false
|
||||
*/
|
||||
selfRequestsOnly?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
102
dist/htmx.js
vendored
102
dist/htmx.js
vendored
@ -86,7 +86,7 @@ return (function () {
|
||||
sock.binaryType = htmx.config.wsBinaryType;
|
||||
return sock;
|
||||
},
|
||||
version: "1.9.6"
|
||||
version: "1.9.7"
|
||||
};
|
||||
|
||||
/** @type {import("./htmx").HtmxInternalApi} */
|
||||
@ -596,8 +596,12 @@ return (function () {
|
||||
return [closest(elt, normalizeSelector(selector.substr(8)))];
|
||||
} else if (selector.indexOf("find ") === 0) {
|
||||
return [find(elt, normalizeSelector(selector.substr(5)))];
|
||||
} else if (selector === "next") {
|
||||
return [elt.nextElementSibling]
|
||||
} else if (selector.indexOf("next ") === 0) {
|
||||
return [scanForwardQuery(elt, normalizeSelector(selector.substr(5)))];
|
||||
} else if (selector === "previous") {
|
||||
return [elt.previousElementSibling]
|
||||
} else if (selector.indexOf("previous ") === 0) {
|
||||
return [scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)))];
|
||||
} else if (selector === 'document') {
|
||||
@ -1279,12 +1283,14 @@ return (function () {
|
||||
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
|
||||
tokens.shift();
|
||||
from_arg +=
|
||||
" " +
|
||||
consumeUntil(
|
||||
tokens,
|
||||
WHITESPACE_OR_COMMA
|
||||
);
|
||||
var selector = consumeUntil(
|
||||
tokens,
|
||||
WHITESPACE_OR_COMMA
|
||||
)
|
||||
// `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] === ":") {
|
||||
@ -1906,32 +1912,39 @@ return (function () {
|
||||
}
|
||||
}
|
||||
|
||||
function initButtonTracking(elt) {
|
||||
// Handle submit buttons/inputs that have the form attribute set
|
||||
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
|
||||
var form = resolveTarget("#" + getRawAttribute(elt, "form")) || closest(elt, "form")
|
||||
if (!form) {
|
||||
return
|
||||
// Handle submit buttons/inputs that have the form attribute set
|
||||
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
|
||||
function maybeSetLastButtonClicked(evt) {
|
||||
var elt = closest(evt.target, "button, input[type='submit']");
|
||||
var internalData = getRelatedFormData(evt)
|
||||
if (internalData) {
|
||||
internalData.lastButtonClicked = elt;
|
||||
}
|
||||
|
||||
var maybeSetLastButtonClicked = function (evt) {
|
||||
var elt = closest(evt.target, "button, input[type='submit']");
|
||||
if (elt !== null) {
|
||||
var internalData = getInternalData(form);
|
||||
internalData.lastButtonClicked = elt;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
function maybeUnsetLastButtonClicked(evt){
|
||||
var internalData = getRelatedFormData(evt)
|
||||
if (internalData) {
|
||||
internalData.lastButtonClicked = null;
|
||||
}
|
||||
}
|
||||
function getRelatedFormData(evt) {
|
||||
var elt = closest(evt.target, "button, input[type='submit']");
|
||||
if (!elt) {
|
||||
return;
|
||||
}
|
||||
var form = resolveTarget('#' + getRawAttribute(elt, 'form')) || closest(elt, 'form');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
return getInternalData(form);
|
||||
}
|
||||
function initButtonTracking(elt) {
|
||||
// need to handle both click and focus in:
|
||||
// focusin - in case someone tabs in to a button and hits the space bar
|
||||
// click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724
|
||||
|
||||
elt.addEventListener('click', maybeSetLastButtonClicked)
|
||||
elt.addEventListener('focusin', maybeSetLastButtonClicked)
|
||||
elt.addEventListener('focusout', function(evt){
|
||||
var internalData = getInternalData(form);
|
||||
internalData.lastButtonClicked = null;
|
||||
})
|
||||
elt.addEventListener('focusout', maybeUnsetLastButtonClicked)
|
||||
}
|
||||
|
||||
function countCurlies(line) {
|
||||
@ -1950,7 +1963,9 @@ return (function () {
|
||||
|
||||
function addHxOnEventHandler(elt, eventName, code) {
|
||||
var nodeData = getInternalData(elt);
|
||||
nodeData.onHandlers = [];
|
||||
if (!Array.isArray(nodeData.onHandlers)) {
|
||||
nodeData.onHandlers = [];
|
||||
}
|
||||
var func;
|
||||
var listener = function (e) {
|
||||
return maybeEval(elt, function() {
|
||||
@ -2169,6 +2184,12 @@ return (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (htmx.config.historyCacheSize <= 0) {
|
||||
// make sure that an eventually already existing cache is purged
|
||||
localStorage.removeItem("htmx-history-cache");
|
||||
return;
|
||||
}
|
||||
|
||||
url = normalizePath(url);
|
||||
|
||||
var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
|
||||
@ -2434,7 +2455,7 @@ return (function () {
|
||||
if (shouldInclude(elt)) {
|
||||
var name = getRawAttribute(elt,"name");
|
||||
var value = elt.value;
|
||||
if (elt.multiple) {
|
||||
if (elt.multiple && elt.tagName === "SELECT") {
|
||||
value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
|
||||
}
|
||||
// include file inputs
|
||||
@ -2474,6 +2495,9 @@ return (function () {
|
||||
var formValues = {};
|
||||
var errors = [];
|
||||
var internalData = getInternalData(elt);
|
||||
if (internalData.lastButtonClicked && !bodyContains(internalData.lastButtonClicked)) {
|
||||
internalData.lastButtonClicked = null
|
||||
}
|
||||
|
||||
// only validate when form is directly submitted and novalidate or formnovalidate are not set
|
||||
// or if the element has an explicit hx-validate="true" on it
|
||||
@ -2959,16 +2983,20 @@ return (function () {
|
||||
|
||||
var buttonVerb = getRawAttribute(submitter, "formmethod")
|
||||
if (buttonVerb != null) {
|
||||
verb = buttonVerb;
|
||||
// ignore buttons with formmethod="dialog"
|
||||
if (buttonVerb.toLowerCase() !== "dialog") {
|
||||
verb = buttonVerb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
|
||||
// allow event-based confirmation w/ a callback
|
||||
if (!confirmed) {
|
||||
var issueRequest = function() {
|
||||
return issueAjaxRequest(verb, path, elt, event, etc, true);
|
||||
if (confirmed === undefined) {
|
||||
var issueRequest = function(skipConfirmation) {
|
||||
return issueAjaxRequest(verb, path, elt, event, etc, !!skipConfirmation);
|
||||
}
|
||||
var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest};
|
||||
var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest, question: confirmQuestion};
|
||||
if (triggerEvent(elt, 'htmx:confirm', confirmDetails) === false) {
|
||||
maybeCall(resolve);
|
||||
return promise;
|
||||
@ -3069,8 +3097,7 @@ return (function () {
|
||||
}
|
||||
}
|
||||
|
||||
var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
|
||||
if (confirmQuestion) {
|
||||
if (confirmQuestion && !confirmed) {
|
||||
if(!confirm(confirmQuestion)) {
|
||||
maybeCall(resolve);
|
||||
endRequestLock()
|
||||
@ -3529,6 +3556,7 @@ return (function () {
|
||||
|
||||
// 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});
|
||||
@ -3538,7 +3566,7 @@ return (function () {
|
||||
}
|
||||
}
|
||||
if (responseInfo.pathInfo.anchor) {
|
||||
var anchorTarget = find("#" + responseInfo.pathInfo.anchor);
|
||||
var anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor);
|
||||
if(anchorTarget) {
|
||||
anchorTarget.scrollIntoView({block:'start', behavior: "auto"});
|
||||
}
|
||||
|
2
dist/htmx.min.js
vendored
2
dist/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
BIN
dist/htmx.min.js.gz
vendored
BIN
dist/htmx.min.js.gz
vendored
Binary file not shown.
@ -5,7 +5,7 @@
|
||||
"AJAX",
|
||||
"HTML"
|
||||
],
|
||||
"version": "1.9.6",
|
||||
"version": "1.9.7",
|
||||
"homepage": "https://htmx.org/",
|
||||
"bugs": {
|
||||
"url": "https://github.com/bigskysoftware/htmx/issues"
|
||||
|
@ -86,7 +86,7 @@ return (function () {
|
||||
sock.binaryType = htmx.config.wsBinaryType;
|
||||
return sock;
|
||||
},
|
||||
version: "1.9.6"
|
||||
version: "1.9.7"
|
||||
};
|
||||
|
||||
/** @type {import("./htmx").HtmxInternalApi} */
|
||||
|
@ -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.6"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.7"></script>
|
||||
<!-- have a button POST a click via AJAX -->
|
||||
<button hx-post="/clicked" hx-swap="outerHTML">
|
||||
Click Me
|
||||
|
@ -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.6" integrity="sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.7" integrity="sha384-DkR/zjYGLV0eOpkzbCfUJvsiyyYVP7iL/r5p5Fn+GU0VcrTEFbcjrXHgb2HPI718" 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).
|
||||
|
23
www/content/posts/2023-11-03-htmx-1.9.7-is-released.md
Normal file
23
www/content/posts/2023-11-03-htmx-1.9.7-is-released.md
Normal file
@ -0,0 +1,23 @@
|
||||
+++
|
||||
title = "htmx 1.9.7 has been released!"
|
||||
date = 2023-11-03
|
||||
[taxonomies]
|
||||
tag = ["posts", "announcements"]
|
||||
+++
|
||||
|
||||
## htmx 1.9.7 Release
|
||||
|
||||
I'm happy to announce the [1.9.7 release](https://unpkg.com/browse/htmx.org@1.9.7/) of htmx.
|
||||
|
||||
### Improvements & Bug fixes
|
||||
|
||||
* Fixed a bug where a button associated with a form that is swapped out of the DOM caused errors
|
||||
* The `hx-target-error` attribute was added to the `response-targets` extension, allowing you to capture all 400 & 500
|
||||
responses with a single attribute
|
||||
* `hx-on` now properly supports multiple listeners
|
||||
* The `hx-confirm` prompt is now passed into custom confirmation handlers
|
||||
* `next` and `previous` are now valid _extended CSS_ symbols in htmx
|
||||
* The `htmx:beforeHistoryUpdate` event was added
|
||||
* Properly ignore the `dialog` formmethod on buttons when resolving the HTTP method to use
|
||||
|
||||
Thank you to everyone who contributed, and enjoy!
|
@ -38,6 +38,9 @@
|
||||
'***',
|
||||
'xxx',
|
||||
];
|
||||
if (respCode.startsWith('4') || respCode.startsWith('5')) {
|
||||
attrPossibilities.push('error');
|
||||
}
|
||||
|
||||
for (var i = 0; i < attrPossibilities.length; i++) {
|
||||
var attr = attrPrefix + attrPossibilities[i];
|
||||
|
@ -379,7 +379,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
|
||||
socketWrapper.send(body, elt);
|
||||
|
||||
if (api.shouldCancel(evt, elt)) {
|
||||
if (evt && api.shouldCancel(evt, elt)) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
});
|
||||
|
@ -86,7 +86,7 @@ return (function () {
|
||||
sock.binaryType = htmx.config.wsBinaryType;
|
||||
return sock;
|
||||
},
|
||||
version: "1.9.6"
|
||||
version: "1.9.7"
|
||||
};
|
||||
|
||||
/** @type {import("./htmx").HtmxInternalApi} */
|
||||
@ -596,8 +596,12 @@ return (function () {
|
||||
return [closest(elt, normalizeSelector(selector.substr(8)))];
|
||||
} else if (selector.indexOf("find ") === 0) {
|
||||
return [find(elt, normalizeSelector(selector.substr(5)))];
|
||||
} else if (selector === "next") {
|
||||
return [elt.nextElementSibling]
|
||||
} else if (selector.indexOf("next ") === 0) {
|
||||
return [scanForwardQuery(elt, normalizeSelector(selector.substr(5)))];
|
||||
} else if (selector === "previous") {
|
||||
return [elt.previousElementSibling]
|
||||
} else if (selector.indexOf("previous ") === 0) {
|
||||
return [scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)))];
|
||||
} else if (selector === 'document') {
|
||||
@ -1279,12 +1283,14 @@ return (function () {
|
||||
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
|
||||
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
|
||||
tokens.shift();
|
||||
from_arg +=
|
||||
" " +
|
||||
consumeUntil(
|
||||
tokens,
|
||||
WHITESPACE_OR_COMMA
|
||||
);
|
||||
var selector = consumeUntil(
|
||||
tokens,
|
||||
WHITESPACE_OR_COMMA
|
||||
)
|
||||
// `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] === ":") {
|
||||
@ -1906,32 +1912,39 @@ return (function () {
|
||||
}
|
||||
}
|
||||
|
||||
function initButtonTracking(elt) {
|
||||
// Handle submit buttons/inputs that have the form attribute set
|
||||
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
|
||||
var form = resolveTarget("#" + getRawAttribute(elt, "form")) || closest(elt, "form")
|
||||
if (!form) {
|
||||
return
|
||||
// Handle submit buttons/inputs that have the form attribute set
|
||||
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
|
||||
function maybeSetLastButtonClicked(evt) {
|
||||
var elt = closest(evt.target, "button, input[type='submit']");
|
||||
var internalData = getRelatedFormData(evt)
|
||||
if (internalData) {
|
||||
internalData.lastButtonClicked = elt;
|
||||
}
|
||||
|
||||
var maybeSetLastButtonClicked = function (evt) {
|
||||
var elt = closest(evt.target, "button, input[type='submit']");
|
||||
if (elt !== null) {
|
||||
var internalData = getInternalData(form);
|
||||
internalData.lastButtonClicked = elt;
|
||||
}
|
||||
};
|
||||
|
||||
};
|
||||
function maybeUnsetLastButtonClicked(evt){
|
||||
var internalData = getRelatedFormData(evt)
|
||||
if (internalData) {
|
||||
internalData.lastButtonClicked = null;
|
||||
}
|
||||
}
|
||||
function getRelatedFormData(evt) {
|
||||
var elt = closest(evt.target, "button, input[type='submit']");
|
||||
if (!elt) {
|
||||
return;
|
||||
}
|
||||
var form = resolveTarget('#' + getRawAttribute(elt, 'form')) || closest(elt, 'form');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
return getInternalData(form);
|
||||
}
|
||||
function initButtonTracking(elt) {
|
||||
// need to handle both click and focus in:
|
||||
// focusin - in case someone tabs in to a button and hits the space bar
|
||||
// click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724
|
||||
|
||||
elt.addEventListener('click', maybeSetLastButtonClicked)
|
||||
elt.addEventListener('focusin', maybeSetLastButtonClicked)
|
||||
elt.addEventListener('focusout', function(evt){
|
||||
var internalData = getInternalData(form);
|
||||
internalData.lastButtonClicked = null;
|
||||
})
|
||||
elt.addEventListener('focusout', maybeUnsetLastButtonClicked)
|
||||
}
|
||||
|
||||
function countCurlies(line) {
|
||||
@ -1950,7 +1963,9 @@ return (function () {
|
||||
|
||||
function addHxOnEventHandler(elt, eventName, code) {
|
||||
var nodeData = getInternalData(elt);
|
||||
nodeData.onHandlers = [];
|
||||
if (!Array.isArray(nodeData.onHandlers)) {
|
||||
nodeData.onHandlers = [];
|
||||
}
|
||||
var func;
|
||||
var listener = function (e) {
|
||||
return maybeEval(elt, function() {
|
||||
@ -2169,6 +2184,12 @@ return (function () {
|
||||
return;
|
||||
}
|
||||
|
||||
if (htmx.config.historyCacheSize <= 0) {
|
||||
// make sure that an eventually already existing cache is purged
|
||||
localStorage.removeItem("htmx-history-cache");
|
||||
return;
|
||||
}
|
||||
|
||||
url = normalizePath(url);
|
||||
|
||||
var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
|
||||
@ -2434,7 +2455,7 @@ return (function () {
|
||||
if (shouldInclude(elt)) {
|
||||
var name = getRawAttribute(elt,"name");
|
||||
var value = elt.value;
|
||||
if (elt.multiple) {
|
||||
if (elt.multiple && elt.tagName === "SELECT") {
|
||||
value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
|
||||
}
|
||||
// include file inputs
|
||||
@ -2474,6 +2495,9 @@ return (function () {
|
||||
var formValues = {};
|
||||
var errors = [];
|
||||
var internalData = getInternalData(elt);
|
||||
if (internalData.lastButtonClicked && !bodyContains(internalData.lastButtonClicked)) {
|
||||
internalData.lastButtonClicked = null
|
||||
}
|
||||
|
||||
// only validate when form is directly submitted and novalidate or formnovalidate are not set
|
||||
// or if the element has an explicit hx-validate="true" on it
|
||||
@ -2959,16 +2983,20 @@ return (function () {
|
||||
|
||||
var buttonVerb = getRawAttribute(submitter, "formmethod")
|
||||
if (buttonVerb != null) {
|
||||
verb = buttonVerb;
|
||||
// ignore buttons with formmethod="dialog"
|
||||
if (buttonVerb.toLowerCase() !== "dialog") {
|
||||
verb = buttonVerb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
|
||||
// allow event-based confirmation w/ a callback
|
||||
if (!confirmed) {
|
||||
var issueRequest = function() {
|
||||
return issueAjaxRequest(verb, path, elt, event, etc, true);
|
||||
if (confirmed === undefined) {
|
||||
var issueRequest = function(skipConfirmation) {
|
||||
return issueAjaxRequest(verb, path, elt, event, etc, !!skipConfirmation);
|
||||
}
|
||||
var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest};
|
||||
var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest, question: confirmQuestion};
|
||||
if (triggerEvent(elt, 'htmx:confirm', confirmDetails) === false) {
|
||||
maybeCall(resolve);
|
||||
return promise;
|
||||
@ -3069,8 +3097,7 @@ return (function () {
|
||||
}
|
||||
}
|
||||
|
||||
var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
|
||||
if (confirmQuestion) {
|
||||
if (confirmQuestion && !confirmed) {
|
||||
if(!confirm(confirmQuestion)) {
|
||||
maybeCall(resolve);
|
||||
endRequestLock()
|
||||
@ -3529,6 +3556,7 @@ return (function () {
|
||||
|
||||
// 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});
|
||||
@ -3538,7 +3566,7 @@ return (function () {
|
||||
}
|
||||
}
|
||||
if (responseInfo.pathInfo.anchor) {
|
||||
var anchorTarget = find("#" + responseInfo.pathInfo.anchor);
|
||||
var anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor);
|
||||
if(anchorTarget) {
|
||||
anchorTarget.scrollIntoView({block:'start', behavior: "auto"});
|
||||
}
|
||||
|
126
www/static/test/attributes/hx-confirm.js
Normal file
126
www/static/test/attributes/hx-confirm.js
Normal file
@ -0,0 +1,126 @@
|
||||
describe("hx-confirm attribute", function () {
|
||||
var confirm
|
||||
beforeEach(function () {
|
||||
this.server = makeServer();
|
||||
confirm = sinon.stub(window, "confirm");
|
||||
clearWorkArea();
|
||||
});
|
||||
afterEach(function () {
|
||||
this.server.restore();
|
||||
confirm.restore()
|
||||
clearWorkArea();
|
||||
});
|
||||
|
||||
it('prompts using window.confirm when hx-confirm is set', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
confirm.returns(true);
|
||||
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
|
||||
btn.click();
|
||||
confirm.calledOnce.should.equal(true);
|
||||
this.server.respond();
|
||||
btn.innerHTML.should.equal("Clicked!");
|
||||
})
|
||||
|
||||
it('stops the request if confirm is cancelled', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
confirm.returns(false);
|
||||
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
|
||||
btn.click();
|
||||
confirm.calledOnce.should.equal(true);
|
||||
this.server.respond();
|
||||
btn.innerHTML.should.equal("Click Me!");
|
||||
})
|
||||
|
||||
it('uses the value of hx-confirm as the prompt', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
confirm.returns(false);
|
||||
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
|
||||
btn.click();
|
||||
confirm.firstCall.args[0].should.equal("Sure?");
|
||||
this.server.respond();
|
||||
btn.innerHTML.should.equal("Click Me!");
|
||||
})
|
||||
|
||||
it('should prompt when htmx:confirm handler calls issueRequest', function () {
|
||||
try {
|
||||
var btn = make('<button hx-get="/test" hx-confirm="Surely?">Click Me!</button>')
|
||||
var handler = htmx.on("htmx:confirm", function (evt) {
|
||||
evt.preventDefault();
|
||||
evt.detail.issueRequest();
|
||||
});
|
||||
btn.click();
|
||||
confirm.calledOnce.should.equal(true);
|
||||
} finally {
|
||||
htmx.off("htmx:confirm", handler);
|
||||
}
|
||||
})
|
||||
|
||||
it('should include the question in htmx:confirm event', function () {
|
||||
var stub = sinon.stub();
|
||||
try {
|
||||
var btn = make('<button hx-get="/test" hx-confirm="Surely?">Click Me!</button>')
|
||||
var handler = htmx.on("htmx:confirm", stub);
|
||||
btn.click();
|
||||
stub.calledOnce.should.equal(true);
|
||||
stub.firstCall.args[0].detail.should.have.property("question", "Surely?");
|
||||
} finally {
|
||||
htmx.off("htmx:confirm", handler);
|
||||
}
|
||||
})
|
||||
|
||||
it('should allow skipping built-in window.confirm when using issueRequest', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
try {
|
||||
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
|
||||
var handler = htmx.on("htmx:confirm", function (evt) {
|
||||
evt.detail.question.should.equal("Sure?");
|
||||
evt.preventDefault();
|
||||
evt.detail.issueRequest(true);
|
||||
});
|
||||
btn.click();
|
||||
confirm.called.should.equal(false);
|
||||
this.server.respond();
|
||||
btn.innerHTML.should.equal("Clicked!");
|
||||
} finally {
|
||||
htmx.off("htmx:confirm", handler);
|
||||
}
|
||||
})
|
||||
it('should allow skipping built-in window.confirm when using issueRequest', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
try {
|
||||
var btn = make('<button hx-get="/test" hx-confirm="Sure?">Click Me!</button>')
|
||||
var handler = htmx.on("htmx:confirm", function (evt) {
|
||||
evt.detail.question.should.equal("Sure?");
|
||||
evt.preventDefault();
|
||||
evt.detail.issueRequest(true);
|
||||
});
|
||||
btn.click();
|
||||
confirm.called.should.equal(false);
|
||||
this.server.respond();
|
||||
btn.innerHTML.should.equal("Clicked!");
|
||||
} finally {
|
||||
htmx.off("htmx:confirm", handler);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
it('should allow htmx:confirm even when no hx-confirm is set', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
try {
|
||||
var btn = make('<button hx-get="/test">Click Me!</button>')
|
||||
var handler = htmx.on("htmx:confirm", function (evt) {
|
||||
evt.detail.should.have.property("question", null);
|
||||
evt.preventDefault();
|
||||
evt.detail.issueRequest();
|
||||
});
|
||||
btn.click();
|
||||
confirm.called.should.equal(false); // no hx-confirm means no window.confirm
|
||||
this.server.respond();
|
||||
btn.innerHTML.should.equal("Clicked!");
|
||||
} finally {
|
||||
htmx.off("htmx:confirm", handler);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
});
|
@ -175,4 +175,22 @@ describe("hx-on:* attribute", function() {
|
||||
delete window.foo;
|
||||
delete window.bar;
|
||||
});
|
||||
|
||||
it("cleans up all handlers when the DOM updates", function () {
|
||||
// setup
|
||||
window.foo = 0;
|
||||
window.bar = 0;
|
||||
var div = make("<div hx-on:increment-foo='window.foo++' hx-on:increment-bar='window.bar++'>Foo</div>");
|
||||
make("<div>Another Div</div>"); // sole purpose is to update the DOM
|
||||
|
||||
// check there is just one handler against each event
|
||||
htmx.trigger(div, "increment-foo");
|
||||
htmx.trigger(div, "increment-bar");
|
||||
window.foo.should.equal(1);
|
||||
window.bar.should.equal(1);
|
||||
|
||||
// teardown
|
||||
delete window.foo;
|
||||
delete window.bar;
|
||||
});
|
||||
});
|
||||
|
@ -171,4 +171,22 @@ describe("hx-on attribute", function() {
|
||||
delete window.foo;
|
||||
delete window.bar;
|
||||
});
|
||||
|
||||
it("cleans up all handlers when the DOM updates", function () {
|
||||
// setup
|
||||
window.foo = 0;
|
||||
window.bar = 0;
|
||||
var div = make("<div hx-on='increment-foo: window.foo++\nincrement-bar: window.bar++'>Foo</div>");
|
||||
make("<div>Another Div</div>"); // sole purpose is to update the DOM
|
||||
|
||||
// check there is just one handler against each event
|
||||
htmx.trigger(div, "increment-foo");
|
||||
htmx.trigger(div, "increment-bar");
|
||||
window.foo.should.equal(1);
|
||||
window.bar.should.equal(1);
|
||||
|
||||
// teardown
|
||||
delete window.foo;
|
||||
delete window.bar;
|
||||
});
|
||||
});
|
||||
|
@ -202,4 +202,43 @@ describe("hx-target attribute", function(){
|
||||
div3.innerHTML.should.equal("Clicked!");
|
||||
});
|
||||
|
||||
it('targets a `next` element properly without selector', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
make('<div>' +
|
||||
' <div id="d3"></div>' +
|
||||
' <button id="b1" hx-target="next" hx-get="/test">Click Me!</button>' +
|
||||
' <div id="d1"></div>' +
|
||||
' <div id="d2"></div>' +
|
||||
'</div>')
|
||||
var btn = byId("b1")
|
||||
var div1 = byId("d1")
|
||||
var div2 = byId("d2")
|
||||
var div3 = byId("d3")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("Clicked!");
|
||||
div2.innerHTML.should.equal("");
|
||||
div3.innerHTML.should.equal("");
|
||||
});
|
||||
|
||||
it('targets a `previous` element properly without selector', function()
|
||||
{
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
make('<div>' +
|
||||
' <div id="d3"></div>' +
|
||||
' <button id="b1" hx-target="previous" hx-get="/test">Click Me!</button>' +
|
||||
' <div id="d1"></div>' +
|
||||
' <div id="d2"></div>' +
|
||||
'</div>')
|
||||
var btn = byId("b1")
|
||||
var div1 = byId("d1")
|
||||
var div2 = byId("d2")
|
||||
var div3 = byId("d3")
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
div1.innerHTML.should.equal("");
|
||||
div2.innerHTML.should.equal("");
|
||||
div3.innerHTML.should.equal("Clicked!");
|
||||
});
|
||||
})
|
||||
|
@ -506,6 +506,36 @@ describe("hx-trigger attribute", function(){
|
||||
a1.innerHTML.should.equal("Requests: 1");
|
||||
});
|
||||
|
||||
it('from clause works with next', function()
|
||||
{
|
||||
var requests = 0;
|
||||
this.server.respondWith("GET", "/test", function (xhr) {
|
||||
requests++;
|
||||
xhr.respond(200, {}, "Requests: " + requests);
|
||||
});
|
||||
make('<div hx-trigger="click from:next" hx-target="#a1" hx-get="/test"></div><a id="a1">Requests: 0</a>');
|
||||
var a1 = byId('a1');
|
||||
a1.innerHTML.should.equal("Requests: 0");
|
||||
a1.click();
|
||||
this.server.respond();
|
||||
a1.innerHTML.should.equal("Requests: 1");
|
||||
});
|
||||
|
||||
it('from clause works with previous', function()
|
||||
{
|
||||
var requests = 0;
|
||||
this.server.respondWith("GET", "/test", function (xhr) {
|
||||
requests++;
|
||||
xhr.respond(200, {}, "Requests: " + requests);
|
||||
});
|
||||
make('<a id="a1">Requests: 0</a><div hx-trigger="click from:previous" hx-target="#a1" hx-get="/test"></div>');
|
||||
var a1 = byId('a1');
|
||||
a1.innerHTML.should.equal("Requests: 0");
|
||||
a1.click();
|
||||
this.server.respond();
|
||||
a1.innerHTML.should.equal("Requests: 1");
|
||||
});
|
||||
|
||||
it('event listeners on other elements are removed when an element is swapped out', function()
|
||||
{
|
||||
var requests = 0;
|
||||
|
@ -481,6 +481,33 @@ describe("Core htmx AJAX Tests", function(){
|
||||
values.should.deep.equal({multiSelect:["m1", "m3", "m7", "m8"]});
|
||||
});
|
||||
|
||||
it('properly handles multiple email input', function()
|
||||
{
|
||||
var values;
|
||||
this.server.respondWith("Post", "/test", function (xhr) {
|
||||
values = getParameters(xhr);
|
||||
xhr.respond(204, {}, "");
|
||||
});
|
||||
|
||||
var form = make('<form hx-post="/test" hx-trigger="click">' +
|
||||
'<input id="multiEmail" name="multiEmail" multiple>'+
|
||||
'</form>');
|
||||
|
||||
form.click();
|
||||
this.server.respond();
|
||||
values.should.deep.equal({multiEmail: ''});
|
||||
|
||||
byId("multiEmail").value = 'foo@example.com';
|
||||
form.click();
|
||||
this.server.respond();
|
||||
values.should.deep.equal({multiEmail:"foo@example.com"});
|
||||
|
||||
byId("multiEmail").value = 'foo@example.com,bar@example.com';
|
||||
form.click();
|
||||
this.server.respond();
|
||||
values.should.deep.equal({multiEmail:"foo@example.com,bar@example.com"});
|
||||
});
|
||||
|
||||
it('properly handles checkbox inputs', function()
|
||||
{
|
||||
var values;
|
||||
@ -1230,4 +1257,59 @@ describe("Core htmx AJAX Tests", function(){
|
||||
this.server.respond();
|
||||
values.should.deep.equal({t1: 'textValue', b1: ['inputValue', 'buttonValue'], s1: "selectValue"});
|
||||
})
|
||||
|
||||
it('handles form post with button formmethod dialog properly', function () {
|
||||
var values;
|
||||
this.server.respondWith("POST", "/test", function (xhr) {
|
||||
values = getParameters(xhr);
|
||||
xhr.respond(200, {}, "");
|
||||
});
|
||||
|
||||
make('<dialog><form hx-post="/test"><button id="submit" formmethod="dialog" name="foo" value="bar">submit</button></form></dialog>');
|
||||
|
||||
byId("submit").click();
|
||||
this.server.respond();
|
||||
values.should.deep.equal({ foo: 'bar' });
|
||||
})
|
||||
|
||||
it('handles form get with button formmethod dialog properly', function () {
|
||||
var responded = false;
|
||||
this.server.respondWith("GET", "/test", function (xhr) {
|
||||
responded = true;
|
||||
xhr.respond(200, {}, "");
|
||||
});
|
||||
|
||||
make('<dialog><form hx-get="/test"><button id="submit" formmethod="dialog">submit</button></form></dialog>');
|
||||
|
||||
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" ' +
|
||||
'hx-target="#hello" ' +
|
||||
'hx-select="#hello" ' +
|
||||
'hx-swap="outerHTML" ' +
|
||||
'hx-post="/test">\n' +
|
||||
'<input id="input" type="text" name="name" />\n' +
|
||||
'<button name="value" type="submit">Submit</button>\n' +
|
||||
'</form>\n' +
|
||||
'<button id="outside" name="outside" form="hello" type="submit">Outside</button>';
|
||||
|
||||
var values
|
||||
this.server.respondWith("/test", function (xhr) {
|
||||
values = getParameters(xhr);
|
||||
xhr.respond(200, {}, template);
|
||||
});
|
||||
make(template);
|
||||
const button = byId("outside");
|
||||
button.focus();
|
||||
button.click();
|
||||
this.server.respond();
|
||||
values.should.deep.equal({name: "", outside: ""});
|
||||
button.focus();
|
||||
button.click();
|
||||
this.server.respond();
|
||||
values.should.deep.equal({name: "", outside: ""});
|
||||
})
|
||||
})
|
||||
|
@ -659,7 +659,7 @@ describe("Core htmx Events", function() {
|
||||
this.server.respond();
|
||||
div.innerHTML.should.equal("updated");
|
||||
} finally {
|
||||
htmx.off("htmx:load", handler);
|
||||
htmx.off("htmx:confirm", handler);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -172,6 +172,18 @@ describe("web-sockets extension", function () {
|
||||
this.messages.length.should.equal(1);
|
||||
})
|
||||
|
||||
it('sends data to the server with polling trigger', function () {
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div hx-trigger="every 1s" ws-send id="d1">div1</div></div>');
|
||||
this.tickMock();
|
||||
this.clock.tick(2000);
|
||||
|
||||
byId("d1").click();
|
||||
|
||||
this.tickMock();
|
||||
|
||||
this.messages.length.should.equal(2);
|
||||
})
|
||||
|
||||
it('sends expected headers to the server', function () {
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><button hx-trigger="click" hx-target="#target" ws-send id="d1" name="d1-name">div1</button><output id="target"></output></div>');
|
||||
this.tickMock();
|
||||
|
@ -70,6 +70,7 @@
|
||||
|
||||
<!-- attribute tests -->
|
||||
<script src="attributes/hx-boost.js"></script>
|
||||
<script src="attributes/hx-confirm.js"></script>
|
||||
<script src="attributes/hx-delete.js"></script>
|
||||
<script src="attributes/hx-ext.js"></script>
|
||||
<script src="attributes/hx-get.js"></script>
|
||||
|
@ -12,10 +12,14 @@ function make(htmlStr) {
|
||||
var wa = getWorkArea();
|
||||
var child = null;
|
||||
var children = fragment.children || fragment.childNodes; // IE
|
||||
var appendedChildren = []
|
||||
while(children.length > 0) {
|
||||
child = children[0];
|
||||
wa.appendChild(child);
|
||||
htmx.process(child);
|
||||
appendedChildren.push(child)
|
||||
}
|
||||
for (var i = 0; i < appendedChildren.length; i++) {
|
||||
htmx.process(appendedChildren[i]);
|
||||
}
|
||||
return child; // return last added element
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user