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/dist/htmx.esm.js CHANGED
@@ -59,7 +59,8 @@ var htmx = (() => {
59
59
  class Htmx {
60
60
 
61
61
  #extMethods = new Map();
62
- #approvedExt = new Set();
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 = new Set(this.config.extensions.split(',').map(s => s.trim()).filter(Boolean));
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.delete(name)) return false;
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) || elt.hasAttribute(inheritName)) {
184
- return elt.getAttribute(name) || elt.getAttribute(inheritName);
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 inheritedValue = this.#attributeValue(parent, name);
192
- return inheritedValue ? inheritedValue + "," + appendValue : appendValue;
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
- return this.#attributeValue(parent, name);
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") || "get";
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 verbAttribute = this.#attributeValue(elt, "hx-" + verb);
271
- if (verbAttribute) {
272
- action = verbAttribute;
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 ctx[actualKey] === 'object') {
346
- Object.assign(ctx[actualKey], configOverrides[key]);
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
- if (elt.hasAttribute(this.#prefix("hx-target"))) {
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._htmx?.boosted;
409
+ return elt?._htmx?.boosted;
394
410
  }
395
411
 
396
- async __handleTriggerEvent(ctx) {
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 __createRequestContext?
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 __issueRequest(ctx) {
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(ctx.elt, {}, js, true)) {
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 (ctx.fetchOverride || ctx.fetch(ctx.request.action, ctx.request));
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.#handleHxHeadersAndMaybeReturnEarly(ctx)){
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, response) {
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
- #handleHxHeadersAndMaybeReturnEarly(ctx) {
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 __handleSSE(ctx, elt, response) {
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* __parseSSE(res) {
673
- let r = res.body.getReader(), d = new TextDecoder(), b = '', m = {data: '', event: '', id: '', retry: null},
674
- ls, i, n, f, v;
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 (1) {
677
- let {done, value} = await r.read();
698
+ while (true) {
699
+ let {done, value} = await reader.read();
678
700
  if (done) break;
679
- for (let l of (b += d.decode(value, {stream: 1}), ls = b.split('\n'), b = ls.pop() || '', ls))
680
- !l || l === '\r' ? m.data && (yield m, m = {data: '', event: '', id: '', retry: null}) :
681
- (i = l.indexOf(':')) > 0 && (f = l.slice(0, i), v = l.slice(i + 1).trimStart(),
682
- f === 'data' ? m.data += (m.data ? '\n' : '') + v :
683
- f === 'event' ? m.event = v :
684
- f === 'id' ? m.id = v :
685
- f === 'retry' && (n = parseInt(v, 10), !isNaN(n)) ? m.retry = n : 0);
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
- r.releaseLock();
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 __executeJavaScriptAsync(thisArg, obj, code, expression = true) {
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
- if (this.#shouldInitialize(elt)) {
1007
- elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt), requests: [], boosted: true}
1008
- elt.setAttribute('data-htmx-powered', 'true');
1009
- if (elt.matches('a') && !elt.hasAttribute("target")) {
1010
- elt.addEventListener('click', (click) => {
1011
- elt._htmx.eventHandler(click)
1012
- })
1013
- } else {
1014
- elt.addEventListener('submit', (submit) => {
1015
- elt._htmx.eventHandler(submit)
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
- // TODO - store any head tag content on the fragment for head extension
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: doc.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
- let colonIdx = oobValue.indexOf(':');
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
- if (swapSpec.target) target = swapSpec.target;
1116
-
1117
- let oobElementClone = elt.cloneNode(true);
1118
- let fragment;
1119
- if (swapSpec.strip === undefined && swapSpec.style !== 'outerHTML') {
1120
- swapSpec.strip = true;
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, ...rest] = spec.split(':');
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
- #handleScroll(target, scroll) {
1226
- if (scroll === 'top') target.scrollTop = 0;
1227
- else if (scroll === 'bottom') target.scrollTop = target.scrollHeight;
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.originalAction?.split('#')[1];
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, title);
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?.title) document.title = mainSwap.title;
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, title) {
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.querySelector(ctx.select);
1323
- if (selected) {
1324
- if (swapSpec.strip === false) {
1325
- resultFragment.append(selected);
1326
- } else {
1327
- resultFragment.append(...selected.childNodes);
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: resultFragment,
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); // maybe only if isConnected?
1484
+ this.process(elt);
1485
+ this.#handleAutoFocus(elt);
1402
1486
  }
1403
- if (swapSpec.scroll) this.#handleScroll(target, swapSpec.scroll);
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
- // on(elt, evt, callback)
1453
- // on(evt, callback)
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 ((context.target && !targetElt) || (context.source && !sourceElt)) {
1510
- return Promise.reject(new Error('Element not found'));
1608
+ if (!targetElt) {
1609
+ return Promise.reject(new Error('Target not found'));
1511
1610
  }
1512
1611
 
1513
- sourceElt = sourceElt || targetElt || document.body;
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
- // Handle browser back/forward navigation
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
- if (attr.startsWith(this.#prefix("hx-on:"))) {
1601
- let evtName = attr.substring(this.#prefix("hx-on:").length)
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
- if (this.config.noSwap.includes(status)) {
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 (swap) {
2070
- ctx.swap = 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 __processTransitionQueue() {
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
- let finished = document.startViewTransition(task).finished;
2097
- this.#trigger(document, "htmx:before:viewTransition", {task, finished})
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