Skip to content

Commit db5167e

Browse files
committed
Merge branch 'dev'
2 parents c538c65 + 555522b commit db5167e

File tree

38 files changed

+1371
-223
lines changed

38 files changed

+1371
-223
lines changed

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## [1.9.9] - 2023-11-21
4+
5+
* Allow CSS selectors with whitespace in attributes like `hx-target` by using parens or curly-braces
6+
* Properly allow users to override the `Content-Type` request header
7+
* Added the `select` option to `htmx.ajax()`
8+
* Fixed a race condition in readystate detection that lead to htmx not being initialized in some scenarios with 3rd
9+
party script loaders
10+
* Fixed a bug that caused relative resources to resolve against the wrong base URL when a new URL is pushed
11+
* Fixed a UI issue that could cause indicators to briefly flash
12+
313
## [1.9.8] - 2023-11-06
414

515
* Fixed a few npm & build related issues

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ By removing these arbitrary constraints htmx completes HTML as a
3333
## quick start
3434

3535
```html
36-
<script src="https://unpkg.com/[email protected].8"></script>
36+
<script src="https://unpkg.com/[email protected].9"></script>
3737
<!-- have a button POST a click via AJAX -->
3838
<button hx-post="/clicked" hx-swap="outerHTML">
3939
Click Me

dist/ext/ws.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
200200
if (!this.socket) {
201201
api.triggerErrorEvent()
202202
}
203-
if (sendElt && api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
203+
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
204204
message: message,
205205
socketWrapper: this.publicInterface
206206
})) {

dist/htmx.d.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function ajax(verb: string, path: string, selector: string): Promise<void
4848
export function ajax(
4949
verb: string,
5050
path: string,
51-
context: Partial<{ source: any; event: any; handler: any; target: any; swap: any; values: any; headers: any }>
51+
context: Partial<{ source: any; event: any; handler: any; target: any; swap: any; values: any; headers: any; select: any }>
5252
): Promise<void>;
5353

5454
/**
@@ -395,6 +395,11 @@ export interface HtmxConfig {
395395
* @default false
396396
*/
397397
selfRequestsOnly?: boolean;
398+
/**
399+
* Whether or not the target of a boosted element is scrolled into the viewport.
400+
* @default true
401+
*/
402+
scrollIntoViewOnBoost?: boolean;
398403
}
399404

400405
/**

dist/htmx.js

+86-49
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ return (function () {
7575
globalViewTransitions: false,
7676
methodsThatUseUrlParams: ["get"],
7777
selfRequestsOnly: false,
78+
ignoreTitle: false,
7879
scrollIntoViewOnBoost: true
7980
},
8081
parseInterval:parseInterval,
@@ -87,7 +88,7 @@ return (function () {
8788
sock.binaryType = htmx.config.wsBinaryType;
8889
return sock;
8990
},
90-
version: "1.9.8"
91+
version: "1.9.9"
9192
};
9293

9394
/** @type {import("./htmx").HtmxInternalApi} */
@@ -1145,6 +1146,8 @@ return (function () {
11451146
var SYMBOL_CONT = /[_$a-zA-Z0-9]/;
11461147
var STRINGISH_START = ['"', "'", "/"];
11471148
var NOT_WHITESPACE = /[^\s]/;
1149+
var COMBINED_SELECTOR_START = /[{(]/;
1150+
var COMBINED_SELECTOR_END = /[})]/;
11481151
function tokenizeString(str) {
11491152
var tokens = [];
11501153
var position = 0;
@@ -1233,6 +1236,18 @@ return (function () {
12331236
return result;
12341237
}
12351238

1239+
function consumeCSSSelector(tokens) {
1240+
var result;
1241+
if (tokens.length > 0 && COMBINED_SELECTOR_START.test(tokens[0])) {
1242+
tokens.shift();
1243+
result = consumeUntil(tokens, COMBINED_SELECTOR_END).trim();
1244+
tokens.shift();
1245+
} else {
1246+
result = consumeUntil(tokens, WHITESPACE_OR_COMMA);
1247+
}
1248+
return result;
1249+
}
1250+
12361251
var INPUT_SELECTOR = 'input, textarea, select';
12371252

12381253
/**
@@ -1281,29 +1296,33 @@ return (function () {
12811296
triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
12821297
} else if (token === "from" && tokens[0] === ":") {
12831298
tokens.shift();
1284-
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
1285-
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
1286-
tokens.shift();
1287-
var selector = consumeUntil(
1288-
tokens,
1289-
WHITESPACE_OR_COMMA
1290-
)
1291-
// `next` and `previous` allow a selector-less syntax
1292-
if (selector.length > 0) {
1293-
from_arg += " " + selector;
1299+
if (COMBINED_SELECTOR_START.test(tokens[0])) {
1300+
var from_arg = consumeCSSSelector(tokens);
1301+
} else {
1302+
var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
1303+
if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") {
1304+
tokens.shift();
1305+
var selector = consumeCSSSelector(tokens);
1306+
// `next` and `previous` allow a selector-less syntax
1307+
if (selector.length > 0) {
1308+
from_arg += " " + selector;
1309+
}
12941310
}
12951311
}
12961312
triggerSpec.from = from_arg;
12971313
} else if (token === "target" && tokens[0] === ":") {
12981314
tokens.shift();
1299-
triggerSpec.target = consumeUntil(tokens, WHITESPACE_OR_COMMA);
1315+
triggerSpec.target = consumeCSSSelector(tokens);
13001316
} else if (token === "throttle" && tokens[0] === ":") {
13011317
tokens.shift();
13021318
triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
13031319
} else if (token === "queue" && tokens[0] === ":") {
13041320
tokens.shift();
13051321
triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA);
1306-
} else if ((token === "root" || token === "threshold") && tokens[0] === ":") {
1322+
} else if (token === "root" && tokens[0] === ":") {
1323+
tokens.shift();
1324+
triggerSpec[token] = consumeCSSSelector(tokens);
1325+
} else if (token === "threshold" && tokens[0] === ":") {
13071326
tokens.shift();
13081327
triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA);
13091328
} else {
@@ -2906,6 +2925,7 @@ return (function () {
29062925
values : context.values,
29072926
targetOverride: resolveTarget(context.target),
29082927
swapOverride: context.swap,
2928+
select: context.select,
29092929
returnPromise: true
29102930
});
29112931
}
@@ -2960,6 +2980,7 @@ return (function () {
29602980
elt = getDocument().body;
29612981
}
29622982
var responseHandler = etc.handler || handleAjaxResponse;
2983+
var select = etc.select || null;
29632984

29642985
if (!bodyContains(elt)) {
29652986
// do not issue requests for elements removed from the DOM
@@ -3108,6 +3129,11 @@ return (function () {
31083129

31093130

31103131
var headers = getHeaders(elt, target, promptResponse);
3132+
3133+
if (verb !== 'get' && !usesFormData(elt)) {
3134+
headers['Content-Type'] = 'application/x-www-form-urlencoded';
3135+
}
3136+
31113137
if (etc.headers) {
31123138
headers = mergeObjects(headers, etc.headers);
31133139
}
@@ -3121,10 +3147,6 @@ return (function () {
31213147
var allParameters = mergeObjects(rawParameters, expressionVars);
31223148
var filteredParameters = filterValues(allParameters, elt);
31233149

3124-
if (verb !== 'get' && !usesFormData(elt)) {
3125-
headers['Content-Type'] = 'application/x-www-form-urlencoded';
3126-
}
3127-
31283150
if (htmx.config.getCacheBusterParam && verb === 'get') {
31293151
filteredParameters['org.htmx.cache-buster'] = getRawAttribute(target, "id") || "true";
31303152
}
@@ -3222,7 +3244,7 @@ return (function () {
32223244
}
32233245

32243246
var responseInfo = {
3225-
xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted,
3247+
xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted, select: select,
32263248
pathInfo: {
32273249
requestPath: path,
32283250
finalRequestPath: finalPath,
@@ -3393,6 +3415,7 @@ return (function () {
33933415
var target = responseInfo.target;
33943416
var etc = responseInfo.etc;
33953417
var requestConfig = responseInfo.requestConfig;
3418+
var select = responseInfo.select;
33963419

33973420
if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return;
33983421

@@ -3502,10 +3525,26 @@ return (function () {
35023525
}
35033526

35043527
var selectOverride;
3528+
if (select) {
3529+
selectOverride = select;
3530+
}
3531+
35053532
if (hasHeader(xhr, /HX-Reselect:/i)) {
35063533
selectOverride = xhr.getResponseHeader("HX-Reselect");
35073534
}
35083535

3536+
// if we need to save history, do so, before swapping so that relative resources have the correct base URL
3537+
if (historyUpdate.type) {
3538+
triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
3539+
if (historyUpdate.type === "push") {
3540+
pushUrlIntoHistory(historyUpdate.path);
3541+
triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
3542+
} else {
3543+
replaceUrlInHistory(historyUpdate.path);
3544+
triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
3545+
}
3546+
}
3547+
35093548
var settleInfo = makeSettleInfo(target);
35103549
selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, selectOverride);
35113550

@@ -3555,17 +3594,6 @@ return (function () {
35553594
triggerEvent(elt, 'htmx:afterSettle', responseInfo);
35563595
});
35573596

3558-
// if we need to save history, do so
3559-
if (historyUpdate.type) {
3560-
triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo));
3561-
if (historyUpdate.type === "push") {
3562-
pushUrlIntoHistory(historyUpdate.path);
3563-
triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path});
3564-
} else {
3565-
replaceUrlInHistory(historyUpdate.path);
3566-
triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path});
3567-
}
3568-
}
35693597
if (responseInfo.pathInfo.anchor) {
35703598
var anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor);
35713599
if(anchorTarget) {
@@ -3724,35 +3752,44 @@ return (function () {
37243752
//====================================================================
37253753
// Initialization
37263754
//====================================================================
3727-
var isReady = false
3728-
getDocument().addEventListener('DOMContentLoaded', function() {
3729-
isReady = true
3730-
})
3731-
37323755
/**
3733-
* Execute a function now if DOMContentLoaded has fired, otherwise listen for it.
3734-
*
3735-
* This function uses isReady because there is no realiable way to ask the browswer whether
3736-
* the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded
3737-
* firing and readystate=complete.
3756+
* We want to initialize the page elements after DOMContentLoaded
3757+
* fires, but there isn't always a good way to tell whether
3758+
* it has already fired when we get here or not.
37383759
*/
3739-
function ready(fn) {
3740-
// Checking readyState here is a failsafe in case the htmx script tag entered the DOM by
3741-
// some means other than the initial page load.
3742-
if (isReady || getDocument().readyState === 'complete') {
3743-
fn();
3744-
} else {
3745-
getDocument().addEventListener('DOMContentLoaded', fn);
3760+
function ready(functionToCall) {
3761+
// call the function exactly once no matter how many times this is called
3762+
var callReadyFunction = function() {
3763+
if (!functionToCall) return;
3764+
functionToCall();
3765+
functionToCall = null;
3766+
};
3767+
3768+
if (getDocument().readyState === "complete") {
3769+
// DOMContentLoaded definitely fired, we can initialize the page
3770+
callReadyFunction();
3771+
}
3772+
else {
3773+
/* DOMContentLoaded *maybe* already fired, wait for
3774+
* the next DOMContentLoaded or readystatechange event
3775+
*/
3776+
getDocument().addEventListener("DOMContentLoaded", function() {
3777+
callReadyFunction();
3778+
});
3779+
getDocument().addEventListener("readystatechange", function() {
3780+
if (getDocument().readyState !== "complete") return;
3781+
callReadyFunction();
3782+
});
37463783
}
37473784
}
37483785

37493786
function insertIndicatorStyles() {
37503787
if (htmx.config.includeIndicatorStyles !== false) {
37513788
getDocument().head.insertAdjacentHTML("beforeend",
37523789
"<style>\
3753-
." + htmx.config.indicatorClass + "{opacity:0;transition: opacity 200ms ease-in;}\
3754-
." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1}\
3755-
." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1}\
3790+
." + htmx.config.indicatorClass + "{opacity:0}\
3791+
." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\
3792+
." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\
37563793
</style>");
37573794
}
37583795
}

dist/htmx.min.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/htmx.min.js.gz

126 Bytes
Binary file not shown.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"AJAX",
66
"HTML"
77
],
8-
"version": "1.9.8",
8+
"version": "1.9.9",
99
"homepage": "https://htmx.org/",
1010
"bugs": {
1111
"url": "https://github.com/bigskysoftware/htmx/issues"

src/ext/ws.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
200200
if (!this.socket) {
201201
api.triggerErrorEvent()
202202
}
203-
if (sendElt && api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
203+
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
204204
message: message,
205205
socketWrapper: this.publicInterface
206206
})) {

src/htmx.d.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function ajax(verb: string, path: string, selector: string): Promise<void
4848
export function ajax(
4949
verb: string,
5050
path: string,
51-
context: Partial<{ source: any; event: any; handler: any; target: any; swap: any; values: any; headers: any }>
51+
context: Partial<{ source: any; event: any; handler: any; target: any; swap: any; values: any; headers: any; select: any }>
5252
): Promise<void>;
5353

5454
/**
@@ -395,6 +395,11 @@ export interface HtmxConfig {
395395
* @default false
396396
*/
397397
selfRequestsOnly?: boolean;
398+
/**
399+
* Whether or not the target of a boosted element is scrolled into the viewport.
400+
* @default true
401+
*/
402+
scrollIntoViewOnBoost?: boolean;
398403
}
399404

400405
/**

0 commit comments

Comments
 (0)