htmx.org 4.0.0-alpha3 → 4.0.0-alpha5

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
@@ -80,7 +80,8 @@ 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)
84
85
  };
85
86
  document.addEventListener("DOMContentLoaded", () => {
86
87
  this.#initHistoryHandling();
@@ -90,12 +91,11 @@ var htmx = (() => {
90
91
 
91
92
  #initHtmxConfig() {
92
93
  this.config = {
93
- version: '4.0.0-alpha3',
94
+ version: '4.0.0-alpha5',
94
95
  logAll: false,
95
96
  prefix: "",
96
- transitions: true,
97
+ transitions: false,
97
98
  history: true,
98
- historyReload: false,
99
99
  mode: 'same-origin',
100
100
  defaultSwap: "innerHTML",
101
101
  indicatorClass: "htmx-indicator",
@@ -104,29 +104,20 @@ var htmx = (() => {
104
104
  defaultTimeout: 60000, /* 60 second default timeout */
105
105
  extensions: '',
106
106
  sse: {
107
- mode: 'once',
108
- maxRetries: Infinity,
109
- initialDelay: 500,
110
- maxDelay: 30000,
111
- pauseHidden: false
107
+ reconnect: false,
108
+ reconnectDelay: 500,
109
+ reconnectMaxDelay: 60000,
110
+ reconnectMaxAttempts: 10,
111
+ reconnectJitter: 0.3,
112
+ pauseInBackground: false
112
113
  },
113
114
  morphIgnore: ["data-htmx-powered"],
114
115
  noSwap: [204, 304],
115
116
  implicitInheritance: false
116
117
  }
117
- let metaConfig = document.querySelector('meta[name="htmx:config"]');
118
+ let metaConfig = document.querySelector('meta[name="htmx-config"]');
118
119
  if (metaConfig) {
119
- let content = metaConfig.content;
120
- let overrides = this.#parseConfig(content);
121
- // Deep merge nested config objects
122
- for (let key in overrides) {
123
- let val = overrides[key];
124
- if (val && typeof val === 'object' && !Array.isArray(val) && this.config[key]) {
125
- Object.assign(this.config[key], val);
126
- } else {
127
- this.config[key] = val;
128
- }
129
- }
120
+ this.#mergeConfig(metaConfig.content, this.config);
130
121
  }
131
122
  this.#approvedExt = this.config.extensions;
132
123
  }
@@ -147,7 +138,7 @@ var htmx = (() => {
147
138
  }
148
139
  }
149
140
 
150
- defineExtension(name, extension) {
141
+ registerExtension(name, extension) {
151
142
  if (this.#approvedExt && !this.#approvedExt.split(/,\s*/).includes(name)) return false;
152
143
  if (this.#registeredExt.has(name)) return false;
153
144
  this.#registeredExt.add(name);
@@ -180,48 +171,60 @@ var htmx = (() => {
180
171
  style === 'append' ? 'beforeend' : style;
181
172
  }
182
173
 
183
- #attributeValue(elt, name, defaultVal, returnElt) {
174
+ #findThisElements(elt, attrName) {
175
+ let result = [];
176
+ this.#attributeValue(elt, attrName, undefined, (val, elt) => {
177
+ if (val?.split(/\s*,\s*/).includes('this')) result.push(elt);
178
+ });
179
+ return result;
180
+ }
181
+
182
+ #attributeValue(elt, name, defaultVal, eltCollector) {
184
183
  name = this.#prefix(name);
185
184
  let appendName = name + this.#maybeAdjustMetaCharacter(":append");
186
185
  let inheritName = name + (this.config.implicitInheritance ? "" : this.#maybeAdjustMetaCharacter(":inherited"));
187
186
  let inheritAppendName = name + this.#maybeAdjustMetaCharacter(":inherited:append");
188
187
 
189
188
  if (elt.hasAttribute(name)) {
190
- return returnElt ? elt : elt.getAttribute(name);
189
+ let val = elt.getAttribute(name);
190
+ return eltCollector ? eltCollector(val, elt) : val;
191
191
  }
192
192
 
193
193
  if (elt.hasAttribute(inheritName)) {
194
- return returnElt ? elt : elt.getAttribute(inheritName);
194
+ let val = elt.getAttribute(inheritName);
195
+ return eltCollector ? eltCollector(val, elt) : val;
195
196
  }
196
197
 
197
198
  if (elt.hasAttribute(appendName) || elt.hasAttribute(inheritAppendName)) {
198
199
  let appendValue = elt.getAttribute(appendName) || elt.getAttribute(inheritAppendName);
199
200
  let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
201
+ if (eltCollector) {
202
+ eltCollector(appendValue, elt);
203
+ }
200
204
  if (parent) {
201
- let inherited = this.#attributeValue(parent, name, undefined, returnElt);
202
- return returnElt ? inherited : (inherited ? inherited + "," + appendValue : appendValue);
203
- } else {
204
- return returnElt ? elt : appendValue;
205
+ let inherited = this.#attributeValue(parent, name, undefined, eltCollector);
206
+ return inherited ? (inherited + "," + appendValue).replace(/[{}]/g, '') : appendValue;
205
207
  }
208
+ return appendValue;
206
209
  }
207
210
 
208
211
  let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
209
212
  if (parent) {
210
- let val = this.#attributeValue(parent, name, undefined, returnElt);
211
- if (!returnElt && val && this.config.implicitInheritance) {
212
- this.#triggerExtensions(elt, "htmx:after:implicitInheritance", {elt, parent})
213
+ let val = this.#attributeValue(parent, name, undefined, eltCollector);
214
+ if (!eltCollector && val && this.config.implicitInheritance) {
215
+ this.#triggerExtensions(elt, "htmx:after:implicitInheritance", {elt, name, parent})
213
216
  }
214
217
  return val;
215
218
  }
216
- return returnElt ? elt : defaultVal;
219
+ return defaultVal;
217
220
  }
218
221
 
219
222
  #parseConfig(configString) {
220
223
  if (configString[0] === '{') return JSON.parse(configString);
221
- let configPattern = /([^\s,]+?)(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
224
+ let configPattern = /(?:"([^"]+)"|([^\s,:]+))(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
222
225
  return [...configString.matchAll(configPattern)].reduce((result, match) => {
223
- let keyPath = match[1].split('.');
224
- let value = (match[2] ?? match[3] ?? match[4] ?? match[5] ?? 'true').trim();
226
+ let keyPath = (match[1] ?? match[2]).split('.');
227
+ let value = (match[3] ?? match[4] ?? match[5] ?? match[6] ?? 'true').trim();
225
228
  if (value === 'true') value = true;
226
229
  else if (value === 'false') value = false;
227
230
  else if (/^\d+$/.test(value)) value = parseInt(value);
@@ -230,6 +233,19 @@ var htmx = (() => {
230
233
  }, {});
231
234
  }
232
235
 
236
+ #mergeConfig(configString, target) {
237
+ let parsed = this.#parseConfig(configString);
238
+ for (let key in parsed) {
239
+ let val = parsed[key];
240
+ if (val && typeof val === 'object' && !Array.isArray(val) && target[key]) {
241
+ Object.assign(target[key], val);
242
+ } else {
243
+ target[key] = val;
244
+ }
245
+ }
246
+ return target;
247
+ }
248
+
233
249
  #parseTriggerSpecs(spec) {
234
250
  return spec.split(',').map(s => {
235
251
  let m = s.match(/^\s*(\S+\[[^\]]*\]|\S+)\s*(.*?)\s*$/);
@@ -296,45 +312,44 @@ var htmx = (() => {
296
312
 
297
313
  #createRequestContext(sourceElement, sourceEvent) {
298
314
  let {action, method} = this.#determineMethodAndAction(sourceElement, sourceEvent);
315
+ let [fullAction, anchor] = (action || '').split('#');
316
+ let ac = new AbortController();
299
317
  let ctx = {
300
318
  sourceElement,
301
319
  sourceEvent,
302
320
  status: "created",
303
321
  select: this.#attributeValue(sourceElement, "hx-select"),
304
322
  selectOOB: this.#attributeValue(sourceElement, "hx-select-oob"),
305
- target: this.#resolveTarget(sourceElement, this.#attributeValue(sourceElement, "hx-target")),
306
- swap: this.#attributeValue(sourceElement, "hx-swap", this.config.defaultSwap),
323
+ target: this.#attributeValue(sourceElement, "hx-target"),
324
+ swap: this.#attributeValue(sourceElement, "hx-swap") ?? this.config.defaultSwap,
307
325
  push: this.#attributeValue(sourceElement, "hx-push-url"),
308
326
  replace: this.#attributeValue(sourceElement, "hx-replace-url"),
309
327
  transition: this.config.transitions,
310
328
  confirm: this.#attributeValue(sourceElement, "hx-confirm"),
311
329
  request: {
312
- validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') ? "true" : "false"),
313
- action,
330
+ validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') && !sourceElement.noValidate && !sourceEvent.submitter?.formNoValidate ? "true" : "false"),
331
+ action: fullAction,
332
+ anchor,
314
333
  method,
315
- headers: this.#determineHeaders(sourceElement)
334
+ headers: this.#createCoreHeaders(sourceElement),
335
+ abort: ac.abort.bind(ac),
336
+ credentials: "same-origin",
337
+ signal: ac.signal,
338
+ mode: this.config.mode
316
339
  }
317
340
  };
341
+ // Apply boost config overrides
342
+ if (sourceElement._htmx?.boosted) {
343
+ this.#mergeConfig(sourceElement._htmx.boosted, ctx);
344
+ }
345
+ ctx.target = this.#resolveTarget(sourceElement, ctx.target);
318
346
 
319
347
  // Apply hx-config overrides
320
348
  let configAttr = this.#attributeValue(sourceElement, "hx-config");
321
349
  if (configAttr) {
322
- let configOverrides = this.#parseConfig(configAttr);
323
- let req = ctx.request;
324
- for (let key in configOverrides) {
325
- if (key.startsWith('+')) {
326
- let actualKey = key.substring(1);
327
- if (req[actualKey] && typeof req[actualKey] === 'object') {
328
- Object.assign(req[actualKey], configOverrides[key]);
329
- } else {
330
- req[actualKey] = configOverrides[key];
331
- }
332
- } else {
333
- req[key] = configOverrides[key];
334
- }
335
- }
336
- if (req.etag) {
337
- (sourceElement._htmx ||= {}).etag ||= req.etag
350
+ this.#mergeConfig(configAttr, ctx.request);
351
+ if (ctx.request.etag) {
352
+ (sourceElement._htmx ||= {}).etag ||= ctx.request.etag
338
353
  }
339
354
  }
340
355
  if (sourceElement._htmx?.etag) {
@@ -343,30 +358,45 @@ var htmx = (() => {
343
358
  return ctx;
344
359
  }
345
360
 
346
- #determineHeaders(elt) {
361
+ #buildIdentifier(elt) {
362
+ return `${elt.tagName.toLowerCase()}${elt.id ? '#' + elt.id : ''}`;
363
+ }
364
+
365
+ #createCoreHeaders(elt) {
347
366
  let headers = {
348
367
  "HX-Request": "true",
349
- "HX-Source": elt.id || elt.name,
368
+ "HX-Source": this.#buildIdentifier(elt),
350
369
  "HX-Current-URL": location.href,
351
370
  "Accept": "text/html, text/event-stream"
352
371
  };
353
372
  if (this.#isBoosted(elt)) {
354
373
  headers["HX-Boosted"] = "true"
355
374
  }
356
- let headersAttribute = this.#attributeValue(elt, "hx-headers");
357
- if (headersAttribute) {
358
- Object.assign(headers, this.#parseConfig(headersAttribute));
359
- }
360
375
  return headers;
361
376
  }
362
377
 
378
+ #handleHxHeaders(elt, headers) {
379
+ let result = this.#getAttributeObject(elt, "hx-headers");
380
+ if (result) {
381
+ if (result instanceof Promise) {
382
+ return result.then(obj => {
383
+ for (let key in obj) {
384
+ headers[key] = String(obj[key]);
385
+ }
386
+ });
387
+ } else {
388
+ for (let key in result) {
389
+ headers[key] = String(result[key]);
390
+ }
391
+ }
392
+ }
393
+ }
394
+
363
395
  #resolveTarget(elt, selector) {
364
396
  if (selector instanceof Element) {
365
397
  return selector;
366
- } else if (selector === 'this') {
367
- return this.#attributeValue(elt, "hx-target", undefined, true);
368
398
  } else if (selector != null) {
369
- return this.find(elt, selector);
399
+ return this.#findExt(elt, selector, "hx-target");
370
400
  } else if (this.#isBoosted(elt)) {
371
401
  return document.body
372
402
  } else {
@@ -389,7 +419,8 @@ var htmx = (() => {
389
419
 
390
420
  // Build request body
391
421
  let form = elt.form || elt.closest("form")
392
- let body = this.#collectFormData(elt, form, evt.submitter)
422
+ let body = this.#collectFormData(elt, form, evt.submitter, ctx.request.validate)
423
+ if (!body) return // Validation failed
393
424
  let valsResult = this.#handleHxVals(elt, body)
394
425
  if (valsResult) await valsResult // Only await if it returned a promise
395
426
  if (ctx.values) {
@@ -399,35 +430,43 @@ var htmx = (() => {
399
430
  }
400
431
  }
401
432
 
402
- // Setup abort controller and action
403
- let ac = new AbortController()
404
- let action = ctx.request.action.replace?.(/#.*$/, '')
405
- // TODO - consider how this works with hx-config, move most to #createRequestContext?
433
+ // Handle dynamic headers
434
+ let headersResult = this.#handleHxHeaders(elt, ctx.request.headers)
435
+ if (headersResult) await headersResult // Only await if it returned a promise
436
+
437
+ // Add HX-Request-Type and HX-Target headers
438
+ ctx.request.headers["HX-Request-Type"] = (ctx.target === document.body || ctx.select) ? "full" : "partial";
439
+ if (ctx.target) {
440
+ ctx.request.headers["HX-Target"] = this.#buildIdentifier(ctx.target);
441
+ }
442
+
443
+ // Setup event-dependent request details
406
444
  Object.assign(ctx.request, {
407
- originalAction: ctx.request.action,
408
- action,
409
445
  form,
410
446
  submitter: evt.submitter,
411
- abort: ac.abort.bind(ac),
412
- body,
413
- credentials: "same-origin",
414
- signal: ac.signal,
415
- mode: this.config.mode
447
+ body
416
448
  })
417
449
 
418
450
  if (!this.#trigger(elt, "htmx:config:request", {ctx: ctx})) return
419
451
  if (!this.#verbs.includes(ctx.request.method.toLowerCase())) return
420
- if (ctx.request.validate && ctx.request.form && !ctx.request.form.reportValidity()) return
421
452
 
422
453
  let javascriptContent = this.#extractJavascriptContent(ctx.request.action);
423
- if (javascriptContent) {
454
+ if (javascriptContent != null) {
424
455
  let data = Object.fromEntries(ctx.request.body);
425
456
  await this.#executeJavaScriptAsync(ctx.sourceElement, data, javascriptContent, false);
426
457
  return
427
458
  } else if (/GET|DELETE/.test(ctx.request.method)) {
428
- let params = new URLSearchParams(ctx.request.body);
429
- if (params.size) ctx.request.action += (/\?/.test(ctx.request.action) ? "&" : "?") + params
430
- ctx.request.body = null
459
+ let url = new URL(ctx.request.action, document.baseURI);
460
+
461
+ for (let key of ctx.request.body.keys()) {
462
+ url.searchParams.delete(key);
463
+ }
464
+ for (let [key, value] of ctx.request.body) {
465
+ url.searchParams.append(key, value);
466
+ }
467
+
468
+ ctx.request.action = url.pathname + url.search;
469
+ ctx.request.body = null;
431
470
  } else if (this.#attributeValue(elt, "hx-encoding") !== "multipart/form-data") {
432
471
  ctx.request.body = new URLSearchParams(ctx.request.body);
433
472
  }
@@ -445,10 +484,8 @@ var htmx = (() => {
445
484
  ctx.status = "issuing"
446
485
  this.#initTimeout(ctx);
447
486
 
448
- let indicatorsSelector = this.#attributeValue(elt, "hx-indicator");
449
- let indicators = this.#showIndicators(elt, indicatorsSelector);
450
- let disableSelector = this.#attributeValue(elt, "hx-disable");
451
- let disableElements = this.#disableElements(elt, disableSelector);
487
+ let indicators = this.#showIndicators(elt);
488
+ let disableElements = this.#disableElements(elt);
452
489
 
453
490
  try {
454
491
  // Handle confirmation
@@ -559,9 +596,7 @@ var htmx = (() => {
559
596
  }
560
597
 
561
598
  async #handleSSE(ctx, elt, response) {
562
- let config = {...this.config.sse, ...ctx.request.sse};
563
- if (config.once) config.mode = 'once';
564
- if (config.continuous) config.mode = 'continuous';
599
+ let config = {...this.config.sse, ...ctx.request.sse}
565
600
 
566
601
  let waitForVisible = () => new Promise(r => {
567
602
  let onVisible = () => !document.hidden && (document.removeEventListener('visibilitychange', onVisible), r());
@@ -573,14 +608,19 @@ var htmx = (() => {
573
608
  while (elt.isConnected) {
574
609
  // Handle reconnection for subsequent iterations
575
610
  if (attempt > 0) {
576
- if (config.mode !== 'continuous' || attempt > config.maxRetries) break;
611
+ if (!config.reconnect || attempt > config.reconnectMaxAttempts) break;
577
612
 
578
- if (config.pauseHidden && document.hidden) {
613
+ if (config.pauseInBackground && document.hidden) {
579
614
  await waitForVisible();
580
615
  if (!elt.isConnected) break;
581
616
  }
582
617
 
583
- let delay = Math.min(this.parseInterval(config.initialDelay) * Math.pow(2, attempt - 1), this.parseInterval(config.maxDelay));
618
+ let delay = Math.min(this.parseInterval(config.reconnectDelay) * Math.pow(2, attempt - 1), this.parseInterval(config.reconnectMaxDelay));
619
+ if (config.reconnectJitter > 0) {
620
+ let jitterRange = delay * config.reconnectJitter;
621
+ let jitter = (Math.random() * 2 - 1) * jitterRange;
622
+ delay = Math.max(0, delay + jitter);
623
+ }
584
624
  let reconnect = {attempt, delay, lastEventId, cancelled: false};
585
625
 
586
626
  ctx.status = "reconnecting to stream";
@@ -613,7 +653,7 @@ var htmx = (() => {
613
653
  for await (const sseMessage of this.#parseSSE(currentResponse)) {
614
654
  if (!elt.isConnected) break;
615
655
 
616
- if (config.pauseHidden && document.hidden) {
656
+ if (config.pauseInBackground && document.hidden) {
617
657
  await waitForVisible();
618
658
  if (!elt.isConnected) break;
619
659
  }
@@ -910,8 +950,6 @@ var htmx = (() => {
910
950
  }
911
951
  }
912
952
 
913
-
914
-
915
953
  #extractFilter(str) {
916
954
  let match = str.match(/^([^\[]*)\[([^\]]*)]/);
917
955
  if (!match) return [str, null];
@@ -993,8 +1031,9 @@ var htmx = (() => {
993
1031
  }
994
1032
 
995
1033
  #maybeBoost(elt) {
996
- if (this.#attributeValue(elt, "hx-boost") === "true" && this.#shouldBoost(elt)) {
997
- elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt), requests: [], boosted: true}
1034
+ let boostValue = this.#attributeValue(elt, "hx-boost");
1035
+ if (boostValue && boostValue !== "false" && this.#shouldBoost(elt)) {
1036
+ elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt), requests: [], boosted: boostValue}
998
1037
  elt.setAttribute('data-htmx-powered', 'true');
999
1038
  if (elt.matches('a') && !elt.hasAttribute("target")) {
1000
1039
  elt.addEventListener('click', (click) => {
@@ -1179,13 +1218,11 @@ var htmx = (() => {
1179
1218
  let type = templateElt.getAttribute('type');
1180
1219
 
1181
1220
  if (type === 'partial') {
1182
- let swapSpec = this.#parseSwapSpec(templateElt.getAttribute(this.#prefix('hx-swap')) || this.config.defaultSwap);
1183
-
1184
1221
  tasks.push({
1185
1222
  type: 'partial',
1186
1223
  fragment: templateElt.content.cloneNode(true),
1187
1224
  target: templateElt.getAttribute(this.#prefix('hx-target')),
1188
- swapSpec,
1225
+ swapSpec: this.#parseSwapSpec(templateElt.getAttribute(this.#prefix('hx-swap')) || this.config.defaultSwap),
1189
1226
  sourceElement: ctx.sourceElement
1190
1227
  });
1191
1228
  } else {
@@ -1202,25 +1239,24 @@ var htmx = (() => {
1202
1239
  autofocus?.focus?.()
1203
1240
  }
1204
1241
 
1205
- #handleScroll(task) {
1206
- if (task.swapSpec.scroll) {
1207
- let target = task.swapSpec.scrollTarget ? this.#findExt(task.swapSpec.scrollTarget) : task.target;
1208
- if (task.swapSpec.scroll === 'top') {
1209
- target.scrollTop = 0;
1210
- } else if (task.swapSpec.scroll === 'bottom'){
1211
- target.scrollTop = target.scrollHeight;
1242
+ #handleScroll(swapSpec, target) {
1243
+ if (swapSpec.scroll) {
1244
+ let scrollTarget = swapSpec.scrollTarget ? this.#findExt(swapSpec.scrollTarget) : target;
1245
+ if (swapSpec.scroll === 'top') {
1246
+ scrollTarget.scrollTop = 0;
1247
+ } else if (swapSpec.scroll === 'bottom'){
1248
+ scrollTarget.scrollTop = scrollTarget.scrollHeight;
1212
1249
  }
1213
1250
  }
1214
- if (task.swapSpec.show) {
1215
- let target = task.swapSpec.showTarget ? this.#findExt(task.swapSpec.showTarget) : task.target;
1216
- target.scrollIntoView(task.swapSpec.show === 'top')
1251
+ if (swapSpec.show) {
1252
+ let showTarget = swapSpec.showTarget ? this.#findExt(swapSpec.showTarget) : target;
1253
+ showTarget.scrollIntoView(swapSpec.show === 'top')
1217
1254
  }
1218
1255
  }
1219
1256
 
1220
1257
  #handleAnchorScroll(ctx) {
1221
- let anchor = ctx.request?.originalAction?.split('#')[1];
1222
- if (anchor) {
1223
- document.getElementById(anchor)?.scrollIntoView({block: 'start', behavior: 'auto'});
1258
+ if (ctx.request?.anchor) {
1259
+ document.getElementById(ctx.request.anchor)?.scrollIntoView({block: 'start', behavior: 'auto'});
1224
1260
  }
1225
1261
  }
1226
1262
 
@@ -1263,25 +1299,35 @@ var htmx = (() => {
1263
1299
  // TODO - can we remove this and just let the function complete?
1264
1300
  if (tasks.length === 0) return;
1265
1301
 
1266
- // Separate transition/nonTransition tasks
1267
- let transitionTasks = tasks.filter(t => t.transition);
1268
- let nonTransitionTasks = tasks.filter(t => !t.transition);
1269
-
1270
1302
  if(!this.#trigger(document, "htmx:before:swap", {ctx, tasks})){
1271
1303
  return
1272
1304
  }
1273
1305
 
1274
- // insert non-transition tasks immediately or with delay
1275
- for (let task of nonTransitionTasks) {
1276
- if (task.swapSpec?.swap) {
1277
- setTimeout(() => this.#insertContent(task), this.parseInterval(task.swapSpec.swap));
1278
- } else {
1306
+ // insert non-transition tasks immediately or with delay, collect transition tasks
1307
+ let transitionTasks = [];
1308
+ for (let task of tasks) {
1309
+ // OOB/partial tasks with swap delays should be non-transition (non-blocking)
1310
+ let swapDelay = task.swapSpec?.swap;
1311
+ if (!(task.swapSpec?.transition ?? mainSwap?.transition) || (swapDelay && task !== mainSwap)) {
1312
+ if (swapDelay) {
1313
+ if (task === mainSwap) {
1314
+ await this.timeout(swapDelay);
1315
+ } else {
1316
+ setTimeout(() => this.#insertContent(task), this.parseInterval(swapDelay));
1317
+ continue;
1318
+ }
1319
+ }
1279
1320
  this.#insertContent(task)
1321
+ } else {
1322
+ transitionTasks.push(task);
1280
1323
  }
1281
1324
  }
1282
1325
 
1283
1326
  // insert transition tasks in the transition queue
1284
1327
  if (transitionTasks.length > 0) {
1328
+ if (mainSwap?.transition && mainSwap?.swapSpec?.swap) {
1329
+ await this.timeout(mainSwap.swapSpec.swap);
1330
+ }
1285
1331
  let tasksWrapper = ()=> {
1286
1332
  for (let task of transitionTasks) {
1287
1333
  this.#insertContent(task)
@@ -1309,7 +1355,7 @@ var htmx = (() => {
1309
1355
  // Create main task if needed
1310
1356
  let swapSpec = this.#parseSwapSpec(ctx.swap || this.config.defaultSwap);
1311
1357
  // skip creating main swap if extracting partials resulted in empty response except for delete style
1312
- if (swapSpec.style === 'delete' || /\S/.test(fragment.innerHTML || '') || !partialTasks.length) {
1358
+ if (swapSpec.style === 'delete' || fragment.childElementCount > 0 || /\S/.test(fragment.textContent) || !partialTasks.length) {
1313
1359
  if (ctx.select) {
1314
1360
  let selected = fragment.querySelectorAll(ctx.select);
1315
1361
  fragment = document.createDocumentFragment();
@@ -1336,8 +1382,10 @@ var htmx = (() => {
1336
1382
  target = document.querySelector(target);
1337
1383
  }
1338
1384
  if (!target) return;
1385
+ if (typeof swapSpec === 'string') {
1386
+ swapSpec = this.#parseSwapSpec(swapSpec);
1387
+ }
1339
1388
  if (swapSpec.strip && fragment.firstElementChild) {
1340
- task.unstripped = fragment;
1341
1389
  fragment = document.createDocumentFragment();
1342
1390
  fragment.append(...(task.fragment.firstElementChild.content || task.fragment.firstElementChild).childNodes);
1343
1391
  }
@@ -1383,17 +1431,24 @@ var htmx = (() => {
1383
1431
  } else if (swapSpec.style === 'none') {
1384
1432
  return;
1385
1433
  } else {
1386
- task.target = target;
1387
- task.fragment = fragment;
1388
- if (!this.#triggerExtensions(target, 'htmx:handle:swap', task)) return;
1389
- throw new Error(`Unknown swap style: ${swapSpec.style}`);
1434
+ let methods = this.#extMethods.get('handle_swap')
1435
+ let handled = false;
1436
+ for (const method of methods) {
1437
+ if (method(swapSpec.style, target, fragment)) {
1438
+ handled = true;
1439
+ break;
1440
+ }
1441
+ }
1442
+ if (!handled) {
1443
+ throw new Error(`Unknown swap style: ${swapSpec.style}`);
1444
+ }
1390
1445
  }
1391
1446
  this.#restorePreservedElements(pantry);
1392
1447
  for (const elt of newContent) {
1393
1448
  this.process(elt);
1394
1449
  this.#handleAutoFocus(elt);
1395
1450
  }
1396
- this.#handleScroll(task);
1451
+ this.#handleScroll(swapSpec, target);
1397
1452
  }
1398
1453
 
1399
1454
  #trigger(on, eventName, detail = {}, bubbles = true) {
@@ -1401,7 +1456,7 @@ var htmx = (() => {
1401
1456
  console.log(eventName, detail, on)
1402
1457
  }
1403
1458
  on = this.#normalizeElement(on)
1404
- this.#triggerExtensions(on, this.#maybeAdjustMetaCharacter(eventName), detail);
1459
+ this.#triggerExtensions(on, eventName, detail);
1405
1460
  return this.trigger(on, eventName, detail, bubbles)
1406
1461
  }
1407
1462
 
@@ -1443,13 +1498,13 @@ var htmx = (() => {
1443
1498
  }
1444
1499
 
1445
1500
  onLoad(callback) {
1446
- this.on("htmx:after:init", (evt) => {
1501
+ this.on("htmx:after:process", (evt) => {
1447
1502
  callback(evt.target)
1448
1503
  })
1449
1504
  }
1450
1505
 
1451
1506
  takeClass(element, className, container = element.parentElement) {
1452
- for (let elt of this.findAll(this.#normalizeElement(container), "." + className)) {
1507
+ for (let elt of this.#findAllExt(this.#normalizeElement(container), "." + className)) {
1453
1508
  elt.classList.remove(className);
1454
1509
  }
1455
1510
  element.classList.add(className);
@@ -1560,7 +1615,7 @@ var htmx = (() => {
1560
1615
  #restoreHistory(path) {
1561
1616
  path = path || location.pathname + location.search;
1562
1617
  if (this.#trigger(document, "htmx:before:restore:history", {path, cacheMiss: true})) {
1563
- if (this.config.historyReload) {
1618
+ if (this.config.history === "reload") {
1564
1619
  location.reload();
1565
1620
  } else {
1566
1621
  this.ajax('GET', path, {
@@ -1588,7 +1643,7 @@ var htmx = (() => {
1588
1643
  if (!path || path === 'false' || path === false) return;
1589
1644
 
1590
1645
  if (path === 'true') {
1591
- path = ctx.request.originalAction;
1646
+ path = ctx.request.action + (ctx.request.anchor ? '#' + ctx.request.anchor : '');
1592
1647
  }
1593
1648
 
1594
1649
  let type = push ? 'push' : 'replace';
@@ -1624,15 +1679,18 @@ var htmx = (() => {
1624
1679
  }
1625
1680
  }
1626
1681
 
1627
- #showIndicators(elt, indicatorsSelector) {
1628
- let indicatorElements = []
1629
- if (indicatorsSelector) {
1630
- indicatorElements = [elt, ...this.#queryEltAndDescendants(elt, indicatorsSelector)];
1631
- for (const indicator of indicatorElements) {
1632
- indicator._htmxReqCount ||= 0
1633
- indicator._htmxReqCount++
1634
- indicator.classList.add(this.config.requestClass)
1635
- }
1682
+ #showIndicators(elt) {
1683
+ let indicatorsSelector = this.#attributeValue(elt, "hx-indicator");
1684
+ let indicatorElements;
1685
+ if (!indicatorsSelector) {
1686
+ indicatorElements = [elt]
1687
+ } else {
1688
+ indicatorElements = this.#findAllExt(elt, indicatorsSelector, "hx-indicator");
1689
+ }
1690
+ for (const indicator of indicatorElements) {
1691
+ indicator._htmxReqCount ||= 0
1692
+ indicator._htmxReqCount++
1693
+ indicator.classList.add(this.config.requestClass)
1636
1694
  }
1637
1695
  return indicatorElements
1638
1696
  }
@@ -1649,10 +1707,11 @@ var htmx = (() => {
1649
1707
  }
1650
1708
  }
1651
1709
 
1652
- #disableElements(elt, disabledSelector) {
1710
+ #disableElements(elt) {
1711
+ let disabledSelector = this.#attributeValue(elt, "hx-disable");
1653
1712
  let disabledElements = []
1654
1713
  if (disabledSelector) {
1655
- disabledElements = this.#queryEltAndDescendants(elt, disabledSelector);
1714
+ disabledElements = this.#findAllExt(elt, disabledSelector, "hx-disable");
1656
1715
  for (let indicator of disabledElements) {
1657
1716
  indicator._htmxDisableCount ||= 0
1658
1717
  indicator._htmxDisableCount++
@@ -1674,10 +1733,13 @@ var htmx = (() => {
1674
1733
  }
1675
1734
  }
1676
1735
 
1677
- #collectFormData(elt, form, submitter) {
1736
+ #collectFormData(elt, form, submitter, validate) {
1737
+ if (validate && form && !form.reportValidity()) return
1738
+
1678
1739
  let formData = form ? new FormData(form) : new FormData()
1679
1740
  let included = form ? new Set(form.elements) : new Set()
1680
1741
  if (!form && elt.name) {
1742
+ if (validate && elt.reportValidity && !elt.reportValidity()) return
1681
1743
  formData.append(elt.name, elt.value)
1682
1744
  included.add(elt);
1683
1745
  }
@@ -1687,8 +1749,8 @@ var htmx = (() => {
1687
1749
  }
1688
1750
  let includeSelector = this.#attributeValue(elt, "hx-include");
1689
1751
  if (includeSelector) {
1690
- let includeNodes = this.#findAllExt(elt, includeSelector);
1691
- for (let node of includeNodes) {
1752
+ for (let node of this.#findAllExt(elt, includeSelector)) {
1753
+ if (validate && node.reportValidity && !node.reportValidity()) return
1692
1754
  this.#addInputValues(node, included, formData);
1693
1755
  }
1694
1756
  }
@@ -1725,22 +1787,36 @@ var htmx = (() => {
1725
1787
  }
1726
1788
  }
1727
1789
 
1790
+ #getAttributeObject(elt, attrName) {
1791
+ let attrValue = this.#attributeValue(elt, attrName);
1792
+ if (!attrValue) return null;
1793
+
1794
+ let javascriptContent = this.#extractJavascriptContent(attrValue);
1795
+ if (javascriptContent) {
1796
+ // Wrap in braces if not already wrapped (for htmx 2.x compatibility)
1797
+ if (javascriptContent.indexOf('{') !== 0) {
1798
+ javascriptContent = '{' + javascriptContent + '}';
1799
+ }
1800
+ // Return promise for async evaluation
1801
+ return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true);
1802
+ } else {
1803
+ // Synchronous path - return the parsed object directly
1804
+ return this.#parseConfig(attrValue);
1805
+ }
1806
+ }
1807
+
1728
1808
  #handleHxVals(elt, body) {
1729
- let hxValsValue = this.#attributeValue(elt, "hx-vals");
1730
- if (hxValsValue) {
1731
- let javascriptContent = this.#extractJavascriptContent(hxValsValue);
1732
- if (javascriptContent) {
1733
- // Return promise for async evaluation
1734
- return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true).then(obj => {
1809
+ let result = this.#getAttributeObject(elt, "hx-vals");
1810
+ if (result) {
1811
+ if (result instanceof Promise) {
1812
+ return result.then(obj => {
1735
1813
  for (let key in obj) {
1736
- body.append(key, obj[key])
1814
+ body.set(key, obj[key])
1737
1815
  }
1738
1816
  });
1739
1817
  } else {
1740
- // Synchronous path
1741
- let obj = this.#parseConfig(hxValsValue);
1742
- for (let key in obj) {
1743
- body.append(key, obj[key])
1818
+ for (let key in result) {
1819
+ body.set(key, result[key])
1744
1820
  }
1745
1821
  }
1746
1822
  }
@@ -1751,11 +1827,11 @@ var htmx = (() => {
1751
1827
  return s.startsWith('<') && s.endsWith('/>') ? s.slice(1, -2) : s;
1752
1828
  }
1753
1829
 
1754
- #findAllExt(eltOrSelector, maybeSelector, global) {
1830
+ #findAllExt(eltOrSelector, maybeSelector, thisAttr, global) {
1755
1831
  let selector = maybeSelector ?? eltOrSelector;
1756
1832
  let elt = maybeSelector ? this.#normalizeElement(eltOrSelector) : document;
1757
1833
  if (selector.startsWith('global ')) {
1758
- return this.#findAllExt(elt, selector.slice(7), true);
1834
+ return this.#findAllExt(elt, selector.slice(7), thisAttr, true);
1759
1835
  }
1760
1836
  let parts = selector ? selector.replace(/<[^>]+\/>/g, m => m.replace(/,/g, '%2C'))
1761
1837
  .split(',').map(p => p.replace(/%2C/g, ',')) : [];
@@ -1767,7 +1843,9 @@ var htmx = (() => {
1767
1843
  if (selector.startsWith('closest ')) {
1768
1844
  item = elt.closest(selector.slice(8))
1769
1845
  } else if (selector.startsWith('find ')) {
1770
- item = document.querySelector(elt, selector.slice(5))
1846
+ item = elt.querySelector(selector.slice(5))
1847
+ } else if (selector.startsWith('findAll ')) {
1848
+ result.push(...elt.querySelectorAll(selector.slice(8)))
1771
1849
  } else if (selector === 'next' || selector === 'nextElementSibling') {
1772
1850
  item = elt.nextElementSibling
1773
1851
  } else if (selector.startsWith('next ')) {
@@ -1782,10 +1860,14 @@ var htmx = (() => {
1782
1860
  item = window
1783
1861
  } else if (selector === 'body') {
1784
1862
  item = document.body
1785
- } else if (selector === 'root') {
1786
- item = this.#getRootNode(elt, !!global)
1787
1863
  } else if (selector === 'host') {
1788
1864
  item = (elt.getRootNode()).host
1865
+ } else if (selector === 'this') {
1866
+ if (thisAttr) {
1867
+ result.push(...this.#findThisElements(elt, thisAttr));
1868
+ continue;
1869
+ }
1870
+ item = elt
1789
1871
  } else {
1790
1872
  unprocessedParts.push(selector)
1791
1873
  }
@@ -1801,7 +1883,7 @@ var htmx = (() => {
1801
1883
  result.push(...rootNode.querySelectorAll(standardSelector))
1802
1884
  }
1803
1885
 
1804
- return result
1886
+ return [...new Set(result)]
1805
1887
  }
1806
1888
 
1807
1889
  #scanForwardQuery(start, match, global) {
@@ -1829,8 +1911,8 @@ var htmx = (() => {
1829
1911
  }
1830
1912
  }
1831
1913
 
1832
- #findExt(eltOrSelector, selector) {
1833
- return this.#findAllExt(eltOrSelector, selector)[0]
1914
+ #findExt(eltOrSelector, selector, thisAttr) {
1915
+ return this.#findAllExt(eltOrSelector, selector, thisAttr)[0]
1834
1916
  }
1835
1917
 
1836
1918
  #extractJavascriptContent(string) {
@@ -1985,8 +2067,8 @@ var htmx = (() => {
1985
2067
  let type = newNode.nodeType;
1986
2068
 
1987
2069
  if (type === 1) {
1988
- let noMorph = this.config.morphIgnore || [];
1989
- this.#copyAttributes(oldNode, newNode, noMorph);
2070
+ if (this.config.morphSkip && oldNode.matches?.(this.config.morphSkip)) return;
2071
+ this.#copyAttributes(oldNode, newNode);
1990
2072
  if (oldNode instanceof HTMLTextAreaElement && oldNode.defaultValue != newNode.defaultValue) {
1991
2073
  oldNode.value = newNode.value;
1992
2074
  }
@@ -1995,10 +2077,13 @@ var htmx = (() => {
1995
2077
  if ((type === 8 || type === 3) && oldNode.nodeValue !== newNode.nodeValue) {
1996
2078
  oldNode.nodeValue = newNode.nodeValue;
1997
2079
  }
1998
- if (!oldNode.isEqualNode(newNode)) this.#morphChildren(ctx, oldNode, newNode);
2080
+
2081
+ let skipChildren = this.config.morphSkipChildren && oldNode.matches?.(this.config.morphSkipChildren);
2082
+ if (!skipChildren && !oldNode.isEqualNode(newNode)) this.#morphChildren(ctx, oldNode, newNode);
1999
2083
  }
2000
2084
 
2001
- #copyAttributes(destination, source, attributesToIgnore = []) {
2085
+ #copyAttributes(destination, source) {
2086
+ let attributesToIgnore = this.config.morphIgnore || [];
2002
2087
  for (const attr of source.attributes) {
2003
2088
  if (!attributesToIgnore.includes(attr.name) && destination.getAttribute(attr.name) !== attr.value) {
2004
2089
  destination.setAttribute(attr.name, attr.value);
@@ -2068,7 +2153,7 @@ var htmx = (() => {
2068
2153
  }
2069
2154
  let statusValue = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
2070
2155
  if (statusValue) {
2071
- Object.assign(ctx, this.#parseConfig(statusValue));
2156
+ this.#mergeConfig(statusValue, ctx);
2072
2157
  return;
2073
2158
  }
2074
2159
  }
@@ -2146,4 +2231,5 @@ var htmx = (() => {
2146
2231
  return new Htmx()
2147
2232
  })()
2148
2233
 
2149
- export default htmx
2234
+ if (typeof window !== "undefined") window.htmx = htmx;
2235
+ export default htmx;