htmx.org 4.0.0-alpha1 → 4.0.0-alpha2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/editors/jetbrains/htmx.svg +46 -0
- package/dist/editors/jetbrains/htmx_dark.svg +46 -0
- package/dist/ext/hx-live.js +624 -0
- package/dist/ext/hx-live.min.js +1 -0
- package/dist/ext/hx-live.min.js.map +1 -0
- package/dist/ext/hx-optimistic.js +81 -0
- package/dist/ext/hx-optimistic.min.js +1 -0
- package/dist/ext/hx-optimistic.min.js.map +1 -0
- package/dist/ext/hx-preload.js +83 -0
- package/dist/ext/hx-preload.min.js +1 -0
- package/dist/ext/hx-preload.min.js.map +1 -0
- package/dist/htmx.esm.js +268 -154
- package/dist/htmx.esm.min.js +1 -1
- package/dist/htmx.esm.min.js.map +1 -0
- package/dist/htmx.js +262 -149
- package/dist/htmx.min.js +1 -1
- package/dist/htmx.min.js.map +1 -0
- package/package.json +17 -8
- package/dist/htmx.esm.js.br +0 -0
- package/dist/htmx.esm.min.js.br +0 -0
- package/dist/htmx.js.br +0 -0
- package/dist/htmx.min.js.br +0 -0
- /package/{src → dist}/editors/jetbrains/htmx.web-types.json +0 -0
package/dist/htmx.esm.js
CHANGED
|
@@ -59,7 +59,8 @@ var htmx = (() => {
|
|
|
59
59
|
class Htmx {
|
|
60
60
|
|
|
61
61
|
#extMethods = new Map();
|
|
62
|
-
#approvedExt =
|
|
62
|
+
#approvedExt = '';
|
|
63
|
+
#registeredExt = new Set();
|
|
63
64
|
#internalAPI;
|
|
64
65
|
#actionSelector
|
|
65
66
|
#boostSelector = "a,form";
|
|
@@ -72,7 +73,7 @@ var htmx = (() => {
|
|
|
72
73
|
this.#initHtmxConfig();
|
|
73
74
|
this.#initRequestIndicatorCss();
|
|
74
75
|
this.#actionSelector = `[${this.#prefix("hx-action")}],[${this.#prefix("hx-get")}],[${this.#prefix("hx-post")}],[${this.#prefix("hx-put")}],[${this.#prefix("hx-patch")}],[${this.#prefix("hx-delete")}]`;
|
|
75
|
-
this.#hxOnQuery = new XPathEvaluator().createExpression(`.//*[@*[ starts-with(name(), "${this.#prefix("hx-on
|
|
76
|
+
this.#hxOnQuery = new XPathEvaluator().createExpression(`.//*[@*[ starts-with(name(), "${this.#prefix("hx-on")}")]]`);
|
|
76
77
|
this.#internalAPI = {
|
|
77
78
|
attributeValue: this.#attributeValue.bind(this),
|
|
78
79
|
parseTriggerSpecs: this.#parseTriggerSpecs.bind(this),
|
|
@@ -89,6 +90,7 @@ var htmx = (() => {
|
|
|
89
90
|
|
|
90
91
|
#initHtmxConfig() {
|
|
91
92
|
this.config = {
|
|
93
|
+
version: '4.0.0-alpha2',
|
|
92
94
|
logAll: false,
|
|
93
95
|
prefix: "",
|
|
94
96
|
transitions: true,
|
|
@@ -109,7 +111,8 @@ var htmx = (() => {
|
|
|
109
111
|
pauseHidden: false
|
|
110
112
|
},
|
|
111
113
|
morphIgnore: ["data-htmx-powered"],
|
|
112
|
-
noSwap: [204],
|
|
114
|
+
noSwap: [204, 304],
|
|
115
|
+
implicitInheritance: false
|
|
113
116
|
}
|
|
114
117
|
let metaConfig = document.querySelector('meta[name="htmx:config"]');
|
|
115
118
|
if (metaConfig) {
|
|
@@ -124,7 +127,7 @@ var htmx = (() => {
|
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
129
|
}
|
|
127
|
-
this.#approvedExt =
|
|
130
|
+
this.#approvedExt = this.config.extensions;
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
#initRequestIndicatorCss() {
|
|
@@ -144,7 +147,9 @@ var htmx = (() => {
|
|
|
144
147
|
}
|
|
145
148
|
|
|
146
149
|
defineExtension(name, extension) {
|
|
147
|
-
if (!this.#approvedExt.
|
|
150
|
+
if (this.#approvedExt && !this.#approvedExt.split(/,\s*/).includes(name)) return false;
|
|
151
|
+
if (this.#registeredExt.has(name)) return false;
|
|
152
|
+
this.#registeredExt.add(name);
|
|
148
153
|
if (extension.init) extension.init(this.#internalAPI);
|
|
149
154
|
Object.entries(extension).forEach(([key, value]) => {
|
|
150
155
|
if(!this.#extMethods.get(key)?.push(value)) this.#extMethods.set(key, [value]);
|
|
@@ -174,31 +179,40 @@ var htmx = (() => {
|
|
|
174
179
|
style === 'append' ? 'beforeend' : style;
|
|
175
180
|
}
|
|
176
181
|
|
|
177
|
-
#attributeValue(elt, name, defaultVal) {
|
|
182
|
+
#attributeValue(elt, name, defaultVal, returnElt) {
|
|
178
183
|
name = this.#prefix(name);
|
|
179
|
-
let appendName = name + ":append";
|
|
180
|
-
let inheritName = name + ":inherited";
|
|
181
|
-
let inheritAppendName = name + ":inherited:append";
|
|
184
|
+
let appendName = name + this.#maybeAdjustMetaCharacter(":append");
|
|
185
|
+
let inheritName = name + (this.config.implicitInheritance ? "" : this.#maybeAdjustMetaCharacter(":inherited"));
|
|
186
|
+
let inheritAppendName = name + this.#maybeAdjustMetaCharacter(":inherited:append");
|
|
182
187
|
|
|
183
|
-
if (elt.hasAttribute(name)
|
|
184
|
-
return elt
|
|
188
|
+
if (elt.hasAttribute(name)) {
|
|
189
|
+
return returnElt ? elt : elt.getAttribute(name);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (elt.hasAttribute(inheritName)) {
|
|
193
|
+
return returnElt ? elt : elt.getAttribute(inheritName);
|
|
185
194
|
}
|
|
186
195
|
|
|
187
196
|
if (elt.hasAttribute(appendName) || elt.hasAttribute(inheritAppendName)) {
|
|
188
197
|
let appendValue = elt.getAttribute(appendName) || elt.getAttribute(inheritAppendName);
|
|
189
198
|
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
|
|
190
199
|
if (parent) {
|
|
191
|
-
let
|
|
192
|
-
return
|
|
200
|
+
let inherited = this.#attributeValue(parent, name, undefined, returnElt);
|
|
201
|
+
return returnElt ? inherited : (inherited ? inherited + "," + appendValue : appendValue);
|
|
202
|
+
} else {
|
|
203
|
+
return returnElt ? elt : appendValue;
|
|
193
204
|
}
|
|
194
|
-
return appendValue;
|
|
195
205
|
}
|
|
196
206
|
|
|
197
207
|
let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
|
|
198
208
|
if (parent) {
|
|
199
|
-
|
|
209
|
+
let val = this.#attributeValue(parent, name, undefined, returnElt);
|
|
210
|
+
if (!returnElt && val && this.config.implicitInheritance) {
|
|
211
|
+
this.#triggerExtensions(elt, "htmx:after:implicitInheritance", {elt, parent})
|
|
212
|
+
}
|
|
213
|
+
return val;
|
|
200
214
|
}
|
|
201
|
-
return defaultVal;
|
|
215
|
+
return returnElt ? elt : defaultVal;
|
|
202
216
|
}
|
|
203
217
|
|
|
204
218
|
#tokenize(str) {
|
|
@@ -263,13 +277,13 @@ var htmx = (() => {
|
|
|
263
277
|
if (this.#isBoosted(elt)) {
|
|
264
278
|
return this.#boostedMethodAndAction(elt, evt)
|
|
265
279
|
} else {
|
|
266
|
-
let method = this.#attributeValue(elt, "hx-method") || "
|
|
280
|
+
let method = this.#attributeValue(elt, "hx-method") || "GET"
|
|
267
281
|
let action = this.#attributeValue(elt, "hx-action");
|
|
268
282
|
if (!action) {
|
|
269
283
|
for (let verb of this.#verbs) {
|
|
270
|
-
let
|
|
271
|
-
if (
|
|
272
|
-
action =
|
|
284
|
+
let verbAction = this.#attributeValue(elt, "hx-" + verb);
|
|
285
|
+
if (verbAction) {
|
|
286
|
+
action = verbAction;
|
|
273
287
|
method = verb;
|
|
274
288
|
break;
|
|
275
289
|
}
|
|
@@ -342,8 +356,8 @@ var htmx = (() => {
|
|
|
342
356
|
for (let key in configOverrides) {
|
|
343
357
|
if (key.startsWith('+')) {
|
|
344
358
|
let actualKey = key.substring(1);
|
|
345
|
-
if (requestConfig[actualKey] && typeof
|
|
346
|
-
Object.assign(
|
|
359
|
+
if (requestConfig[actualKey] && typeof requestConfig[actualKey] === 'object') {
|
|
360
|
+
Object.assign(requestConfig[actualKey], configOverrides[key]);
|
|
347
361
|
} else {
|
|
348
362
|
requestConfig[actualKey] = configOverrides[key];
|
|
349
363
|
}
|
|
@@ -351,8 +365,14 @@ var htmx = (() => {
|
|
|
351
365
|
requestConfig[key] = configOverrides[key];
|
|
352
366
|
}
|
|
353
367
|
}
|
|
368
|
+
if (requestConfig.etag) {
|
|
369
|
+
sourceElement._htmx ||= {}
|
|
370
|
+
sourceElement._htmx.etag ||= requestConfig.etag
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (sourceElement._htmx?.etag) {
|
|
374
|
+
ctx.request.headers["If-none-match"] = sourceElement._htmx.etag
|
|
354
375
|
}
|
|
355
|
-
|
|
356
376
|
return ctx;
|
|
357
377
|
}
|
|
358
378
|
|
|
@@ -375,11 +395,7 @@ var htmx = (() => {
|
|
|
375
395
|
if (selector instanceof Element) {
|
|
376
396
|
return selector;
|
|
377
397
|
} else if (selector === 'this') {
|
|
378
|
-
|
|
379
|
-
return elt;
|
|
380
|
-
} else {
|
|
381
|
-
return elt.closest(`[${this.#prefix("hx-target")}\\:inherited='this']`)
|
|
382
|
-
}
|
|
398
|
+
return this.#attributeValue(elt, "hx-target", undefined, true);
|
|
383
399
|
} else if (selector != null) {
|
|
384
400
|
return this.find(elt, selector);
|
|
385
401
|
} else if (this.#isBoosted(elt)) {
|
|
@@ -390,10 +406,10 @@ var htmx = (() => {
|
|
|
390
406
|
}
|
|
391
407
|
|
|
392
408
|
#isBoosted(elt) {
|
|
393
|
-
return elt
|
|
409
|
+
return elt?._htmx?.boosted;
|
|
394
410
|
}
|
|
395
411
|
|
|
396
|
-
async
|
|
412
|
+
async #handleTriggerEvent(ctx) {
|
|
397
413
|
let elt = ctx.sourceElement
|
|
398
414
|
let evt = ctx.sourceEvent
|
|
399
415
|
if (!elt.isConnected) return
|
|
@@ -419,7 +435,7 @@ var htmx = (() => {
|
|
|
419
435
|
// Setup abort controller and action
|
|
420
436
|
let ac = new AbortController()
|
|
421
437
|
let action = ctx.request.action.replace?.(/#.*$/, '')
|
|
422
|
-
// TODO - consider how this works with hx-config, move most to
|
|
438
|
+
// TODO - consider how this works with hx-config, move most to #createRequestContext?
|
|
423
439
|
Object.assign(ctx.request, {
|
|
424
440
|
originalAction: ctx.request.action,
|
|
425
441
|
action,
|
|
@@ -452,7 +468,7 @@ var htmx = (() => {
|
|
|
452
468
|
await this.#issueRequest(ctx);
|
|
453
469
|
}
|
|
454
470
|
|
|
455
|
-
async
|
|
471
|
+
async #issueRequest(ctx) {
|
|
456
472
|
let elt = ctx.sourceElement
|
|
457
473
|
let syncStrategy = this.#determineSyncStrategy(elt);
|
|
458
474
|
let requestQueue = this.#getRequestQueue(elt);
|
|
@@ -469,11 +485,11 @@ var htmx = (() => {
|
|
|
469
485
|
|
|
470
486
|
try {
|
|
471
487
|
// Confirm dialog
|
|
472
|
-
let confirmVal = this.#attributeValue(elt, 'hx-confirm')
|
|
488
|
+
let confirmVal = this.#attributeValue(elt, 'hx-confirm');
|
|
473
489
|
if (confirmVal) {
|
|
474
490
|
let js = this.#extractJavascriptContent(confirmVal);
|
|
475
491
|
if (js) {
|
|
476
|
-
if (!await this.#executeJavaScriptAsync(
|
|
492
|
+
if (!await this.#executeJavaScriptAsync(elt, {}, js, true)) {
|
|
477
493
|
return
|
|
478
494
|
}
|
|
479
495
|
} else {
|
|
@@ -483,10 +499,10 @@ var htmx = (() => {
|
|
|
483
499
|
}
|
|
484
500
|
}
|
|
485
501
|
|
|
486
|
-
ctx.fetch ||= window.fetch
|
|
502
|
+
ctx.fetch ||= window.fetch.bind(window)
|
|
487
503
|
if (!this.#trigger(elt, "htmx:before:request", {ctx})) return;
|
|
488
504
|
|
|
489
|
-
let response = await
|
|
505
|
+
let response = await ctx.fetch(ctx.request.action, ctx.request);
|
|
490
506
|
|
|
491
507
|
ctx.response = {
|
|
492
508
|
raw: response,
|
|
@@ -494,9 +510,13 @@ var htmx = (() => {
|
|
|
494
510
|
headers: response.headers,
|
|
495
511
|
}
|
|
496
512
|
this.#extractHxHeaders(ctx);
|
|
513
|
+
ctx.isSSE = response.headers.get("Content-Type")?.includes('text/event-stream');
|
|
514
|
+
if (!ctx.isSSE) {
|
|
515
|
+
ctx.text = await response.text();
|
|
516
|
+
}
|
|
497
517
|
if (!this.#trigger(elt, "htmx:after:request", {ctx})) return;
|
|
498
518
|
|
|
499
|
-
if(this.#
|
|
519
|
+
if(this.#handleHeadersAndMaybeReturnEarly(ctx)){
|
|
500
520
|
return
|
|
501
521
|
}
|
|
502
522
|
|
|
@@ -506,16 +526,13 @@ var htmx = (() => {
|
|
|
506
526
|
await this.#handleSSE(ctx, elt, response);
|
|
507
527
|
} else {
|
|
508
528
|
// HTTP response
|
|
509
|
-
ctx.text = await response.text();
|
|
510
529
|
if (ctx.status === "issuing") {
|
|
511
530
|
if (ctx.hx.retarget) ctx.target = this.#resolveTarget(elt, ctx.hx.retarget);
|
|
512
531
|
if (ctx.hx.reswap) ctx.swap = ctx.hx.reswap;
|
|
513
532
|
if (ctx.hx.reselect) ctx.select = ctx.hx.reselect;
|
|
514
533
|
ctx.status = "response received";
|
|
515
534
|
this.#handleStatusCodes(ctx);
|
|
516
|
-
this.#handleHistoryUpdate(ctx);
|
|
517
535
|
await this.swap(ctx);
|
|
518
|
-
this.#handleAnchorScroll(ctx)
|
|
519
536
|
ctx.status = "swapped";
|
|
520
537
|
}
|
|
521
538
|
}
|
|
@@ -537,7 +554,7 @@ var htmx = (() => {
|
|
|
537
554
|
}
|
|
538
555
|
|
|
539
556
|
// Extract HX-* headers into ctx.hx
|
|
540
|
-
#extractHxHeaders(ctx
|
|
557
|
+
#extractHxHeaders(ctx) {
|
|
541
558
|
ctx.hx = {}
|
|
542
559
|
for (let [k, v] of ctx.response.raw.headers) {
|
|
543
560
|
if (k.toLowerCase().startsWith('hx-')) {
|
|
@@ -547,7 +564,7 @@ var htmx = (() => {
|
|
|
547
564
|
}
|
|
548
565
|
|
|
549
566
|
// returns true if the header aborts the current response handling
|
|
550
|
-
#
|
|
567
|
+
#handleHeadersAndMaybeReturnEarly(ctx) {
|
|
551
568
|
if (ctx.hx.trigger) {
|
|
552
569
|
this.#handleTriggerHeader(ctx.hx.trigger, ctx.sourceElement);
|
|
553
570
|
}
|
|
@@ -570,9 +587,13 @@ var htmx = (() => {
|
|
|
570
587
|
this.ajax('GET', path, opts);
|
|
571
588
|
return true // TODO this seems legit
|
|
572
589
|
}
|
|
590
|
+
if(ctx.response?.headers?.get?.("Etag")) {
|
|
591
|
+
ctx.sourceElement._htmx ||= {}
|
|
592
|
+
ctx.sourceElement._htmx.etag = ctx.response.headers.get("Etag");
|
|
593
|
+
}
|
|
573
594
|
}
|
|
574
595
|
|
|
575
|
-
async
|
|
596
|
+
async #handleSSE(ctx, elt, response) {
|
|
576
597
|
let config = elt._htmx?.streamConfig || {...this.config.streams};
|
|
577
598
|
|
|
578
599
|
let waitForVisible = () => new Promise(r => {
|
|
@@ -650,9 +671,7 @@ var htmx = (() => {
|
|
|
650
671
|
ctx.status = "stream message received";
|
|
651
672
|
|
|
652
673
|
if (!ctx.response.cancelled) {
|
|
653
|
-
this.#handleHistoryUpdate(ctx);
|
|
654
674
|
await this.swap(ctx);
|
|
655
|
-
this.#handleAnchorScroll(ctx);
|
|
656
675
|
ctx.status = "swapped";
|
|
657
676
|
}
|
|
658
677
|
this.#trigger(elt, "htmx:after:sse:message", {ctx, message: msg});
|
|
@@ -669,23 +688,56 @@ var htmx = (() => {
|
|
|
669
688
|
}
|
|
670
689
|
}
|
|
671
690
|
|
|
672
|
-
async*
|
|
673
|
-
let
|
|
674
|
-
|
|
691
|
+
async* #parseSSE(response) {
|
|
692
|
+
let reader = response.body.getReader();
|
|
693
|
+
let decoder = new TextDecoder();
|
|
694
|
+
let buffer = '';
|
|
695
|
+
let message = {data: '', event: '', id: '', retry: null};
|
|
696
|
+
|
|
675
697
|
try {
|
|
676
|
-
while (
|
|
677
|
-
let {done, value} = await
|
|
698
|
+
while (true) {
|
|
699
|
+
let {done, value} = await reader.read();
|
|
678
700
|
if (done) break;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
701
|
+
|
|
702
|
+
// Decode chunk and add to buffer
|
|
703
|
+
buffer += decoder.decode(value, {stream: true});
|
|
704
|
+
let lines = buffer.split('\n');
|
|
705
|
+
// Keep incomplete line in buffer
|
|
706
|
+
buffer = lines.pop() || '';
|
|
707
|
+
|
|
708
|
+
for (let line of lines) {
|
|
709
|
+
// Empty line or carriage return indicates end of message
|
|
710
|
+
if (!line || line === '\r') {
|
|
711
|
+
if (message.data) {
|
|
712
|
+
yield message;
|
|
713
|
+
message = {data: '', event: '', id: '', retry: null};
|
|
714
|
+
}
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Parse field: value
|
|
719
|
+
let colonIndex = line.indexOf(':');
|
|
720
|
+
if (colonIndex <= 0) continue;
|
|
721
|
+
|
|
722
|
+
let field = line.slice(0, colonIndex);
|
|
723
|
+
let value = line.slice(colonIndex + 1).trimStart();
|
|
724
|
+
|
|
725
|
+
if (field === 'data') {
|
|
726
|
+
message.data += (message.data ? '\n' : '') + value;
|
|
727
|
+
} else if (field === 'event') {
|
|
728
|
+
message.event = value;
|
|
729
|
+
} else if (field === 'id') {
|
|
730
|
+
message.id = value;
|
|
731
|
+
} else if (field === 'retry') {
|
|
732
|
+
let retryValue = parseInt(value, 10);
|
|
733
|
+
if (!isNaN(retryValue)) {
|
|
734
|
+
message.retry = retryValue;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
686
738
|
}
|
|
687
739
|
} finally {
|
|
688
|
-
|
|
740
|
+
reader.releaseLock();
|
|
689
741
|
}
|
|
690
742
|
}
|
|
691
743
|
|
|
@@ -961,7 +1013,7 @@ var htmx = (() => {
|
|
|
961
1013
|
return bound;
|
|
962
1014
|
}
|
|
963
1015
|
|
|
964
|
-
async
|
|
1016
|
+
async #executeJavaScriptAsync(thisArg, obj, code, expression = true) {
|
|
965
1017
|
let args = {}
|
|
966
1018
|
Object.assign(args, this.#apiMethods(thisArg))
|
|
967
1019
|
Object.assign(args, obj)
|
|
@@ -1002,23 +1054,45 @@ var htmx = (() => {
|
|
|
1002
1054
|
}
|
|
1003
1055
|
|
|
1004
1056
|
#maybeBoost(elt) {
|
|
1005
|
-
if (this.#attributeValue(elt, "hx-boost") === "true") {
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
elt.
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
elt.
|
|
1015
|
-
|
|
1016
|
-
|
|
1057
|
+
if (this.#attributeValue(elt, "hx-boost") === "true" && this.#shouldBoost(elt)) {
|
|
1058
|
+
elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt), requests: [], boosted: true}
|
|
1059
|
+
elt.setAttribute('data-htmx-powered', 'true');
|
|
1060
|
+
if (elt.matches('a') && !elt.hasAttribute("target")) {
|
|
1061
|
+
elt.addEventListener('click', (click) => {
|
|
1062
|
+
elt._htmx.eventHandler(click)
|
|
1063
|
+
})
|
|
1064
|
+
} else {
|
|
1065
|
+
elt.addEventListener('submit', (submit) => {
|
|
1066
|
+
elt._htmx.eventHandler(submit)
|
|
1067
|
+
})
|
|
1068
|
+
}
|
|
1069
|
+
this.#trigger(elt, "htmx:after:init", {}, true)
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
#shouldBoost(elt) {
|
|
1074
|
+
if (this.#shouldInitialize(elt)) {
|
|
1075
|
+
if (elt.tagName === "A") {
|
|
1076
|
+
if (elt.target === '' || elt.target === '_self') {
|
|
1077
|
+
return !elt.getAttribute('href')?.startsWith?.("#") && this.#isSameOrigin(elt.href)
|
|
1017
1078
|
}
|
|
1079
|
+
} else if (elt.tagName === "FORM") {
|
|
1080
|
+
return elt.method !== 'dialog' && this.#isSameOrigin(elt.action);
|
|
1018
1081
|
}
|
|
1019
1082
|
}
|
|
1020
1083
|
}
|
|
1021
1084
|
|
|
1085
|
+
#isSameOrigin(url) {
|
|
1086
|
+
try {
|
|
1087
|
+
// URL constructor handles both relative and absolute URLs
|
|
1088
|
+
const parsed = new URL(url, window.location.href);
|
|
1089
|
+
return parsed.origin === window.location.origin;
|
|
1090
|
+
} catch (e) {
|
|
1091
|
+
// If URL parsing fails, assume not same-origin
|
|
1092
|
+
return false;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1022
1096
|
#shouldInitialize(elt) {
|
|
1023
1097
|
return !elt._htmx && !this.#ignore(elt);
|
|
1024
1098
|
}
|
|
@@ -1076,8 +1150,9 @@ var htmx = (() => {
|
|
|
1076
1150
|
}
|
|
1077
1151
|
|
|
1078
1152
|
#makeFragment(text) {
|
|
1079
|
-
let response = text.replace(/<partial(\s+|>)/gi, '<template partial$1').replace(/<\/partial>/gi, '</template>');
|
|
1080
|
-
|
|
1153
|
+
let response = text.replace(/<hx-partial(\s+|>)/gi, '<template partial$1').replace(/<\/hx-partial>/gi, '</template>');
|
|
1154
|
+
let title = '';
|
|
1155
|
+
response = response.replace(/<title[^>]*>[\s\S]*?<\/title>/i, m => (title = this.#parseHTML(m).title, ''));
|
|
1081
1156
|
let responseWithNoHead = response.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i, '');
|
|
1082
1157
|
let startTag = responseWithNoHead.match(/<([a-z][^\/>\x20\t\r\n\f]*)/i)?.[1]?.toLowerCase();
|
|
1083
1158
|
|
|
@@ -1092,49 +1167,28 @@ var htmx = (() => {
|
|
|
1092
1167
|
doc = this.#parseHTML(`<template>${responseWithNoHead}</template>`);
|
|
1093
1168
|
fragment = doc.querySelector('template').content;
|
|
1094
1169
|
}
|
|
1170
|
+
this.#processScripts(fragment);
|
|
1095
1171
|
|
|
1096
1172
|
return {
|
|
1097
1173
|
fragment,
|
|
1098
|
-
title
|
|
1174
|
+
title
|
|
1099
1175
|
};
|
|
1100
1176
|
}
|
|
1101
1177
|
|
|
1102
1178
|
#createOOBTask(tasks, elt, oobValue, sourceElement) {
|
|
1103
|
-
// Handle legacy format: swapStyle:target (only if no spaces, which indicate modifiers)
|
|
1104
1179
|
let target = elt.id ? '#' + CSS.escape(elt.id) : null;
|
|
1105
1180
|
if (oobValue !== 'true' && oobValue && !oobValue.includes(' ')) {
|
|
1106
|
-
|
|
1107
|
-
if (colonIdx !== -1) {
|
|
1108
|
-
target = oobValue.substring(colonIdx + 1);
|
|
1109
|
-
oobValue = oobValue.substring(0, colonIdx);
|
|
1110
|
-
}
|
|
1181
|
+
[oobValue, target = target] = oobValue.split(/:(.*)/);
|
|
1111
1182
|
}
|
|
1112
1183
|
if (oobValue === 'true' || !oobValue) oobValue = 'outerHTML';
|
|
1113
1184
|
|
|
1114
1185
|
let swapSpec = this.#parseSwapSpec(oobValue);
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
let fragment;
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
}
|
|
1122
|
-
if (swapSpec.strip) {
|
|
1123
|
-
fragment = oobElementClone.content || oobElementClone;
|
|
1124
|
-
} else {
|
|
1125
|
-
fragment = document.createDocumentFragment();
|
|
1126
|
-
fragment.appendChild(oobElementClone);
|
|
1127
|
-
}
|
|
1128
|
-
elt.remove();
|
|
1129
|
-
if (!target && !oobValue.includes('target:')) return;
|
|
1130
|
-
|
|
1131
|
-
tasks.push({
|
|
1132
|
-
type: 'oob',
|
|
1133
|
-
fragment,
|
|
1134
|
-
target,
|
|
1135
|
-
swapSpec,
|
|
1136
|
-
sourceElement
|
|
1137
|
-
});
|
|
1186
|
+
target = swapSpec.target || target;
|
|
1187
|
+
swapSpec.strip ??= !swapSpec.style.startsWith('outer');
|
|
1188
|
+
if (!target) return;
|
|
1189
|
+
let fragment = document.createDocumentFragment();
|
|
1190
|
+
fragment.append(elt);
|
|
1191
|
+
tasks.push({type: 'oob', fragment, target, swapSpec, sourceElement});
|
|
1138
1192
|
}
|
|
1139
1193
|
|
|
1140
1194
|
#processOOB(fragment, sourceElement, selectOOB) {
|
|
@@ -1143,9 +1197,7 @@ var htmx = (() => {
|
|
|
1143
1197
|
// Process hx-select-oob first (select elements from response)
|
|
1144
1198
|
if (selectOOB) {
|
|
1145
1199
|
for (let spec of selectOOB.split(',')) {
|
|
1146
|
-
let [selector,
|
|
1147
|
-
let oobValue = rest.length ? rest.join(':') : 'true';
|
|
1148
|
-
|
|
1200
|
+
let [selector, oobValue = 'true'] = spec.split(/:(.*)/);
|
|
1149
1201
|
for (let elt of fragment.querySelectorAll(selector)) {
|
|
1150
1202
|
this.#createOOBTask(tasks, elt, oobValue, sourceElement);
|
|
1151
1203
|
}
|
|
@@ -1222,13 +1274,42 @@ var htmx = (() => {
|
|
|
1222
1274
|
return tasks;
|
|
1223
1275
|
}
|
|
1224
1276
|
|
|
1225
|
-
#
|
|
1226
|
-
|
|
1227
|
-
|
|
1277
|
+
#handleAutoFocus(elt) {
|
|
1278
|
+
let autofocus = this.find(elt, "[autofocus]");
|
|
1279
|
+
autofocus?.focus?.()
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
#handleScroll(task) {
|
|
1283
|
+
if (task.swapSpec.scroll) {
|
|
1284
|
+
let target;
|
|
1285
|
+
let [selectorOrValue, value] = task.swapSpec.scroll.split(":");
|
|
1286
|
+
if (value) {
|
|
1287
|
+
target = this.#findExt(selectorOrValue);
|
|
1288
|
+
} else {
|
|
1289
|
+
target = task.target;
|
|
1290
|
+
value = selectorOrValue
|
|
1291
|
+
}
|
|
1292
|
+
if (value === 'top') {
|
|
1293
|
+
target.scrollTop = 0;
|
|
1294
|
+
} else if (value === 'bottom'){
|
|
1295
|
+
target.scrollTop = target.scrollHeight;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (task.swapSpec.show) {
|
|
1299
|
+
let target;
|
|
1300
|
+
let [selectorOrValue, value] = task.swapSpec.show.split(":");
|
|
1301
|
+
if (value) {
|
|
1302
|
+
target = this.#findExt(selectorOrValue);
|
|
1303
|
+
} else {
|
|
1304
|
+
target = task.target;
|
|
1305
|
+
value = selectorOrValue
|
|
1306
|
+
}
|
|
1307
|
+
target.scrollIntoView(value === 'top')
|
|
1308
|
+
}
|
|
1228
1309
|
}
|
|
1229
1310
|
|
|
1230
1311
|
#handleAnchorScroll(ctx) {
|
|
1231
|
-
let anchor = ctx.request
|
|
1312
|
+
let anchor = ctx.request?.originalAction?.split('#')[1];
|
|
1232
1313
|
if (anchor) {
|
|
1233
1314
|
document.getElementById(anchor)?.scrollIntoView({block: 'start', behavior: 'auto'});
|
|
1234
1315
|
}
|
|
@@ -1254,7 +1335,9 @@ var htmx = (() => {
|
|
|
1254
1335
|
//============================================================================================
|
|
1255
1336
|
|
|
1256
1337
|
async swap(ctx) {
|
|
1338
|
+
this.#handleHistoryUpdate(ctx);
|
|
1257
1339
|
let {fragment, title} = this.#makeFragment(ctx.text);
|
|
1340
|
+
ctx.title = title;
|
|
1258
1341
|
let tasks = [];
|
|
1259
1342
|
|
|
1260
1343
|
// Process OOB and partials
|
|
@@ -1263,7 +1346,7 @@ var htmx = (() => {
|
|
|
1263
1346
|
tasks.push(...oobTasks, ...partialTasks);
|
|
1264
1347
|
|
|
1265
1348
|
// Process main swap
|
|
1266
|
-
let mainSwap = this.#processMainSwap(ctx, fragment, partialTasks
|
|
1349
|
+
let mainSwap = this.#processMainSwap(ctx, fragment, partialTasks);
|
|
1267
1350
|
if (mainSwap) {
|
|
1268
1351
|
tasks.push(mainSwap);
|
|
1269
1352
|
}
|
|
@@ -1299,7 +1382,7 @@ var htmx = (() => {
|
|
|
1299
1382
|
}
|
|
1300
1383
|
|
|
1301
1384
|
this.#trigger(document, "htmx:after:swap", {ctx});
|
|
1302
|
-
if (mainSwap?.
|
|
1385
|
+
if (ctx.title && !mainSwap?.swapSpec?.ignoreTitle) document.title = ctx.title;
|
|
1303
1386
|
await this.timeout(1);
|
|
1304
1387
|
// invoke restore tasks
|
|
1305
1388
|
for (let task of tasks) {
|
|
@@ -1307,38 +1390,32 @@ var htmx = (() => {
|
|
|
1307
1390
|
restore()
|
|
1308
1391
|
}
|
|
1309
1392
|
}
|
|
1310
|
-
this.#trigger(document, "htmx:after:restore", {ctx});
|
|
1393
|
+
this.#trigger(document, "htmx:after:restore", { ctx });
|
|
1394
|
+
this.#handleAnchorScroll(ctx);
|
|
1311
1395
|
// TODO this stuff should be an extension
|
|
1312
1396
|
// if (ctx.hx?.triggerafterswap) this.#handleTriggerHeader(ctx.hx.triggerafterswap, ctx.sourceElement);
|
|
1313
1397
|
}
|
|
1314
1398
|
|
|
1315
|
-
#processMainSwap(ctx, fragment, partialTasks
|
|
1399
|
+
#processMainSwap(ctx, fragment, partialTasks) {
|
|
1316
1400
|
// Create main task if needed
|
|
1317
1401
|
let swapSpec = this.#parseSwapSpec(ctx.swap || this.config.defaultSwap);
|
|
1318
1402
|
// skip creating main swap if extracting partials resulted in empty response except for delete style
|
|
1319
1403
|
if (swapSpec.style === 'delete' || /\S/.test(fragment.innerHTML || '') || !partialTasks.length) {
|
|
1320
|
-
let resultFragment = document.createDocumentFragment();
|
|
1321
1404
|
if (ctx.select) {
|
|
1322
|
-
let selected = fragment.
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
} else {
|
|
1331
|
-
resultFragment.append(...fragment.childNodes);
|
|
1405
|
+
let selected = fragment.querySelectorAll(ctx.select);
|
|
1406
|
+
fragment = document.createDocumentFragment();
|
|
1407
|
+
fragment.append(...selected);
|
|
1408
|
+
}
|
|
1409
|
+
if (this.#isBoosted(ctx.sourceElement)) {
|
|
1410
|
+
swapSpec.show ||= 'top';
|
|
1332
1411
|
}
|
|
1333
|
-
|
|
1334
1412
|
let mainSwap = {
|
|
1335
1413
|
type: 'main',
|
|
1336
|
-
fragment
|
|
1414
|
+
fragment,
|
|
1337
1415
|
target: swapSpec.target || ctx.target,
|
|
1338
1416
|
swapSpec,
|
|
1339
1417
|
sourceElement: ctx.sourceElement,
|
|
1340
|
-
transition: (ctx.transition !== false) && (swapSpec.transition !== false)
|
|
1341
|
-
title
|
|
1418
|
+
transition: (ctx.transition !== false) && (swapSpec.transition !== false)
|
|
1342
1419
|
};
|
|
1343
1420
|
return mainSwap;
|
|
1344
1421
|
}
|
|
@@ -1350,8 +1427,13 @@ var htmx = (() => {
|
|
|
1350
1427
|
target = document.querySelector(target);
|
|
1351
1428
|
}
|
|
1352
1429
|
if (!target) return;
|
|
1430
|
+
if (swapSpec.strip && fragment.firstElementChild) {
|
|
1431
|
+
task.unstripped = fragment;
|
|
1432
|
+
fragment = document.createDocumentFragment();
|
|
1433
|
+
fragment.append(...(task.fragment.firstElementChild.content || task.fragment.firstElementChild).childNodes);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1353
1436
|
let pantry = this.#handlePreservedElements(fragment);
|
|
1354
|
-
this.#processScripts(fragment);
|
|
1355
1437
|
let parentNode = target.parentNode;
|
|
1356
1438
|
let newContent = [...fragment.childNodes]
|
|
1357
1439
|
if (swapSpec.style === 'innerHTML') {
|
|
@@ -1391,22 +1473,26 @@ var htmx = (() => {
|
|
|
1391
1473
|
return;
|
|
1392
1474
|
} else if (swapSpec.style === 'none') {
|
|
1393
1475
|
return;
|
|
1394
|
-
} else if (!this.#triggerExtensions(target, 'htmx:handle:swap', task)) {
|
|
1395
|
-
return;
|
|
1396
1476
|
} else {
|
|
1477
|
+
task.target = target;
|
|
1478
|
+
task.fragment = fragment;
|
|
1479
|
+
if (!this.#triggerExtensions(target, 'htmx:handle:swap', task)) return;
|
|
1397
1480
|
throw new Error(`Unknown swap style: ${swapSpec.style}`);
|
|
1398
1481
|
}
|
|
1399
1482
|
this.#restorePreservedElements(pantry);
|
|
1400
1483
|
for (const elt of newContent) {
|
|
1401
|
-
this.process(elt);
|
|
1484
|
+
this.process(elt);
|
|
1485
|
+
this.#handleAutoFocus(elt);
|
|
1402
1486
|
}
|
|
1403
|
-
|
|
1487
|
+
this.#handleScroll(task);
|
|
1404
1488
|
}
|
|
1405
1489
|
|
|
1406
1490
|
#trigger(on, eventName, detail = {}, bubbles = true) {
|
|
1407
1491
|
if (this.config.logAll) {
|
|
1408
1492
|
console.log(eventName, detail, on)
|
|
1409
1493
|
}
|
|
1494
|
+
on = this.#normalizeElement(on)
|
|
1495
|
+
this.#triggerExtensions(on, this.#maybeAdjustMetaCharacter(eventName), detail);
|
|
1410
1496
|
return this.trigger(on, eventName, detail, bubbles)
|
|
1411
1497
|
}
|
|
1412
1498
|
|
|
@@ -1449,8 +1535,19 @@ var htmx = (() => {
|
|
|
1449
1535
|
})
|
|
1450
1536
|
}
|
|
1451
1537
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1538
|
+
onLoad(callback) {
|
|
1539
|
+
this.on("htmx:after:init", (evt) => {
|
|
1540
|
+
callback(evt.target)
|
|
1541
|
+
})
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
takeClass(element, className, container = element.parentElement) {
|
|
1545
|
+
for (let elt of this.findAll(this.#normalizeElement(container), "." + className)) {
|
|
1546
|
+
elt.classList.remove(className);
|
|
1547
|
+
}
|
|
1548
|
+
element.classList.add(className);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1454
1551
|
on(eventOrElt, eventOrCallback, callback) {
|
|
1455
1552
|
let event;
|
|
1456
1553
|
let elt = document;
|
|
@@ -1482,7 +1579,6 @@ var htmx = (() => {
|
|
|
1482
1579
|
|
|
1483
1580
|
trigger(on, eventName, detail = {}, bubbles = true) {
|
|
1484
1581
|
on = this.#normalizeElement(on)
|
|
1485
|
-
this.#triggerExtensions(on, eventName, detail);
|
|
1486
1582
|
let evt = new CustomEvent(eventName, {
|
|
1487
1583
|
detail,
|
|
1488
1584
|
cancelable: true,
|
|
@@ -1503,14 +1599,18 @@ var htmx = (() => {
|
|
|
1503
1599
|
|
|
1504
1600
|
let sourceElt = typeof context.source === 'string' ?
|
|
1505
1601
|
document.querySelector(context.source) : context.source;
|
|
1602
|
+
|
|
1603
|
+
// TODO we have a contradiction here: the tests say that we should default to the source element
|
|
1604
|
+
// but the logic here targets the source element
|
|
1506
1605
|
let targetElt = context.target ?
|
|
1507
1606
|
this.#resolveTarget(sourceElt || document.body, context.target) : sourceElt;
|
|
1508
1607
|
|
|
1509
|
-
if (
|
|
1510
|
-
return Promise.reject(new Error('
|
|
1608
|
+
if (!targetElt) {
|
|
1609
|
+
return Promise.reject(new Error('Target not found'));
|
|
1511
1610
|
}
|
|
1512
1611
|
|
|
1513
|
-
|
|
1612
|
+
// TODO is this logic correct?
|
|
1613
|
+
sourceElt ||= targetElt || document.body;
|
|
1514
1614
|
|
|
1515
1615
|
let ctx = this.#createRequestContext(sourceElt, context.event || {});
|
|
1516
1616
|
Object.assign(ctx, context, {target: targetElt});
|
|
@@ -1526,11 +1626,13 @@ var htmx = (() => {
|
|
|
1526
1626
|
|
|
1527
1627
|
#initHistoryHandling() {
|
|
1528
1628
|
if (!this.config.history) return;
|
|
1529
|
-
|
|
1629
|
+
if (!history.state) {
|
|
1630
|
+
history.replaceState({htmx: true}, '', location.pathname + location.search);
|
|
1631
|
+
}
|
|
1530
1632
|
window.addEventListener('popstate', (event) => {
|
|
1531
1633
|
if (event.state && event.state.htmx) {
|
|
1532
1634
|
this.#restoreHistory();
|
|
1533
|
-
}
|
|
1635
|
+
}
|
|
1534
1636
|
});
|
|
1535
1637
|
}
|
|
1536
1638
|
|
|
@@ -1554,10 +1656,11 @@ var htmx = (() => {
|
|
|
1554
1656
|
} else {
|
|
1555
1657
|
this.ajax('GET', path, {
|
|
1556
1658
|
target: 'body',
|
|
1557
|
-
swap: 'outerHTML',
|
|
1558
1659
|
request: {headers: {'HX-History-Restore-Request': 'true'}}
|
|
1559
1660
|
});
|
|
1560
1661
|
}
|
|
1662
|
+
} else if (elt.tagName === "FORM") {
|
|
1663
|
+
return elt.method !== 'dialog' && this.#isSameOrigin(elt.action);
|
|
1561
1664
|
}
|
|
1562
1665
|
}
|
|
1563
1666
|
|
|
@@ -1597,8 +1700,9 @@ var htmx = (() => {
|
|
|
1597
1700
|
|
|
1598
1701
|
#handleHxOnAttributes(node) {
|
|
1599
1702
|
for (let attr of node.getAttributeNames()) {
|
|
1600
|
-
|
|
1601
|
-
|
|
1703
|
+
var searchString = this.#maybeAdjustMetaCharacter(this.#prefix("hx-on:"));
|
|
1704
|
+
if (attr.startsWith(searchString)) {
|
|
1705
|
+
let evtName = attr.substring(searchString.length)
|
|
1602
1706
|
let code = node.getAttribute(attr);
|
|
1603
1707
|
node.addEventListener(evtName, async (evt) => {
|
|
1604
1708
|
try {
|
|
@@ -2060,16 +2164,18 @@ var htmx = (() => {
|
|
|
2060
2164
|
|
|
2061
2165
|
#handleStatusCodes(ctx) {
|
|
2062
2166
|
let status = ctx.response.raw.status;
|
|
2063
|
-
|
|
2064
|
-
ctx.swap = "none";
|
|
2065
|
-
}
|
|
2167
|
+
let noSwapStrings = this.config.noSwap.map(x => x + "");
|
|
2066
2168
|
let str = status + ""
|
|
2067
2169
|
for (let pattern of [str, str.slice(0, 2) + 'x', str[0] + 'xx']) {
|
|
2068
2170
|
let swap = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
|
|
2069
|
-
if (
|
|
2070
|
-
ctx.swap =
|
|
2171
|
+
if (noSwapStrings.includes(pattern)) {
|
|
2172
|
+
ctx.swap = "none";
|
|
2071
2173
|
return
|
|
2072
2174
|
}
|
|
2175
|
+
if (swap) {
|
|
2176
|
+
ctx.swap = swap;
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2073
2179
|
}
|
|
2074
2180
|
}
|
|
2075
2181
|
|
|
@@ -2083,7 +2189,7 @@ var htmx = (() => {
|
|
|
2083
2189
|
});
|
|
2084
2190
|
}
|
|
2085
2191
|
|
|
2086
|
-
async
|
|
2192
|
+
async #processTransitionQueue() {
|
|
2087
2193
|
if (this.#transitionQueue.length === 0 || this.#processingTransition) {
|
|
2088
2194
|
return;
|
|
2089
2195
|
}
|
|
@@ -2093,9 +2199,8 @@ var htmx = (() => {
|
|
|
2093
2199
|
|
|
2094
2200
|
try {
|
|
2095
2201
|
if (document.startViewTransition) {
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
await finished;
|
|
2202
|
+
this.#trigger(document, "htmx:before:viewTransition", {task})
|
|
2203
|
+
await document.startViewTransition(task).finished;
|
|
2099
2204
|
this.#trigger(document, "htmx:after:viewTransition", {task})
|
|
2100
2205
|
} else {
|
|
2101
2206
|
task();
|
|
@@ -2133,8 +2238,17 @@ var htmx = (() => {
|
|
|
2133
2238
|
return cssOrElement
|
|
2134
2239
|
}
|
|
2135
2240
|
}
|
|
2241
|
+
|
|
2242
|
+
#maybeAdjustMetaCharacter(string) {
|
|
2243
|
+
if (this.config.metaCharacter) {
|
|
2244
|
+
return string.replace(/:/g, this.config.metaCharacter);
|
|
2245
|
+
} else {
|
|
2246
|
+
return string;
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2136
2249
|
}
|
|
2137
2250
|
|
|
2138
2251
|
return new Htmx()
|
|
2139
2252
|
})()
|
|
2253
|
+
|
|
2140
2254
|
export default htmx
|