htmx.org 4.0.0-alpha4 → 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",
@@ -115,19 +115,9 @@ var htmx = (() => {
115
115
  noSwap: [204, 304],
116
116
  implicitInheritance: false
117
117
  }
118
- let metaConfig = document.querySelector('meta[name="htmx:config"]');
118
+ let metaConfig = document.querySelector('meta[name="htmx-config"]');
119
119
  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
- }
120
+ this.#mergeConfig(metaConfig.content, this.config);
131
121
  }
132
122
  this.#approvedExt = this.config.extensions;
133
123
  }
@@ -181,48 +171,60 @@ var htmx = (() => {
181
171
  style === 'append' ? 'beforeend' : style;
182
172
  }
183
173
 
184
- #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) {
185
183
  name = this.#prefix(name);
186
184
  let appendName = name + this.#maybeAdjustMetaCharacter(":append");
187
185
  let inheritName = name + (this.config.implicitInheritance ? "" : this.#maybeAdjustMetaCharacter(":inherited"));
188
186
  let inheritAppendName = name + this.#maybeAdjustMetaCharacter(":inherited:append");
189
187
 
190
188
  if (elt.hasAttribute(name)) {
191
- return returnElt ? elt : elt.getAttribute(name);
189
+ let val = elt.getAttribute(name);
190
+ return eltCollector ? eltCollector(val, elt) : val;
192
191
  }
193
192
 
194
193
  if (elt.hasAttribute(inheritName)) {
195
- return returnElt ? elt : elt.getAttribute(inheritName);
194
+ let val = elt.getAttribute(inheritName);
195
+ return eltCollector ? eltCollector(val, elt) : val;
196
196
  }
197
197
 
198
198
  if (elt.hasAttribute(appendName) || elt.hasAttribute(inheritAppendName)) {
199
199
  let appendValue = elt.getAttribute(appendName) || elt.getAttribute(inheritAppendName);
200
200
  let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
201
+ if (eltCollector) {
202
+ eltCollector(appendValue, elt);
203
+ }
201
204
  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;
205
+ let inherited = this.#attributeValue(parent, name, undefined, eltCollector);
206
+ return inherited ? (inherited + "," + appendValue).replace(/[{}]/g, '') : appendValue;
206
207
  }
208
+ return appendValue;
207
209
  }
208
210
 
209
211
  let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
210
212
  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})
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})
214
216
  }
215
217
  return val;
216
218
  }
217
- return returnElt ? elt : defaultVal;
219
+ return defaultVal;
218
220
  }
219
221
 
220
222
  #parseConfig(configString) {
221
223
  if (configString[0] === '{') return JSON.parse(configString);
222
- let configPattern = /([^\s,]+?)(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
224
+ let configPattern = /(?:"([^"]+)"|([^\s,:]+))(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
223
225
  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();
226
+ let keyPath = (match[1] ?? match[2]).split('.');
227
+ let value = (match[3] ?? match[4] ?? match[5] ?? match[6] ?? 'true').trim();
226
228
  if (value === 'true') value = true;
227
229
  else if (value === 'false') value = false;
228
230
  else if (/^\d+$/.test(value)) value = parseInt(value);
@@ -231,6 +233,19 @@ var htmx = (() => {
231
233
  }, {});
232
234
  }
233
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
+
234
249
  #parseTriggerSpecs(spec) {
235
250
  return spec.split(',').map(s => {
236
251
  let m = s.match(/^\s*(\S+\[[^\]]*\]|\S+)\s*(.*?)\s*$/);
@@ -305,44 +320,36 @@ var htmx = (() => {
305
320
  status: "created",
306
321
  select: this.#attributeValue(sourceElement, "hx-select"),
307
322
  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),
323
+ target: this.#attributeValue(sourceElement, "hx-target"),
324
+ swap: this.#attributeValue(sourceElement, "hx-swap") ?? this.config.defaultSwap,
310
325
  push: this.#attributeValue(sourceElement, "hx-push-url"),
311
326
  replace: this.#attributeValue(sourceElement, "hx-replace-url"),
312
327
  transition: this.config.transitions,
313
328
  confirm: this.#attributeValue(sourceElement, "hx-confirm"),
314
329
  request: {
315
- validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') ? "true" : "false"),
330
+ validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') && !sourceElement.noValidate && !sourceEvent.submitter?.formNoValidate ? "true" : "false"),
316
331
  action: fullAction,
317
332
  anchor,
318
333
  method,
319
- headers: this.#determineHeaders(sourceElement),
334
+ headers: this.#createCoreHeaders(sourceElement),
320
335
  abort: ac.abort.bind(ac),
321
336
  credentials: "same-origin",
322
337
  signal: ac.signal,
323
338
  mode: this.config.mode
324
339
  }
325
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);
326
346
 
327
347
  // Apply hx-config overrides
328
348
  let configAttr = this.#attributeValue(sourceElement, "hx-config");
329
349
  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
350
+ this.#mergeConfig(configAttr, ctx.request);
351
+ if (ctx.request.etag) {
352
+ (sourceElement._htmx ||= {}).etag ||= ctx.request.etag
346
353
  }
347
354
  }
348
355
  if (sourceElement._htmx?.etag) {
@@ -351,30 +358,45 @@ var htmx = (() => {
351
358
  return ctx;
352
359
  }
353
360
 
354
- #determineHeaders(elt) {
361
+ #buildIdentifier(elt) {
362
+ return `${elt.tagName.toLowerCase()}${elt.id ? '#' + elt.id : ''}`;
363
+ }
364
+
365
+ #createCoreHeaders(elt) {
355
366
  let headers = {
356
367
  "HX-Request": "true",
357
- "HX-Source": elt.id || elt.name,
368
+ "HX-Source": this.#buildIdentifier(elt),
358
369
  "HX-Current-URL": location.href,
359
370
  "Accept": "text/html, text/event-stream"
360
371
  };
361
372
  if (this.#isBoosted(elt)) {
362
373
  headers["HX-Boosted"] = "true"
363
374
  }
364
- let headersAttribute = this.#attributeValue(elt, "hx-headers");
365
- if (headersAttribute) {
366
- Object.assign(headers, this.#parseConfig(headersAttribute));
367
- }
368
375
  return headers;
369
376
  }
370
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
+
371
395
  #resolveTarget(elt, selector) {
372
396
  if (selector instanceof Element) {
373
397
  return selector;
374
- } else if (selector === 'this') {
375
- return this.#attributeValue(elt, "hx-target", undefined, true);
376
398
  } else if (selector != null) {
377
- return this.find(elt, selector);
399
+ return this.#findExt(elt, selector, "hx-target");
378
400
  } else if (this.#isBoosted(elt)) {
379
401
  return document.body
380
402
  } else {
@@ -397,7 +419,8 @@ var htmx = (() => {
397
419
 
398
420
  // Build request body
399
421
  let form = elt.form || elt.closest("form")
400
- 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
401
424
  let valsResult = this.#handleHxVals(elt, body)
402
425
  if (valsResult) await valsResult // Only await if it returned a promise
403
426
  if (ctx.values) {
@@ -407,6 +430,16 @@ var htmx = (() => {
407
430
  }
408
431
  }
409
432
 
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
+
410
443
  // Setup event-dependent request details
411
444
  Object.assign(ctx.request, {
412
445
  form,
@@ -416,7 +449,6 @@ var htmx = (() => {
416
449
 
417
450
  if (!this.#trigger(elt, "htmx:config:request", {ctx: ctx})) return
418
451
  if (!this.#verbs.includes(ctx.request.method.toLowerCase())) return
419
- if (ctx.request.validate && ctx.request.form && !ctx.request.form.reportValidity()) return
420
452
 
421
453
  let javascriptContent = this.#extractJavascriptContent(ctx.request.action);
422
454
  if (javascriptContent != null) {
@@ -452,10 +484,8 @@ var htmx = (() => {
452
484
  ctx.status = "issuing"
453
485
  this.#initTimeout(ctx);
454
486
 
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);
487
+ let indicators = this.#showIndicators(elt);
488
+ let disableElements = this.#disableElements(elt);
459
489
 
460
490
  try {
461
491
  // Handle confirmation
@@ -1001,8 +1031,9 @@ var htmx = (() => {
1001
1031
  }
1002
1032
 
1003
1033
  #maybeBoost(elt) {
1004
- if (this.#attributeValue(elt, "hx-boost") === "true" && this.#shouldBoost(elt)) {
1005
- 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}
1006
1037
  elt.setAttribute('data-htmx-powered', 'true');
1007
1038
  if (elt.matches('a') && !elt.hasAttribute("target")) {
1008
1039
  elt.addEventListener('click', (click) => {
@@ -1187,13 +1218,11 @@ var htmx = (() => {
1187
1218
  let type = templateElt.getAttribute('type');
1188
1219
 
1189
1220
  if (type === 'partial') {
1190
- let swapSpec = this.#parseSwapSpec(templateElt.getAttribute(this.#prefix('hx-swap')) || this.config.defaultSwap);
1191
-
1192
1221
  tasks.push({
1193
1222
  type: 'partial',
1194
1223
  fragment: templateElt.content.cloneNode(true),
1195
1224
  target: templateElt.getAttribute(this.#prefix('hx-target')),
1196
- swapSpec,
1225
+ swapSpec: this.#parseSwapSpec(templateElt.getAttribute(this.#prefix('hx-swap')) || this.config.defaultSwap),
1197
1226
  sourceElement: ctx.sourceElement
1198
1227
  });
1199
1228
  } else {
@@ -1210,18 +1239,18 @@ var htmx = (() => {
1210
1239
  autofocus?.focus?.()
1211
1240
  }
1212
1241
 
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;
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;
1220
1249
  }
1221
1250
  }
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')
1251
+ if (swapSpec.show) {
1252
+ let showTarget = swapSpec.showTarget ? this.#findExt(swapSpec.showTarget) : target;
1253
+ showTarget.scrollIntoView(swapSpec.show === 'top')
1225
1254
  }
1226
1255
  }
1227
1256
 
@@ -1270,25 +1299,35 @@ var htmx = (() => {
1270
1299
  // TODO - can we remove this and just let the function complete?
1271
1300
  if (tasks.length === 0) return;
1272
1301
 
1273
- // Separate transition/nonTransition tasks
1274
- let transitionTasks = tasks.filter(t => t.transition);
1275
- let nonTransitionTasks = tasks.filter(t => !t.transition);
1276
-
1277
1302
  if(!this.#trigger(document, "htmx:before:swap", {ctx, tasks})){
1278
1303
  return
1279
1304
  }
1280
1305
 
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));
1285
- } 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
+ }
1286
1320
  this.#insertContent(task)
1321
+ } else {
1322
+ transitionTasks.push(task);
1287
1323
  }
1288
1324
  }
1289
1325
 
1290
1326
  // insert transition tasks in the transition queue
1291
1327
  if (transitionTasks.length > 0) {
1328
+ if (mainSwap?.transition && mainSwap?.swapSpec?.swap) {
1329
+ await this.timeout(mainSwap.swapSpec.swap);
1330
+ }
1292
1331
  let tasksWrapper = ()=> {
1293
1332
  for (let task of transitionTasks) {
1294
1333
  this.#insertContent(task)
@@ -1316,7 +1355,7 @@ var htmx = (() => {
1316
1355
  // Create main task if needed
1317
1356
  let swapSpec = this.#parseSwapSpec(ctx.swap || this.config.defaultSwap);
1318
1357
  // 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) {
1358
+ if (swapSpec.style === 'delete' || fragment.childElementCount > 0 || /\S/.test(fragment.textContent) || !partialTasks.length) {
1320
1359
  if (ctx.select) {
1321
1360
  let selected = fragment.querySelectorAll(ctx.select);
1322
1361
  fragment = document.createDocumentFragment();
@@ -1343,8 +1382,10 @@ var htmx = (() => {
1343
1382
  target = document.querySelector(target);
1344
1383
  }
1345
1384
  if (!target) return;
1385
+ if (typeof swapSpec === 'string') {
1386
+ swapSpec = this.#parseSwapSpec(swapSpec);
1387
+ }
1346
1388
  if (swapSpec.strip && fragment.firstElementChild) {
1347
- task.unstripped = fragment;
1348
1389
  fragment = document.createDocumentFragment();
1349
1390
  fragment.append(...(task.fragment.firstElementChild.content || task.fragment.firstElementChild).childNodes);
1350
1391
  }
@@ -1390,17 +1431,24 @@ var htmx = (() => {
1390
1431
  } else if (swapSpec.style === 'none') {
1391
1432
  return;
1392
1433
  } 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}`);
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
+ }
1397
1445
  }
1398
1446
  this.#restorePreservedElements(pantry);
1399
1447
  for (const elt of newContent) {
1400
1448
  this.process(elt);
1401
1449
  this.#handleAutoFocus(elt);
1402
1450
  }
1403
- this.#handleScroll(task);
1451
+ this.#handleScroll(swapSpec, target);
1404
1452
  }
1405
1453
 
1406
1454
  #trigger(on, eventName, detail = {}, bubbles = true) {
@@ -1456,7 +1504,7 @@ var htmx = (() => {
1456
1504
  }
1457
1505
 
1458
1506
  takeClass(element, className, container = element.parentElement) {
1459
- for (let elt of this.findAll(this.#normalizeElement(container), "." + className)) {
1507
+ for (let elt of this.#findAllExt(this.#normalizeElement(container), "." + className)) {
1460
1508
  elt.classList.remove(className);
1461
1509
  }
1462
1510
  element.classList.add(className);
@@ -1567,7 +1615,7 @@ var htmx = (() => {
1567
1615
  #restoreHistory(path) {
1568
1616
  path = path || location.pathname + location.search;
1569
1617
  if (this.#trigger(document, "htmx:before:restore:history", {path, cacheMiss: true})) {
1570
- if (this.config.historyReload) {
1618
+ if (this.config.history === "reload") {
1571
1619
  location.reload();
1572
1620
  } else {
1573
1621
  this.ajax('GET', path, {
@@ -1631,15 +1679,18 @@ var htmx = (() => {
1631
1679
  }
1632
1680
  }
1633
1681
 
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
- }
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)
1643
1694
  }
1644
1695
  return indicatorElements
1645
1696
  }
@@ -1656,10 +1707,11 @@ var htmx = (() => {
1656
1707
  }
1657
1708
  }
1658
1709
 
1659
- #disableElements(elt, disabledSelector) {
1710
+ #disableElements(elt) {
1711
+ let disabledSelector = this.#attributeValue(elt, "hx-disable");
1660
1712
  let disabledElements = []
1661
1713
  if (disabledSelector) {
1662
- disabledElements = this.#queryEltAndDescendants(elt, disabledSelector);
1714
+ disabledElements = this.#findAllExt(elt, disabledSelector, "hx-disable");
1663
1715
  for (let indicator of disabledElements) {
1664
1716
  indicator._htmxDisableCount ||= 0
1665
1717
  indicator._htmxDisableCount++
@@ -1681,10 +1733,13 @@ var htmx = (() => {
1681
1733
  }
1682
1734
  }
1683
1735
 
1684
- #collectFormData(elt, form, submitter) {
1736
+ #collectFormData(elt, form, submitter, validate) {
1737
+ if (validate && form && !form.reportValidity()) return
1738
+
1685
1739
  let formData = form ? new FormData(form) : new FormData()
1686
1740
  let included = form ? new Set(form.elements) : new Set()
1687
1741
  if (!form && elt.name) {
1742
+ if (validate && elt.reportValidity && !elt.reportValidity()) return
1688
1743
  formData.append(elt.name, elt.value)
1689
1744
  included.add(elt);
1690
1745
  }
@@ -1694,8 +1749,8 @@ var htmx = (() => {
1694
1749
  }
1695
1750
  let includeSelector = this.#attributeValue(elt, "hx-include");
1696
1751
  if (includeSelector) {
1697
- let includeNodes = this.#findAllExt(elt, includeSelector);
1698
- for (let node of includeNodes) {
1752
+ for (let node of this.#findAllExt(elt, includeSelector)) {
1753
+ if (validate && node.reportValidity && !node.reportValidity()) return
1699
1754
  this.#addInputValues(node, included, formData);
1700
1755
  }
1701
1756
  }
@@ -1732,22 +1787,36 @@ var htmx = (() => {
1732
1787
  }
1733
1788
  }
1734
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
+
1735
1808
  #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 => {
1809
+ let result = this.#getAttributeObject(elt, "hx-vals");
1810
+ if (result) {
1811
+ if (result instanceof Promise) {
1812
+ return result.then(obj => {
1742
1813
  for (let key in obj) {
1743
- body.append(key, obj[key])
1814
+ body.set(key, obj[key])
1744
1815
  }
1745
1816
  });
1746
1817
  } else {
1747
- // Synchronous path
1748
- let obj = this.#parseConfig(hxValsValue);
1749
- for (let key in obj) {
1750
- body.append(key, obj[key])
1818
+ for (let key in result) {
1819
+ body.set(key, result[key])
1751
1820
  }
1752
1821
  }
1753
1822
  }
@@ -1758,11 +1827,11 @@ var htmx = (() => {
1758
1827
  return s.startsWith('<') && s.endsWith('/>') ? s.slice(1, -2) : s;
1759
1828
  }
1760
1829
 
1761
- #findAllExt(eltOrSelector, maybeSelector, global) {
1830
+ #findAllExt(eltOrSelector, maybeSelector, thisAttr, global) {
1762
1831
  let selector = maybeSelector ?? eltOrSelector;
1763
1832
  let elt = maybeSelector ? this.#normalizeElement(eltOrSelector) : document;
1764
1833
  if (selector.startsWith('global ')) {
1765
- return this.#findAllExt(elt, selector.slice(7), true);
1834
+ return this.#findAllExt(elt, selector.slice(7), thisAttr, true);
1766
1835
  }
1767
1836
  let parts = selector ? selector.replace(/<[^>]+\/>/g, m => m.replace(/,/g, '%2C'))
1768
1837
  .split(',').map(p => p.replace(/%2C/g, ',')) : [];
@@ -1774,7 +1843,9 @@ var htmx = (() => {
1774
1843
  if (selector.startsWith('closest ')) {
1775
1844
  item = elt.closest(selector.slice(8))
1776
1845
  } else if (selector.startsWith('find ')) {
1777
- 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)))
1778
1849
  } else if (selector === 'next' || selector === 'nextElementSibling') {
1779
1850
  item = elt.nextElementSibling
1780
1851
  } else if (selector.startsWith('next ')) {
@@ -1789,10 +1860,14 @@ var htmx = (() => {
1789
1860
  item = window
1790
1861
  } else if (selector === 'body') {
1791
1862
  item = document.body
1792
- } else if (selector === 'root') {
1793
- item = this.#getRootNode(elt, !!global)
1794
1863
  } else if (selector === 'host') {
1795
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
1796
1871
  } else {
1797
1872
  unprocessedParts.push(selector)
1798
1873
  }
@@ -1808,7 +1883,7 @@ var htmx = (() => {
1808
1883
  result.push(...rootNode.querySelectorAll(standardSelector))
1809
1884
  }
1810
1885
 
1811
- return result
1886
+ return [...new Set(result)]
1812
1887
  }
1813
1888
 
1814
1889
  #scanForwardQuery(start, match, global) {
@@ -1836,8 +1911,8 @@ var htmx = (() => {
1836
1911
  }
1837
1912
  }
1838
1913
 
1839
- #findExt(eltOrSelector, selector) {
1840
- return this.#findAllExt(eltOrSelector, selector)[0]
1914
+ #findExt(eltOrSelector, selector, thisAttr) {
1915
+ return this.#findAllExt(eltOrSelector, selector, thisAttr)[0]
1841
1916
  }
1842
1917
 
1843
1918
  #extractJavascriptContent(string) {
@@ -1992,8 +2067,8 @@ var htmx = (() => {
1992
2067
  let type = newNode.nodeType;
1993
2068
 
1994
2069
  if (type === 1) {
1995
- let noMorph = this.config.morphIgnore || [];
1996
- this.#copyAttributes(oldNode, newNode, noMorph);
2070
+ if (this.config.morphSkip && oldNode.matches?.(this.config.morphSkip)) return;
2071
+ this.#copyAttributes(oldNode, newNode);
1997
2072
  if (oldNode instanceof HTMLTextAreaElement && oldNode.defaultValue != newNode.defaultValue) {
1998
2073
  oldNode.value = newNode.value;
1999
2074
  }
@@ -2002,10 +2077,13 @@ var htmx = (() => {
2002
2077
  if ((type === 8 || type === 3) && oldNode.nodeValue !== newNode.nodeValue) {
2003
2078
  oldNode.nodeValue = newNode.nodeValue;
2004
2079
  }
2005
- 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);
2006
2083
  }
2007
2084
 
2008
- #copyAttributes(destination, source, attributesToIgnore = []) {
2085
+ #copyAttributes(destination, source) {
2086
+ let attributesToIgnore = this.config.morphIgnore || [];
2009
2087
  for (const attr of source.attributes) {
2010
2088
  if (!attributesToIgnore.includes(attr.name) && destination.getAttribute(attr.name) !== attr.value) {
2011
2089
  destination.setAttribute(attr.name, attr.value);
@@ -2075,7 +2153,7 @@ var htmx = (() => {
2075
2153
  }
2076
2154
  let statusValue = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
2077
2155
  if (statusValue) {
2078
- Object.assign(ctx, this.#parseConfig(statusValue));
2156
+ this.#mergeConfig(statusValue, ctx);
2079
2157
  return;
2080
2158
  }
2081
2159
  }