htmx.org 4.0.0-alpha4 → 4.0.0-alpha6

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.js CHANGED
@@ -6,17 +6,17 @@ var htmx = (() => {
6
6
  #q = []
7
7
 
8
8
  issue(ctx, queueStrategy) {
9
+ ctx.queueStrategy = queueStrategy
9
10
  if (!this.#c) {
10
11
  this.#c = ctx
11
12
  return true
12
13
  } else {
13
- // Update ctx.status properly for replaced request contexts
14
- if (queueStrategy === "replace") {
14
+ // Replace strategy OR current is abortable: abort current and issue new
15
+ if (queueStrategy === "replace" || (queueStrategy !== "abort" && this.#c.queueStrategy === "abort")) {
15
16
  this.#q.map(value => value.status = "dropped");
16
17
  this.#q = []
17
- if (this.#c) {
18
- this.#c.abort();
19
- }
18
+ this.#c.request.abort();
19
+ this.#c = ctx
20
20
  return true
21
21
  } else if (queueStrategy === "queue all") {
22
22
  this.#q.push(ctx)
@@ -28,7 +28,7 @@ var htmx = (() => {
28
28
  this.#q.map(value => value.status = "dropped");
29
29
  this.#q = [ctx]
30
30
  ctx.status = "queued";
31
- } else if (this.#q.length === 0) {
31
+ } else if (this.#q.length === 0 && queueStrategy !== "abort") {
32
32
  // default queue first
33
33
  this.#q.push(ctx)
34
34
  ctx.status = "queued";
@@ -80,7 +80,9 @@ var htmx = (() => {
80
80
  determineMethodAndAction: this.#determineMethodAndAction.bind(this),
81
81
  createRequestContext: this.#createRequestContext.bind(this),
82
82
  collectFormData: this.#collectFormData.bind(this),
83
- handleHxVals: this.#handleHxVals.bind(this)
83
+ handleHxVals: this.#handleHxVals.bind(this),
84
+ insertContent: this.#insertContent.bind(this),
85
+ morph: this.#morph.bind(this)
84
86
  };
85
87
  document.addEventListener("DOMContentLoaded", () => {
86
88
  this.#initHistoryHandling();
@@ -90,12 +92,11 @@ var htmx = (() => {
90
92
 
91
93
  #initHtmxConfig() {
92
94
  this.config = {
93
- version: '4.0.0-alpha3',
95
+ version: '4.0.0-alpha6',
94
96
  logAll: false,
95
97
  prefix: "",
96
- transitions: true,
98
+ transitions: false,
97
99
  history: true,
98
- historyReload: false,
99
100
  mode: 'same-origin',
100
101
  defaultSwap: "innerHTML",
101
102
  indicatorClass: "htmx-indicator",
@@ -112,22 +113,13 @@ var htmx = (() => {
112
113
  pauseInBackground: false
113
114
  },
114
115
  morphIgnore: ["data-htmx-powered"],
116
+ morphScanLimit: 10,
115
117
  noSwap: [204, 304],
116
118
  implicitInheritance: false
117
119
  }
118
- let metaConfig = document.querySelector('meta[name="htmx:config"]');
120
+ let metaConfig = document.querySelector('meta[name="htmx-config"]');
119
121
  if (metaConfig) {
120
- let content = metaConfig.content;
121
- let overrides = this.#parseConfig(content);
122
- // Deep merge nested config objects
123
- for (let key in overrides) {
124
- let val = overrides[key];
125
- if (val && typeof val === 'object' && !Array.isArray(val) && this.config[key]) {
126
- Object.assign(this.config[key], val);
127
- } else {
128
- this.config[key] = val;
129
- }
130
- }
122
+ this.#mergeConfig(metaConfig.content, this.config);
131
123
  }
132
124
  this.#approvedExt = this.config.extensions;
133
125
  }
@@ -181,48 +173,60 @@ var htmx = (() => {
181
173
  style === 'append' ? 'beforeend' : style;
182
174
  }
183
175
 
184
- #attributeValue(elt, name, defaultVal, returnElt) {
176
+ #findThisElements(elt, attrName) {
177
+ let result = [];
178
+ this.#attributeValue(elt, attrName, undefined, (val, elt) => {
179
+ if (val?.split(/\s*,\s*/).includes('this')) result.push(elt);
180
+ });
181
+ return result;
182
+ }
183
+
184
+ #attributeValue(elt, name, defaultVal, eltCollector) {
185
185
  name = this.#prefix(name);
186
186
  let appendName = name + this.#maybeAdjustMetaCharacter(":append");
187
187
  let inheritName = name + (this.config.implicitInheritance ? "" : this.#maybeAdjustMetaCharacter(":inherited"));
188
188
  let inheritAppendName = name + this.#maybeAdjustMetaCharacter(":inherited:append");
189
189
 
190
190
  if (elt.hasAttribute(name)) {
191
- return returnElt ? elt : elt.getAttribute(name);
191
+ let val = elt.getAttribute(name);
192
+ return eltCollector ? eltCollector(val, elt) : val;
192
193
  }
193
194
 
194
195
  if (elt.hasAttribute(inheritName)) {
195
- return returnElt ? elt : elt.getAttribute(inheritName);
196
+ let val = elt.getAttribute(inheritName);
197
+ return eltCollector ? eltCollector(val, elt) : val;
196
198
  }
197
199
 
198
200
  if (elt.hasAttribute(appendName) || elt.hasAttribute(inheritAppendName)) {
199
201
  let appendValue = elt.getAttribute(appendName) || elt.getAttribute(inheritAppendName);
200
202
  let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
203
+ if (eltCollector) {
204
+ eltCollector(appendValue, elt);
205
+ }
201
206
  if (parent) {
202
- let inherited = this.#attributeValue(parent, name, undefined, returnElt);
203
- return returnElt ? inherited : (inherited ? inherited + "," + appendValue : appendValue);
204
- } else {
205
- return returnElt ? elt : appendValue;
207
+ let inherited = this.#attributeValue(parent, name, undefined, eltCollector);
208
+ return inherited ? (inherited + "," + appendValue).replace(/[{}]/g, '') : appendValue;
206
209
  }
210
+ return appendValue;
207
211
  }
208
212
 
209
213
  let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
210
214
  if (parent) {
211
- let val = this.#attributeValue(parent, name, undefined, returnElt);
212
- if (!returnElt && val && this.config.implicitInheritance) {
213
- this.#triggerExtensions(elt, "htmx:after:implicitInheritance", {elt, parent})
215
+ let val = this.#attributeValue(parent, name, undefined, eltCollector);
216
+ if (!eltCollector && val && this.config.implicitInheritance) {
217
+ this.#triggerExtensions(elt, "htmx:after:implicitInheritance", {elt, name, parent})
214
218
  }
215
219
  return val;
216
220
  }
217
- return returnElt ? elt : defaultVal;
221
+ return defaultVal;
218
222
  }
219
223
 
220
224
  #parseConfig(configString) {
221
225
  if (configString[0] === '{') return JSON.parse(configString);
222
- let configPattern = /([^\s,]+?)(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
226
+ let configPattern = /(?:"([^"]+)"|([^\s,:]+))(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
223
227
  return [...configString.matchAll(configPattern)].reduce((result, match) => {
224
- let keyPath = match[1].split('.');
225
- let value = (match[2] ?? match[3] ?? match[4] ?? match[5] ?? 'true').trim();
228
+ let keyPath = (match[1] ?? match[2]).split('.');
229
+ let value = (match[3] ?? match[4] ?? match[5] ?? match[6] ?? 'true').trim();
226
230
  if (value === 'true') value = true;
227
231
  else if (value === 'false') value = false;
228
232
  else if (/^\d+$/.test(value)) value = parseInt(value);
@@ -231,6 +235,19 @@ var htmx = (() => {
231
235
  }, {});
232
236
  }
233
237
 
238
+ #mergeConfig(configString, target) {
239
+ let parsed = this.#parseConfig(configString);
240
+ for (let key in parsed) {
241
+ let val = parsed[key];
242
+ if (val && typeof val === 'object' && !Array.isArray(val) && target[key]) {
243
+ Object.assign(target[key], val);
244
+ } else {
245
+ target[key] = val;
246
+ }
247
+ }
248
+ return target;
249
+ }
250
+
234
251
  #parseTriggerSpecs(spec) {
235
252
  return spec.split(',').map(s => {
236
253
  let m = s.match(/^\s*(\S+\[[^\]]*\]|\S+)\s*(.*?)\s*$/);
@@ -305,44 +322,36 @@ var htmx = (() => {
305
322
  status: "created",
306
323
  select: this.#attributeValue(sourceElement, "hx-select"),
307
324
  selectOOB: this.#attributeValue(sourceElement, "hx-select-oob"),
308
- target: this.#resolveTarget(sourceElement, this.#attributeValue(sourceElement, "hx-target")),
309
- swap: this.#attributeValue(sourceElement, "hx-swap", this.config.defaultSwap),
325
+ target: this.#attributeValue(sourceElement, "hx-target"),
326
+ swap: this.#attributeValue(sourceElement, "hx-swap") ?? this.config.defaultSwap,
310
327
  push: this.#attributeValue(sourceElement, "hx-push-url"),
311
328
  replace: this.#attributeValue(sourceElement, "hx-replace-url"),
312
329
  transition: this.config.transitions,
313
330
  confirm: this.#attributeValue(sourceElement, "hx-confirm"),
314
331
  request: {
315
- validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') ? "true" : "false"),
332
+ validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') && !sourceElement.noValidate && !sourceEvent.submitter?.formNoValidate ? "true" : "false"),
316
333
  action: fullAction,
317
334
  anchor,
318
335
  method,
319
- headers: this.#determineHeaders(sourceElement),
336
+ headers: this.#createCoreHeaders(sourceElement),
320
337
  abort: ac.abort.bind(ac),
321
338
  credentials: "same-origin",
322
339
  signal: ac.signal,
323
340
  mode: this.config.mode
324
341
  }
325
342
  };
343
+ // Apply boost config overrides
344
+ if (sourceElement._htmx?.boosted) {
345
+ this.#mergeConfig(sourceElement._htmx.boosted, ctx);
346
+ }
347
+ ctx.target = this.#resolveTarget(sourceElement, ctx.target);
326
348
 
327
349
  // Apply hx-config overrides
328
350
  let configAttr = this.#attributeValue(sourceElement, "hx-config");
329
351
  if (configAttr) {
330
- let configOverrides = this.#parseConfig(configAttr);
331
- let req = ctx.request;
332
- for (let key in configOverrides) {
333
- if (key.startsWith('+')) {
334
- let actualKey = key.substring(1);
335
- if (req[actualKey] && typeof req[actualKey] === 'object') {
336
- Object.assign(req[actualKey], configOverrides[key]);
337
- } else {
338
- req[actualKey] = configOverrides[key];
339
- }
340
- } else {
341
- req[key] = configOverrides[key];
342
- }
343
- }
344
- if (req.etag) {
345
- (sourceElement._htmx ||= {}).etag ||= req.etag
352
+ this.#mergeConfig(configAttr, ctx.request);
353
+ if (ctx.request.etag) {
354
+ (sourceElement._htmx ||= {}).etag ||= ctx.request.etag
346
355
  }
347
356
  }
348
357
  if (sourceElement._htmx?.etag) {
@@ -351,30 +360,34 @@ var htmx = (() => {
351
360
  return ctx;
352
361
  }
353
362
 
354
- #determineHeaders(elt) {
363
+ #buildIdentifier(elt) {
364
+ return `${elt.tagName.toLowerCase()}${elt.id ? '#' + elt.id : ''}`;
365
+ }
366
+
367
+ #createCoreHeaders(elt) {
355
368
  let headers = {
356
369
  "HX-Request": "true",
357
- "HX-Source": elt.id || elt.name,
370
+ "HX-Source": this.#buildIdentifier(elt),
358
371
  "HX-Current-URL": location.href,
359
372
  "Accept": "text/html, text/event-stream"
360
373
  };
361
374
  if (this.#isBoosted(elt)) {
362
375
  headers["HX-Boosted"] = "true"
363
376
  }
364
- let headersAttribute = this.#attributeValue(elt, "hx-headers");
365
- if (headersAttribute) {
366
- Object.assign(headers, this.#parseConfig(headersAttribute));
367
- }
368
377
  return headers;
369
378
  }
370
379
 
380
+ #handleHxHeaders(elt, headers) {
381
+ return this.#getAttributeObject(elt, "hx-headers", obj => {
382
+ for (let key in obj) headers[key] = String(obj[key]);
383
+ });
384
+ }
385
+
371
386
  #resolveTarget(elt, selector) {
372
387
  if (selector instanceof Element) {
373
388
  return selector;
374
- } else if (selector === 'this') {
375
- return this.#attributeValue(elt, "hx-target", undefined, true);
376
389
  } else if (selector != null) {
377
- return this.find(elt, selector);
390
+ return this.#findExt(elt, selector, "hx-target");
378
391
  } else if (this.#isBoosted(elt)) {
379
392
  return document.body
380
393
  } else {
@@ -397,7 +410,8 @@ var htmx = (() => {
397
410
 
398
411
  // Build request body
399
412
  let form = elt.form || elt.closest("form")
400
- let body = this.#collectFormData(elt, form, evt.submitter)
413
+ let body = this.#collectFormData(elt, form, evt.submitter, ctx.request.validate)
414
+ if (!body) return // Validation failed
401
415
  let valsResult = this.#handleHxVals(elt, body)
402
416
  if (valsResult) await valsResult // Only await if it returned a promise
403
417
  if (ctx.values) {
@@ -407,6 +421,16 @@ var htmx = (() => {
407
421
  }
408
422
  }
409
423
 
424
+ // Handle dynamic headers
425
+ let headersResult = this.#handleHxHeaders(elt, ctx.request.headers)
426
+ if (headersResult) await headersResult // Only await if it returned a promise
427
+
428
+ // Add HX-Request-Type and HX-Target headers
429
+ ctx.request.headers["HX-Request-Type"] = (ctx.target === document.body || ctx.select) ? "full" : "partial";
430
+ if (ctx.target) {
431
+ ctx.request.headers["HX-Target"] = this.#buildIdentifier(ctx.target);
432
+ }
433
+
410
434
  // Setup event-dependent request details
411
435
  Object.assign(ctx.request, {
412
436
  form,
@@ -416,7 +440,6 @@ var htmx = (() => {
416
440
 
417
441
  if (!this.#trigger(elt, "htmx:config:request", {ctx: ctx})) return
418
442
  if (!this.#verbs.includes(ctx.request.method.toLowerCase())) return
419
- if (ctx.request.validate && ctx.request.form && !ctx.request.form.reportValidity()) return
420
443
 
421
444
  let javascriptContent = this.#extractJavascriptContent(ctx.request.action);
422
445
  if (javascriptContent != null) {
@@ -425,15 +448,20 @@ var htmx = (() => {
425
448
  return
426
449
  } else if (/GET|DELETE/.test(ctx.request.method)) {
427
450
  let url = new URL(ctx.request.action, document.baseURI);
428
-
451
+
429
452
  for (let key of ctx.request.body.keys()) {
430
453
  url.searchParams.delete(key);
431
454
  }
432
455
  for (let [key, value] of ctx.request.body) {
433
456
  url.searchParams.append(key, value);
434
457
  }
435
-
436
- ctx.request.action = url.pathname + url.search;
458
+
459
+ // Keep relative if same origin, otherwise use full URL
460
+ if (url.origin === location.origin) {
461
+ ctx.request.action = url.pathname + url.search;
462
+ } else {
463
+ ctx.request.action = url.href;
464
+ }
437
465
  ctx.request.body = null;
438
466
  } else if (this.#attributeValue(elt, "hx-encoding") !== "multipart/form-data") {
439
467
  ctx.request.body = new URLSearchParams(ctx.request.body);
@@ -452,10 +480,8 @@ var htmx = (() => {
452
480
  ctx.status = "issuing"
453
481
  this.#initTimeout(ctx);
454
482
 
455
- let indicatorsSelector = this.#attributeValue(elt, "hx-indicator");
456
- let indicators = this.#showIndicators(elt, indicatorsSelector);
457
- let disableSelector = this.#attributeValue(elt, "hx-disable");
458
- let disableElements = this.#disableElements(elt, disableSelector);
483
+ let indicators = this.#showIndicators(elt);
484
+ let disableElements = this.#disableElements(elt);
459
485
 
460
486
  try {
461
487
  // Handle confirmation
@@ -739,7 +765,7 @@ var htmx = (() => {
739
765
  if (syncValue && syncValue.includes(":")) {
740
766
  let strings = syncValue.split(":");
741
767
  let selector = strings[0];
742
- syncElt = this.#findExt(selector);
768
+ syncElt = this.#findExt(elt, selector, "hx-sync");
743
769
  }
744
770
  return syncElt._htmxRequestQueue ||= new ReqQ()
745
771
  }
@@ -1001,8 +1027,9 @@ var htmx = (() => {
1001
1027
  }
1002
1028
 
1003
1029
  #maybeBoost(elt) {
1004
- if (this.#attributeValue(elt, "hx-boost") === "true" && this.#shouldBoost(elt)) {
1005
- elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt), requests: [], boosted: true}
1030
+ let boostValue = this.#attributeValue(elt, "hx-boost");
1031
+ if (boostValue && boostValue !== "false" && this.#shouldBoost(elt)) {
1032
+ elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt), requests: [], boosted: boostValue}
1006
1033
  elt.setAttribute('data-htmx-powered', 'true');
1007
1034
  if (elt.matches('a') && !elt.hasAttribute("target")) {
1008
1035
  elt.addEventListener('click', (click) => {
@@ -1057,8 +1084,10 @@ var htmx = (() => {
1057
1084
  }
1058
1085
  this.#trigger(elt, "htmx:after:cleanup")
1059
1086
  }
1060
- for (let child of elt.querySelectorAll('[data-htmx-powered]')) {
1061
- this.#cleanup(child);
1087
+ if (elt.firstChild) {
1088
+ for (let child of elt.querySelectorAll('[data-htmx-powered]')) {
1089
+ this.#cleanup(child);
1090
+ }
1062
1091
  }
1063
1092
  }
1064
1093
 
@@ -1069,10 +1098,12 @@ var htmx = (() => {
1069
1098
  let newPreservedElts = fragment.querySelectorAll?.(`[${this.#prefix('hx-preserve')}]`) || [];
1070
1099
  for (let preservedElt of newPreservedElts) {
1071
1100
  let currentElt = document.getElementById(preservedElt.id);
1072
- if (pantry.moveBefore) {
1073
- pantry.moveBefore(currentElt, null);
1074
- } else {
1075
- pantry.appendChild(currentElt);
1101
+ if (currentElt) {
1102
+ if (pantry.moveBefore) {
1103
+ pantry.moveBefore(currentElt, null);
1104
+ } else {
1105
+ pantry.appendChild(currentElt);
1106
+ }
1076
1107
  }
1077
1108
  }
1078
1109
  return pantry
@@ -1081,13 +1112,15 @@ var htmx = (() => {
1081
1112
  #restorePreservedElements(pantry) {
1082
1113
  for (let preservedElt of pantry.children) {
1083
1114
  let newElt = document.getElementById(preservedElt.id);
1084
- if (newElt.parentNode.moveBefore) {
1085
- newElt.parentNode.moveBefore(preservedElt, newElt);
1086
- } else {
1087
- newElt.replaceWith(preservedElt);
1115
+ if (newElt) {
1116
+ if (newElt.parentNode.moveBefore) {
1117
+ newElt.parentNode.moveBefore(preservedElt, newElt);
1118
+ } else {
1119
+ newElt.replaceWith(preservedElt);
1120
+ }
1121
+ this.#cleanup(newElt)
1122
+ newElt.remove()
1088
1123
  }
1089
- this.#cleanup(newElt)
1090
- newElt.remove()
1091
1124
  }
1092
1125
  pantry.remove();
1093
1126
  }
@@ -1187,13 +1220,11 @@ var htmx = (() => {
1187
1220
  let type = templateElt.getAttribute('type');
1188
1221
 
1189
1222
  if (type === 'partial') {
1190
- let swapSpec = this.#parseSwapSpec(templateElt.getAttribute(this.#prefix('hx-swap')) || this.config.defaultSwap);
1191
-
1192
1223
  tasks.push({
1193
1224
  type: 'partial',
1194
1225
  fragment: templateElt.content.cloneNode(true),
1195
1226
  target: templateElt.getAttribute(this.#prefix('hx-target')),
1196
- swapSpec,
1227
+ swapSpec: this.#parseSwapSpec(templateElt.getAttribute(this.#prefix('hx-swap')) || this.config.defaultSwap),
1197
1228
  sourceElement: ctx.sourceElement
1198
1229
  });
1199
1230
  } else {
@@ -1210,18 +1241,18 @@ var htmx = (() => {
1210
1241
  autofocus?.focus?.()
1211
1242
  }
1212
1243
 
1213
- #handleScroll(task) {
1214
- if (task.swapSpec.scroll) {
1215
- let target = task.swapSpec.scrollTarget ? this.#findExt(task.swapSpec.scrollTarget) : task.target;
1216
- if (task.swapSpec.scroll === 'top') {
1217
- target.scrollTop = 0;
1218
- } else if (task.swapSpec.scroll === 'bottom'){
1219
- target.scrollTop = target.scrollHeight;
1244
+ #handleScroll(swapSpec, target) {
1245
+ if (swapSpec.scroll) {
1246
+ let scrollTarget = swapSpec.scrollTarget ? this.#findExt(swapSpec.scrollTarget) : target;
1247
+ if (swapSpec.scroll === 'top') {
1248
+ scrollTarget.scrollTop = 0;
1249
+ } else if (swapSpec.scroll === 'bottom'){
1250
+ scrollTarget.scrollTop = scrollTarget.scrollHeight;
1220
1251
  }
1221
1252
  }
1222
- if (task.swapSpec.show) {
1223
- let target = task.swapSpec.showTarget ? this.#findExt(task.swapSpec.showTarget) : task.target;
1224
- target.scrollIntoView(task.swapSpec.show === 'top')
1253
+ if (swapSpec.show) {
1254
+ let showTarget = swapSpec.showTarget ? this.#findExt(swapSpec.showTarget) : target;
1255
+ showTarget.scrollIntoView(swapSpec.show === 'top')
1225
1256
  }
1226
1257
  }
1227
1258
 
@@ -1261,62 +1292,48 @@ var htmx = (() => {
1261
1292
  let partialTasks = this.#processPartials(fragment, ctx);
1262
1293
  tasks.push(...oobTasks, ...partialTasks);
1263
1294
 
1264
- // Process main swap
1295
+ // Process main swap first
1265
1296
  let mainSwap = this.#processMainSwap(ctx, fragment, partialTasks);
1266
1297
  if (mainSwap) {
1267
- tasks.push(mainSwap);
1298
+ tasks.unshift(mainSwap);
1268
1299
  }
1269
1300
 
1270
- // TODO - can we remove this and just let the function complete?
1271
- if (tasks.length === 0) return;
1272
-
1273
- // Separate transition/nonTransition tasks
1274
- let transitionTasks = tasks.filter(t => t.transition);
1275
- let nonTransitionTasks = tasks.filter(t => !t.transition);
1276
-
1277
1301
  if(!this.#trigger(document, "htmx:before:swap", {ctx, tasks})){
1278
1302
  return
1279
1303
  }
1280
1304
 
1281
- // insert non-transition tasks immediately or with delay
1282
- for (let task of nonTransitionTasks) {
1283
- if (task.swapSpec?.swap) {
1284
- setTimeout(() => this.#insertContent(task), this.parseInterval(task.swapSpec.swap));
1305
+ let swapPromises = [];
1306
+ let transitionTasks = [];
1307
+ for (let task of tasks) {
1308
+ if (task.swapSpec?.transition ?? mainSwap?.transition) {
1309
+ transitionTasks.push(task);
1285
1310
  } else {
1286
- this.#insertContent(task)
1311
+ swapPromises.push(this.#insertContent(task));
1287
1312
  }
1288
1313
  }
1289
1314
 
1290
- // insert transition tasks in the transition queue
1315
+ // submit all transition tasks in the transition queue w/no CSS transitions
1291
1316
  if (transitionTasks.length > 0) {
1292
- let tasksWrapper = ()=> {
1317
+ let tasksWrapper = async ()=> {
1293
1318
  for (let task of transitionTasks) {
1294
- this.#insertContent(task)
1319
+ await this.#insertContent(task, false)
1295
1320
  }
1296
1321
  }
1297
- await this.#submitTransitionTask(tasksWrapper);
1322
+ swapPromises.push(this.#submitTransitionTask(tasksWrapper));
1298
1323
  }
1299
1324
 
1325
+ await Promise.all(swapPromises);
1326
+
1300
1327
  this.#trigger(document, "htmx:after:swap", {ctx});
1301
1328
  if (ctx.title && !mainSwap?.swapSpec?.ignoreTitle) document.title = ctx.title;
1302
- await this.timeout(1);
1303
- // invoke restore tasks
1304
- for (let task of tasks) {
1305
- for (let restore of task.restoreTasks || []) {
1306
- restore()
1307
- }
1308
- }
1309
- this.#trigger(document, "htmx:after:restore", { ctx });
1310
1329
  this.#handleAnchorScroll(ctx);
1311
- // TODO this stuff should be an extension
1312
- // if (ctx.hx?.triggerafterswap) this.#handleTriggerHeader(ctx.hx.triggerafterswap, ctx.sourceElement);
1313
1330
  }
1314
1331
 
1315
1332
  #processMainSwap(ctx, fragment, partialTasks) {
1316
1333
  // Create main task if needed
1317
1334
  let swapSpec = this.#parseSwapSpec(ctx.swap || this.config.defaultSwap);
1318
1335
  // skip creating main swap if extracting partials resulted in empty response except for delete style
1319
- if (swapSpec.style === 'delete' || /\S/.test(fragment.innerHTML || '') || !partialTasks.length) {
1336
+ if (swapSpec.style === 'delete' || fragment.childElementCount > 0 || /\S/.test(fragment.textContent) || !partialTasks.length) {
1320
1337
  if (ctx.select) {
1321
1338
  let selected = fragment.querySelectorAll(ctx.select);
1322
1339
  fragment = document.createDocumentFragment();
@@ -1337,70 +1354,122 @@ var htmx = (() => {
1337
1354
  }
1338
1355
  }
1339
1356
 
1340
- #insertContent(task) {
1357
+ async #insertContent(task, cssTransition = true) {
1341
1358
  let {target, swapSpec, fragment} = task;
1342
1359
  if (typeof target === 'string') {
1343
1360
  target = document.querySelector(target);
1344
1361
  }
1345
1362
  if (!target) return;
1363
+ if (typeof swapSpec === 'string') {
1364
+ swapSpec = this.#parseSwapSpec(swapSpec);
1365
+ }
1366
+ if (swapSpec.style === 'none') return;
1346
1367
  if (swapSpec.strip && fragment.firstElementChild) {
1347
- task.unstripped = fragment;
1348
1368
  fragment = document.createDocumentFragment();
1349
1369
  fragment.append(...(task.fragment.firstElementChild.content || task.fragment.firstElementChild).childNodes);
1350
1370
  }
1351
1371
 
1372
+ target.classList.add("htmx-swapping")
1373
+ if (cssTransition && task.swapSpec?.swap) {
1374
+ await this.timeout(task.swapSpec?.swap)
1375
+ }
1376
+
1377
+ if (swapSpec.style === 'delete') {
1378
+ if (target.parentNode) {
1379
+ this.#cleanup(target);
1380
+ target.parentNode.removeChild(target);
1381
+ }
1382
+ return;
1383
+ }
1384
+
1385
+ if (swapSpec.style === 'textContent') {
1386
+ target.textContent = fragment.textContent;
1387
+ target.classList.remove("htmx-swapping")
1388
+ return;
1389
+ }
1390
+
1352
1391
  let pantry = this.#handlePreservedElements(fragment);
1353
1392
  let parentNode = target.parentNode;
1354
1393
  let newContent = [...fragment.childNodes]
1355
- if (swapSpec.style === 'innerHTML') {
1356
- this.#captureCSSTransitions(task, target);
1357
- for (const child of target.children) {
1358
- this.#cleanup(child)
1359
- }
1360
- target.replaceChildren(...fragment.childNodes);
1361
- } else if (swapSpec.style === 'outerHTML') {
1362
- if (parentNode) {
1363
- this.#captureCSSTransitions(task, parentNode);
1364
- this.#insertNodes(parentNode, target, fragment);
1365
- this.#cleanup(target)
1366
- parentNode.removeChild(target);
1367
- }
1368
- } else if (swapSpec.style === 'innerMorph') {
1369
- this.#morph(target, fragment, true);
1370
- } else if (swapSpec.style === 'outerMorph') {
1371
- this.#morph(target, fragment, false);
1372
- } else if (swapSpec.style === 'beforebegin') {
1373
- if (parentNode) {
1374
- this.#insertNodes(parentNode, target, fragment);
1375
- }
1376
- } else if (swapSpec.style === 'afterbegin') {
1377
- this.#insertNodes(target, target.firstChild, fragment);
1378
- } else if (swapSpec.style === 'beforeend') {
1379
- this.#insertNodes(target, null, fragment);
1380
- } else if (swapSpec.style === 'afterend') {
1381
- if (parentNode) {
1382
- this.#insertNodes(parentNode, target.nextSibling, fragment);
1383
- }
1384
- } else if (swapSpec.style === 'delete') {
1385
- if (parentNode) {
1386
- this.#cleanup(target)
1387
- parentNode.removeChild(target)
1394
+ let settleTasks = []
1395
+ try {
1396
+ if (swapSpec.style === 'innerHTML') {
1397
+ settleTasks = cssTransition ? this.#startCSSTransitions(fragment, target) : []
1398
+ for (const child of target.children) {
1399
+ this.#cleanup(child)
1400
+ }
1401
+ target.replaceChildren(...fragment.childNodes);
1402
+ } else if (swapSpec.style === 'outerHTML') {
1403
+ if (parentNode) {
1404
+ settleTasks = cssTransition ? this.#startCSSTransitions(fragment, target) : []
1405
+ this.#insertNodes(parentNode, target, fragment);
1406
+ this.#cleanup(target)
1407
+ parentNode.removeChild(target);
1408
+ }
1409
+ } else if (swapSpec.style === 'innerMorph') {
1410
+ this.#morph(target, fragment, true);
1411
+ newContent = [...target.childNodes];
1412
+ } else if (swapSpec.style === 'outerMorph') {
1413
+ this.#morph(target, fragment, false);
1414
+ newContent.push(target);
1415
+ } else if (swapSpec.style === 'beforebegin') {
1416
+ if (parentNode) {
1417
+ this.#insertNodes(parentNode, target, fragment);
1418
+ }
1419
+ } else if (swapSpec.style === 'afterbegin') {
1420
+ this.#insertNodes(target, target.firstChild, fragment);
1421
+ } else if (swapSpec.style === 'beforeend') {
1422
+ this.#insertNodes(target, null, fragment);
1423
+ } else if (swapSpec.style === 'afterend') {
1424
+ if (parentNode) {
1425
+ this.#insertNodes(parentNode, target.nextSibling, fragment);
1426
+ }
1427
+ } else {
1428
+ let methods = this.#extMethods.get('handle_swap')
1429
+ let handled = false;
1430
+ for (const method of methods) {
1431
+ let result = method(swapSpec.style, target, fragment, swapSpec);
1432
+ if (result) {
1433
+ handled = true;
1434
+ if (Array.isArray(result)) {
1435
+ newContent = result;
1436
+ }
1437
+ break;
1438
+ }
1439
+ }
1440
+ if (!handled) {
1441
+ throw new Error(`Unknown swap style: ${swapSpec.style}`);
1442
+ }
1388
1443
  }
1389
- return;
1390
- } else if (swapSpec.style === 'none') {
1391
- return;
1392
- } else {
1393
- task.target = target;
1394
- task.fragment = fragment;
1395
- if (!this.#triggerExtensions(target, 'htmx:handle:swap', task)) return;
1396
- throw new Error(`Unknown swap style: ${swapSpec.style}`);
1444
+ } finally {
1445
+ target.classList.remove("htmx-swapping")
1397
1446
  }
1398
1447
  this.#restorePreservedElements(pantry);
1448
+
1449
+ this.#trigger(target, "htmx:before:settle", {task, newContent, settleTasks})
1450
+
1399
1451
  for (const elt of newContent) {
1452
+ elt.classList?.add?.("htmx-added")
1453
+ }
1454
+
1455
+ if (cssTransition) {
1456
+ target.classList.add("htmx-settling")
1457
+ await this.timeout(swapSpec.settle ?? 1);
1458
+ // invoke settle tasks
1459
+ for (let settleTask of settleTasks) {
1460
+ settleTask()
1461
+ }
1462
+ target.classList.remove("htmx-settling")
1463
+ }
1464
+
1465
+ this.#trigger(target, "htmx:after:settle", {task, newContent, settleTasks})
1466
+
1467
+ for (const elt of newContent) {
1468
+ elt.classList?.remove?.("htmx-added")
1400
1469
  this.process(elt);
1401
1470
  this.#handleAutoFocus(elt);
1402
1471
  }
1403
- this.#handleScroll(task);
1472
+ this.#handleScroll(swapSpec, target);
1404
1473
  }
1405
1474
 
1406
1475
  #trigger(on, eventName, detail = {}, bubbles = true) {
@@ -1409,7 +1478,7 @@ var htmx = (() => {
1409
1478
  }
1410
1479
  on = this.#normalizeElement(on)
1411
1480
  this.#triggerExtensions(on, eventName, detail);
1412
- return this.trigger(on, eventName, detail, bubbles)
1481
+ return this.trigger(on, this.#maybeAdjustMetaCharacter(eventName), detail, bubbles)
1413
1482
  }
1414
1483
 
1415
1484
  #triggerExtensions(elt, eventName, detail = {}) {
@@ -1456,7 +1525,7 @@ var htmx = (() => {
1456
1525
  }
1457
1526
 
1458
1527
  takeClass(element, className, container = element.parentElement) {
1459
- for (let elt of this.findAll(this.#normalizeElement(container), "." + className)) {
1528
+ for (let elt of this.#findAllExt(this.#normalizeElement(container), "." + className)) {
1460
1529
  elt.classList.remove(className);
1461
1530
  }
1462
1531
  element.classList.add(className);
@@ -1567,7 +1636,7 @@ var htmx = (() => {
1567
1636
  #restoreHistory(path) {
1568
1637
  path = path || location.pathname + location.search;
1569
1638
  if (this.#trigger(document, "htmx:before:restore:history", {path, cacheMiss: true})) {
1570
- if (this.config.historyReload) {
1639
+ if (this.config.history === "reload") {
1571
1640
  location.reload();
1572
1641
  } else {
1573
1642
  this.ajax('GET', path, {
@@ -1595,7 +1664,9 @@ var htmx = (() => {
1595
1664
  if (!path || path === 'false' || path === false) return;
1596
1665
 
1597
1666
  if (path === 'true') {
1598
- path = ctx.request.action + (ctx.request.anchor ? '#' + ctx.request.anchor : '');
1667
+ let finalUrl = response?.raw?.url || ctx.request.action;
1668
+ let url = new URL(finalUrl, location.href);
1669
+ path = url.pathname + url.search + (ctx.request.anchor ? '#' + ctx.request.anchor : '');
1599
1670
  }
1600
1671
 
1601
1672
  let type = push ? 'push' : 'replace';
@@ -1631,15 +1702,18 @@ var htmx = (() => {
1631
1702
  }
1632
1703
  }
1633
1704
 
1634
- #showIndicators(elt, indicatorsSelector) {
1635
- let indicatorElements = []
1636
- if (indicatorsSelector) {
1637
- indicatorElements = [elt, ...this.#queryEltAndDescendants(elt, indicatorsSelector)];
1638
- for (const indicator of indicatorElements) {
1639
- indicator._htmxReqCount ||= 0
1640
- indicator._htmxReqCount++
1641
- indicator.classList.add(this.config.requestClass)
1642
- }
1705
+ #showIndicators(elt) {
1706
+ let indicatorsSelector = this.#attributeValue(elt, "hx-indicator");
1707
+ let indicatorElements;
1708
+ if (!indicatorsSelector) {
1709
+ indicatorElements = [elt]
1710
+ } else {
1711
+ indicatorElements = this.#findAllExt(elt, indicatorsSelector, "hx-indicator");
1712
+ }
1713
+ for (const indicator of indicatorElements) {
1714
+ indicator._htmxReqCount ||= 0
1715
+ indicator._htmxReqCount++
1716
+ indicator.classList.add(this.config.requestClass)
1643
1717
  }
1644
1718
  return indicatorElements
1645
1719
  }
@@ -1656,10 +1730,11 @@ var htmx = (() => {
1656
1730
  }
1657
1731
  }
1658
1732
 
1659
- #disableElements(elt, disabledSelector) {
1733
+ #disableElements(elt) {
1734
+ let disabledSelector = this.#attributeValue(elt, "hx-disable");
1660
1735
  let disabledElements = []
1661
1736
  if (disabledSelector) {
1662
- disabledElements = this.#queryEltAndDescendants(elt, disabledSelector);
1737
+ disabledElements = this.#findAllExt(elt, disabledSelector, "hx-disable");
1663
1738
  for (let indicator of disabledElements) {
1664
1739
  indicator._htmxDisableCount ||= 0
1665
1740
  indicator._htmxDisableCount++
@@ -1681,10 +1756,13 @@ var htmx = (() => {
1681
1756
  }
1682
1757
  }
1683
1758
 
1684
- #collectFormData(elt, form, submitter) {
1759
+ #collectFormData(elt, form, submitter, validate) {
1760
+ if (validate && form && !form.reportValidity()) return
1761
+
1685
1762
  let formData = form ? new FormData(form) : new FormData()
1686
1763
  let included = form ? new Set(form.elements) : new Set()
1687
1764
  if (!form && elt.name) {
1765
+ if (validate && elt.reportValidity && !elt.reportValidity()) return
1688
1766
  formData.append(elt.name, elt.value)
1689
1767
  included.add(elt);
1690
1768
  }
@@ -1694,8 +1772,8 @@ var htmx = (() => {
1694
1772
  }
1695
1773
  let includeSelector = this.#attributeValue(elt, "hx-include");
1696
1774
  if (includeSelector) {
1697
- let includeNodes = this.#findAllExt(elt, includeSelector);
1698
- for (let node of includeNodes) {
1775
+ for (let node of this.#findAllExt(elt, includeSelector)) {
1776
+ if (validate && node.reportValidity && !node.reportValidity()) return
1699
1777
  this.#addInputValues(node, included, formData);
1700
1778
  }
1701
1779
  }
@@ -1732,37 +1810,42 @@ var htmx = (() => {
1732
1810
  }
1733
1811
  }
1734
1812
 
1735
- #handleHxVals(elt, body) {
1736
- let hxValsValue = this.#attributeValue(elt, "hx-vals");
1737
- if (hxValsValue) {
1738
- let javascriptContent = this.#extractJavascriptContent(hxValsValue);
1739
- if (javascriptContent) {
1740
- // Return promise for async evaluation
1741
- return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true).then(obj => {
1742
- for (let key in obj) {
1743
- body.append(key, obj[key])
1744
- }
1745
- });
1746
- } else {
1747
- // Synchronous path
1748
- let obj = this.#parseConfig(hxValsValue);
1749
- for (let key in obj) {
1750
- body.append(key, obj[key])
1751
- }
1813
+ #getAttributeObject(elt, attrName, callback) {
1814
+ let attrValue = this.#attributeValue(elt, attrName);
1815
+ if (!attrValue) return null;
1816
+
1817
+ let javascriptContent = this.#extractJavascriptContent(attrValue);
1818
+ if (javascriptContent) {
1819
+ // Wrap in braces if not already wrapped (for htmx 2.x compatibility)
1820
+ if (javascriptContent.indexOf('{') !== 0) {
1821
+ javascriptContent = '{' + javascriptContent + '}';
1752
1822
  }
1823
+ // Return promise for async evaluation
1824
+ return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true).then(obj => {
1825
+ callback(obj);
1826
+ });
1827
+ } else {
1828
+ // Synchronous path - return the parsed object directly
1829
+ callback(this.#parseConfig(attrValue));
1753
1830
  }
1754
1831
  }
1755
1832
 
1833
+ #handleHxVals(elt, body) {
1834
+ return this.#getAttributeObject(elt, "hx-vals", obj => {
1835
+ for (let key in obj) body.set(key, obj[key]);
1836
+ });
1837
+ }
1838
+
1756
1839
  #stringHyperscriptStyleSelector(selector) {
1757
1840
  let s = selector.trim();
1758
1841
  return s.startsWith('<') && s.endsWith('/>') ? s.slice(1, -2) : s;
1759
1842
  }
1760
1843
 
1761
- #findAllExt(eltOrSelector, maybeSelector, global) {
1844
+ #findAllExt(eltOrSelector, maybeSelector, thisAttr, global) {
1762
1845
  let selector = maybeSelector ?? eltOrSelector;
1763
1846
  let elt = maybeSelector ? this.#normalizeElement(eltOrSelector) : document;
1764
1847
  if (selector.startsWith('global ')) {
1765
- return this.#findAllExt(elt, selector.slice(7), true);
1848
+ return this.#findAllExt(elt, selector.slice(7), thisAttr, true);
1766
1849
  }
1767
1850
  let parts = selector ? selector.replace(/<[^>]+\/>/g, m => m.replace(/,/g, '%2C'))
1768
1851
  .split(',').map(p => p.replace(/%2C/g, ',')) : [];
@@ -1774,7 +1857,9 @@ var htmx = (() => {
1774
1857
  if (selector.startsWith('closest ')) {
1775
1858
  item = elt.closest(selector.slice(8))
1776
1859
  } else if (selector.startsWith('find ')) {
1777
- item = document.querySelector(elt, selector.slice(5))
1860
+ item = elt.querySelector(selector.slice(5))
1861
+ } else if (selector.startsWith('findAll ')) {
1862
+ result.push(...elt.querySelectorAll(selector.slice(8)))
1778
1863
  } else if (selector === 'next' || selector === 'nextElementSibling') {
1779
1864
  item = elt.nextElementSibling
1780
1865
  } else if (selector.startsWith('next ')) {
@@ -1789,10 +1874,14 @@ var htmx = (() => {
1789
1874
  item = window
1790
1875
  } else if (selector === 'body') {
1791
1876
  item = document.body
1792
- } else if (selector === 'root') {
1793
- item = this.#getRootNode(elt, !!global)
1794
1877
  } else if (selector === 'host') {
1795
1878
  item = (elt.getRootNode()).host
1879
+ } else if (selector === 'this') {
1880
+ if (thisAttr) {
1881
+ result.push(...this.#findThisElements(elt, thisAttr));
1882
+ continue;
1883
+ }
1884
+ item = elt
1796
1885
  } else {
1797
1886
  unprocessedParts.push(selector)
1798
1887
  }
@@ -1808,7 +1897,7 @@ var htmx = (() => {
1808
1897
  result.push(...rootNode.querySelectorAll(standardSelector))
1809
1898
  }
1810
1899
 
1811
- return result
1900
+ return [...new Set(result)]
1812
1901
  }
1813
1902
 
1814
1903
  #scanForwardQuery(start, match, global) {
@@ -1836,8 +1925,8 @@ var htmx = (() => {
1836
1925
  }
1837
1926
  }
1838
1927
 
1839
- #findExt(eltOrSelector, selector) {
1840
- return this.#findAllExt(eltOrSelector, selector)[0]
1928
+ #findExt(eltOrSelector, selector, thisAttr) {
1929
+ return this.#findAllExt(eltOrSelector, selector, thisAttr)[0]
1841
1930
  }
1842
1931
 
1843
1932
  #extractJavascriptContent(string) {
@@ -1937,26 +2026,35 @@ var htmx = (() => {
1937
2026
  }
1938
2027
 
1939
2028
  #findBestMatch(ctx, node, startPoint, endPoint) {
1940
- let softMatch = null, nextSibling = node.nextSibling, siblingSoftMatchCount = 0, displaceMatchCount = 0;
2029
+ let softMatch = null, nextSibling = node.nextSibling, siblingMatchCount = 0, displaceMatchCount = 0, scanLimit = this.config.morphScanLimit;
2030
+ // Get ID count for this node to prioritize ID-based matches
1941
2031
  let newSet = ctx.idMap.get(node), nodeMatchCount = newSet?.size || 0;
1942
2032
  let cursor = startPoint;
1943
2033
  while (cursor && cursor != endPoint) {
1944
2034
  let oldSet = ctx.idMap.get(cursor);
1945
2035
  if (this.#isSoftMatch(cursor, node)) {
2036
+ // Hard match: matching IDs found in both nodes
1946
2037
  if (oldSet && newSet && [...oldSet].some(id => newSet.has(id))) return cursor;
1947
- if (softMatch === null && !oldSet) {
1948
- if (!nodeMatchCount) return cursor;
1949
- else softMatch = cursor;
2038
+ if (!oldSet) {
2039
+ // Exact match: nodes are identical
2040
+ if (scanLimit > 0 && cursor.isEqualNode(node)) return cursor;
2041
+ // Soft match: same tag/type, save as fallback
2042
+ if (!softMatch) softMatch = cursor;
1950
2043
  }
1951
2044
  }
2045
+ // Stop if too many ID elements would be displaced
1952
2046
  displaceMatchCount += oldSet?.size || 0;
1953
2047
  if (displaceMatchCount > nodeMatchCount) break;
1954
- if (softMatch === null && nextSibling && this.#isSoftMatch(cursor, nextSibling)) {
1955
- siblingSoftMatchCount++;
2048
+ // Look ahead: if next siblings match exactly, abort to let them match instead
2049
+ if (nextSibling && scanLimit > 0 && cursor.isEqualNode(nextSibling)) {
2050
+ siblingMatchCount++;
1956
2051
  nextSibling = nextSibling.nextSibling;
1957
- if (siblingSoftMatchCount >= 2) softMatch = undefined;
2052
+ if (siblingMatchCount >= 2) return null;
1958
2053
  }
2054
+ // Don't move elements containing focus
1959
2055
  if (cursor.contains(document.activeElement)) break;
2056
+ // Stop scanning if limit reached and no IDs to match
2057
+ if (--scanLimit < 1 && nodeMatchCount === 0) break;
1960
2058
  cursor = cursor.nextSibling;
1961
2059
  }
1962
2060
  return softMatch || null;
@@ -1992,8 +2090,8 @@ var htmx = (() => {
1992
2090
  let type = newNode.nodeType;
1993
2091
 
1994
2092
  if (type === 1) {
1995
- let noMorph = this.config.morphIgnore || [];
1996
- this.#copyAttributes(oldNode, newNode, noMorph);
2093
+ if (this.config.morphSkip && oldNode.matches?.(this.config.morphSkip)) return;
2094
+ this.#copyAttributes(oldNode, newNode);
1997
2095
  if (oldNode instanceof HTMLTextAreaElement && oldNode.defaultValue != newNode.defaultValue) {
1998
2096
  oldNode.value = newNode.value;
1999
2097
  }
@@ -2002,10 +2100,13 @@ var htmx = (() => {
2002
2100
  if ((type === 8 || type === 3) && oldNode.nodeValue !== newNode.nodeValue) {
2003
2101
  oldNode.nodeValue = newNode.nodeValue;
2004
2102
  }
2005
- if (!oldNode.isEqualNode(newNode)) this.#morphChildren(ctx, oldNode, newNode);
2103
+
2104
+ let skipChildren = this.config.morphSkipChildren && oldNode.matches?.(this.config.morphSkipChildren);
2105
+ if (!skipChildren && !oldNode.isEqualNode(newNode)) this.#morphChildren(ctx, oldNode, newNode);
2006
2106
  }
2007
2107
 
2008
- #copyAttributes(destination, source, attributesToIgnore = []) {
2108
+ #copyAttributes(destination, source) {
2109
+ let attributesToIgnore = this.config.morphIgnore || [];
2009
2110
  for (const attr of source.attributes) {
2010
2111
  if (!attributesToIgnore.includes(attr.name) && destination.getAttribute(attr.name) !== attr.value) {
2011
2112
  destination.setAttribute(attr.name, attr.value);
@@ -2075,7 +2176,7 @@ var htmx = (() => {
2075
2176
  }
2076
2177
  let statusValue = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
2077
2178
  if (statusValue) {
2078
- Object.assign(ctx, this.#parseConfig(statusValue));
2179
+ this.#mergeConfig(statusValue, ctx);
2079
2180
  return;
2080
2181
  }
2081
2182
  }
@@ -2116,21 +2217,22 @@ var htmx = (() => {
2116
2217
  }
2117
2218
  }
2118
2219
 
2119
- #captureCSSTransitions(task, root) {
2220
+ #startCSSTransitions(fragment, root) {
2120
2221
  let idElements = root.querySelectorAll("[id]");
2121
2222
  let existingElementsById = Object.fromEntries([...idElements].map(e => [e.id, e]));
2122
- let newElementsWithIds = task.fragment.querySelectorAll("[id]");
2123
- task.restoreTasks = []
2223
+ let newElementsWithIds = fragment.querySelectorAll("[id]");
2224
+ let restoreTasks = []
2124
2225
  for (let elt of newElementsWithIds) {
2125
2226
  let existing = existingElementsById[elt.id];
2126
2227
  if (existing?.tagName === elt.tagName) {
2127
2228
  let clone = elt.cloneNode(false); // shallow clone node
2128
- this.#copyAttributes(elt, existing, this.config.morphIgnore)
2129
- task.restoreTasks.push(()=>{
2130
- this.#copyAttributes(elt, clone, this.config.morphIgnore)
2229
+ this.#copyAttributes(elt, existing)
2230
+ restoreTasks.push(()=>{
2231
+ this.#copyAttributes(elt, clone)
2131
2232
  })
2132
2233
  }
2133
2234
  }
2235
+ return restoreTasks;
2134
2236
  }
2135
2237
 
2136
2238
  #normalizeElement(cssOrElement) {