htmx.org 4.0.0-alpha2 → 4.0.0-alpha3

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
@@ -90,7 +90,7 @@ var htmx = (() => {
90
90
 
91
91
  #initHtmxConfig() {
92
92
  this.config = {
93
- version: '4.0.0-alpha2',
93
+ version: '4.0.0-alpha3',
94
94
  logAll: false,
95
95
  prefix: "",
96
96
  transitions: true,
@@ -103,7 +103,7 @@ var htmx = (() => {
103
103
  includeIndicatorCSS: true,
104
104
  defaultTimeout: 60000, /* 60 second default timeout */
105
105
  extensions: '',
106
- streams: {
106
+ sse: {
107
107
  mode: 'once',
108
108
  maxRetries: Infinity,
109
109
  initialDelay: 500,
@@ -116,7 +116,8 @@ var htmx = (() => {
116
116
  }
117
117
  let metaConfig = document.querySelector('meta[name="htmx:config"]');
118
118
  if (metaConfig) {
119
- let overrides = JSON.parse(metaConfig.content);
119
+ let content = metaConfig.content;
120
+ let overrides = this.#parseConfig(content);
120
121
  // Deep merge nested config objects
121
122
  for (let key in overrides) {
122
123
  let val = overrides[key];
@@ -215,62 +216,29 @@ var htmx = (() => {
215
216
  return returnElt ? elt : defaultVal;
216
217
  }
217
218
 
218
- #tokenize(str) {
219
- let tokens = [], i = 0;
220
- while (i < str.length) {
221
- let c = str[i];
222
- if (c === '"' || c === "'") {
223
- let q = c, s = c;
224
- i++;
225
- while (i < str.length) {
226
- c = str[i];
227
- s += c;
228
- i++;
229
- if (c === '\\' && i < str.length) {
230
- s += str[i];
231
- i++;
232
- } else if (c === q) break;
233
- }
234
- tokens.push(s);
235
- } else if (/\s/.test(c)) {
236
- while (i < str.length && /\s/.test(str[i])) i++;
237
- } else if (c === ':' || c === ',') {
238
- tokens.push(c);
239
- i++;
240
- } else {
241
- let t = '';
242
- while (i < str.length && !/[\s"':,]/.test(str[i])) t += str[i++];
243
- tokens.push(t);
244
- }
245
- }
246
- return tokens;
219
+ #parseConfig(configString) {
220
+ if (configString[0] === '{') return JSON.parse(configString);
221
+ let configPattern = /([^\s,]+?)(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
222
+ 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();
225
+ if (value === 'true') value = true;
226
+ else if (value === 'false') value = false;
227
+ else if (/^\d+$/.test(value)) value = parseInt(value);
228
+ keyPath.slice(0, -1).reduce((obj, key) => obj[key] ??= {}, result)[keyPath.at(-1)] = value;
229
+ return result;
230
+ }, {});
247
231
  }
248
232
 
249
233
  #parseTriggerSpecs(spec) {
250
- let specs = []
251
- let currentSpec = null
252
- let tokens = this.#tokenize(spec);
253
- for (let i = 0; i < tokens.length; i++) {
254
- let token = tokens[i];
255
- if (token === ",") {
256
- currentSpec = null;
257
- } else if (!currentSpec) {
258
- while (token.includes("[") && !token.includes("]") && i + 1 < tokens.length) {
259
- token += tokens[++i];
260
- }
261
- if (token.includes("[") && !token.includes("]")) {
262
- throw "unterminated:" + token;
263
- }
264
- currentSpec = {name: token};
265
- specs.push(currentSpec);
266
- } else if (tokens[i + 1] === ":") {
267
- currentSpec[token] = tokens[i += 2];
268
- } else {
269
- currentSpec[token] = true;
270
- }
271
- }
272
-
273
- return specs;
234
+ return spec.split(',').map(s => {
235
+ let m = s.match(/^\s*(\S+\[[^\]]*\]|\S+)\s*(.*?)\s*$/);
236
+ if (!m || !m[1]) return null;
237
+ if (m[1].includes('[') && !m[1].includes(']')) throw "unterminated:" + m[1];
238
+ let result = m[2] ? this.#parseConfig(m[2]) : {};
239
+ result.name = m[1];
240
+ return result;
241
+ }).filter(s => s);
274
242
  }
275
243
 
276
244
  #determineMethodAndAction(elt, evt) {
@@ -309,7 +277,6 @@ var htmx = (() => {
309
277
  elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt)}
310
278
  elt.setAttribute('data-htmx-powered', 'true');
311
279
  this.#initializeTriggers(elt);
312
- this.#initializeStreamConfig(elt);
313
280
  this.#initializeAbortListener(elt)
314
281
  this.#trigger(elt, "htmx:after:init", {}, true)
315
282
  this.#trigger(elt, "load", {}, false)
@@ -335,11 +302,12 @@ var htmx = (() => {
335
302
  status: "created",
336
303
  select: this.#attributeValue(sourceElement, "hx-select"),
337
304
  selectOOB: this.#attributeValue(sourceElement, "hx-select-oob"),
338
- target: this.#attributeValue(sourceElement, "hx-target"),
305
+ target: this.#resolveTarget(sourceElement, this.#attributeValue(sourceElement, "hx-target")),
339
306
  swap: this.#attributeValue(sourceElement, "hx-swap", this.config.defaultSwap),
340
307
  push: this.#attributeValue(sourceElement, "hx-push-url"),
341
308
  replace: this.#attributeValue(sourceElement, "hx-replace-url"),
342
309
  transition: this.config.transitions,
310
+ confirm: this.#attributeValue(sourceElement, "hx-confirm"),
343
311
  request: {
344
312
  validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') ? "true" : "false"),
345
313
  action,
@@ -351,23 +319,22 @@ var htmx = (() => {
351
319
  // Apply hx-config overrides
352
320
  let configAttr = this.#attributeValue(sourceElement, "hx-config");
353
321
  if (configAttr) {
354
- let configOverrides = JSON.parse(configAttr);
355
- let requestConfig = ctx.request;
322
+ let configOverrides = this.#parseConfig(configAttr);
323
+ let req = ctx.request;
356
324
  for (let key in configOverrides) {
357
325
  if (key.startsWith('+')) {
358
326
  let actualKey = key.substring(1);
359
- if (requestConfig[actualKey] && typeof requestConfig[actualKey] === 'object') {
360
- Object.assign(requestConfig[actualKey], configOverrides[key]);
327
+ if (req[actualKey] && typeof req[actualKey] === 'object') {
328
+ Object.assign(req[actualKey], configOverrides[key]);
361
329
  } else {
362
- requestConfig[actualKey] = configOverrides[key];
330
+ req[actualKey] = configOverrides[key];
363
331
  }
364
332
  } else {
365
- requestConfig[key] = configOverrides[key];
333
+ req[key] = configOverrides[key];
366
334
  }
367
335
  }
368
- if (requestConfig.etag) {
369
- sourceElement._htmx ||= {}
370
- sourceElement._htmx.etag ||= requestConfig.etag
336
+ if (req.etag) {
337
+ (sourceElement._htmx ||= {}).etag ||= req.etag
371
338
  }
372
339
  }
373
340
  if (sourceElement._htmx?.etag) {
@@ -379,6 +346,8 @@ var htmx = (() => {
379
346
  #determineHeaders(elt) {
380
347
  let headers = {
381
348
  "HX-Request": "true",
349
+ "HX-Source": elt.id || elt.name,
350
+ "HX-Current-URL": location.href,
382
351
  "Accept": "text/html, text/event-stream"
383
352
  };
384
353
  if (this.#isBoosted(elt)) {
@@ -386,7 +355,7 @@ var htmx = (() => {
386
355
  }
387
356
  let headersAttribute = this.#attributeValue(elt, "hx-headers");
388
357
  if (headersAttribute) {
389
- Object.assign(headers, JSON.parse(headersAttribute));
358
+ Object.assign(headers, this.#parseConfig(headersAttribute));
390
359
  }
391
360
  return headers;
392
361
  }
@@ -418,13 +387,11 @@ var htmx = (() => {
418
387
 
419
388
  if (this.#shouldCancel(evt)) evt.preventDefault()
420
389
 
421
- // Resolve swap target
422
- ctx.target = this.#resolveTarget(elt, ctx.target);
423
-
424
390
  // Build request body
425
391
  let form = elt.form || elt.closest("form")
426
392
  let body = this.#collectFormData(elt, form, evt.submitter)
427
- this.#handleHxVals(elt, body)
393
+ let valsResult = this.#handleHxVals(elt, body)
394
+ if (valsResult) await valsResult // Only await if it returned a promise
428
395
  if (ctx.values) {
429
396
  for (let k in ctx.values) {
430
397
  body.delete(k);
@@ -484,21 +451,19 @@ var htmx = (() => {
484
451
  let disableElements = this.#disableElements(elt, disableSelector);
485
452
 
486
453
  try {
487
- // Confirm dialog
488
- let confirmVal = this.#attributeValue(elt, 'hx-confirm');
489
- if (confirmVal) {
490
- let js = this.#extractJavascriptContent(confirmVal);
491
- if (js) {
492
- if (!await this.#executeJavaScriptAsync(elt, {}, js, true)) {
493
- return
494
- }
495
- } else {
496
- if (!window.confirm(confirmVal)) {
497
- return;
454
+ // Handle confirmation
455
+ if (ctx.confirm) {
456
+ let issueRequest = null;
457
+ let confirmed = await new Promise(resolve => {
458
+ issueRequest = resolve;
459
+ if (this.#trigger(elt, "htmx:confirm", {ctx, issueRequest: (skip) => issueRequest?.(skip !== false)})) {
460
+ let js = this.#extractJavascriptContent(ctx.confirm);
461
+ resolve(js ? this.#executeJavaScriptAsync(elt, {}, js, true) : window.confirm(ctx.confirm));
498
462
  }
499
- }
463
+ });
464
+ if (!confirmed) return;
500
465
  }
501
-
466
+
502
467
  ctx.fetch ||= window.fetch.bind(window)
503
468
  if (!this.#trigger(elt, "htmx:before:request", {ctx})) return;
504
469
 
@@ -527,7 +492,7 @@ var htmx = (() => {
527
492
  } else {
528
493
  // HTTP response
529
494
  if (ctx.status === "issuing") {
530
- if (ctx.hx.retarget) ctx.target = this.#resolveTarget(elt, ctx.hx.retarget);
495
+ if (ctx.hx.retarget) ctx.target = ctx.hx.retarget;
531
496
  if (ctx.hx.reswap) ctx.swap = ctx.hx.reswap;
532
497
  if (ctx.hx.reselect) ctx.select = ctx.hx.reselect;
533
498
  ctx.status = "response received";
@@ -578,8 +543,8 @@ var htmx = (() => {
578
543
  }
579
544
  if (ctx.hx.location) {
580
545
  let path = ctx.hx.location, opts = {};
581
- if (path[0] === '{') {
582
- opts = JSON.parse(path);
546
+ if (path[0] === '{' || /[\s,]/.test(path)) {
547
+ opts = this.#parseConfig(path);
583
548
  path = opts.path;
584
549
  delete opts.path;
585
550
  }
@@ -594,7 +559,9 @@ var htmx = (() => {
594
559
  }
595
560
 
596
561
  async #handleSSE(ctx, elt, response) {
597
- let config = elt._htmx?.streamConfig || {...this.config.streams};
562
+ let config = {...this.config.sse, ...ctx.request.sse};
563
+ if (config.once) config.mode = 'once';
564
+ if (config.continuous) config.mode = 'continuous';
598
565
 
599
566
  let waitForVisible = () => new Promise(r => {
600
567
  let onVisible = () => !document.hidden && (document.removeEventListener('visibilitychange', onVisible), r());
@@ -613,7 +580,7 @@ var htmx = (() => {
613
580
  if (!elt.isConnected) break;
614
581
  }
615
582
 
616
- let delay = Math.min(config.initialDelay * Math.pow(2, attempt - 1), config.maxDelay);
583
+ let delay = Math.min(this.parseInterval(config.initialDelay) * Math.pow(2, attempt - 1), this.parseInterval(config.maxDelay));
617
584
  let reconnect = {attempt, delay, lastEventId, cancelled: false};
618
585
 
619
586
  ctx.status = "reconnecting to stream";
@@ -744,7 +711,7 @@ var htmx = (() => {
744
711
  #initTimeout(ctx) {
745
712
  let timeoutInterval;
746
713
  if (ctx.request.timeout) {
747
- timeoutInterval = typeof ctx.request.timeout == "string" ? this.parseInterval(ctx.request.timeout) : ctx.request.timeout;
714
+ timeoutInterval = this.parseInterval(ctx.request.timeout);
748
715
  } else {
749
716
  timeoutInterval = this.config.defaultTimeout;
750
717
  }
@@ -943,35 +910,7 @@ var htmx = (() => {
943
910
  }
944
911
  }
945
912
 
946
- #initializeStreamConfig(elt) {
947
- let streamSpec = this.#attributeValue(elt, 'hx-stream');
948
- if (!streamSpec) return;
949
-
950
- // Start with global defaults
951
- let streamConfig = {...this.config.streams};
952
- let tokens = this.#tokenize(streamSpec);
953
-
954
- for (let i = 0; i < tokens.length; i++) {
955
- let token = tokens[i];
956
- // Main value: once or continuous
957
- if (token === 'once' || token === 'continuous') {
958
- streamConfig.mode = token;
959
- } else if (token === 'pauseHidden') {
960
- streamConfig.pauseHidden = true;
961
- } else if (tokens[i + 1] === ':') {
962
- let key = token, value = tokens[i + 2];
963
- if (key === 'mode') streamConfig.mode = value;
964
- else if (key === 'maxRetries') streamConfig.maxRetries = parseInt(value);
965
- else if (key === 'initialDelay') streamConfig.initialDelay = this.parseInterval(value);
966
- else if (key === 'maxDelay') streamConfig.maxDelay = this.parseInterval(value);
967
- else if (key === 'pauseHidden') streamConfig.pauseHidden = value === 'true';
968
- i += 2;
969
- }
970
- }
971
913
 
972
- if (!elt._htmx) elt._htmx = {};
973
- elt._htmx.streamConfig = streamConfig;
974
- }
975
914
 
976
915
  #extractFilter(str) {
977
916
  let match = str.match(/^([^\[]*)\[([^\]]*)]/);
@@ -981,7 +920,7 @@ var htmx = (() => {
981
920
 
982
921
  #handleTriggerHeader(value, elt) {
983
922
  if (value[0] === '{') {
984
- let triggers = JSON.parse(value);
923
+ let triggers = this.#parseConfig(value);
985
924
  for (let name in triggers) {
986
925
  let detail = triggers[name];
987
926
  if (detail?.target) elt = this.find(detail.target) || elt;
@@ -1150,7 +1089,7 @@ var htmx = (() => {
1150
1089
  }
1151
1090
 
1152
1091
  #makeFragment(text) {
1153
- let response = text.replace(/<hx-partial(\s+|>)/gi, '<template partial$1').replace(/<\/hx-partial>/gi, '</template>');
1092
+ let response = text.replace(/<hx-([a-z]+)(\s+|>)/gi, '<template hx type="$1"$2').replace(/<\/hx-[a-z]+>/gi, '</template>');
1154
1093
  let title = '';
1155
1094
  response = response.replace(/<title[^>]*>[\s\S]*?<\/title>/i, m => (title = this.#parseHTML(m).title, ''));
1156
1095
  let responseWithNoHead = response.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i, '');
@@ -1223,52 +1162,36 @@ var htmx = (() => {
1223
1162
  }
1224
1163
 
1225
1164
  #parseSwapSpec(swapStr) {
1226
- let tokens = this.#tokenize(swapStr);
1227
- let config = {style: tokens[1] === ':' ? this.config.defaultSwap : (tokens[0] || this.config.defaultSwap)};
1228
- config.style = this.#normalizeSwapStyle(config.style);
1229
- let startIdx = tokens[1] === ':' ? 0 : 1;
1230
-
1231
- for (let i = startIdx; i < tokens.length; i++) {
1232
- if (tokens[i + 1] === ':') {
1233
- let key = tokens[i], value = tokens[i = i + 2];
1234
- if (key === 'swap') config.swapDelay = this.parseInterval(value);
1235
- else if (key === 'transition' || key === 'ignoreTitle' || key === 'strip') config[key] = value === 'true';
1236
- else if (key === 'focus-scroll') config.focusScroll = value === 'true';
1237
- else if (key === 'scroll' || key === 'show') {
1238
- let parts = [value];
1239
- while (tokens[i + 1] === ':') {
1240
- parts.push(tokens[i + 2]);
1241
- i += 2;
1242
- }
1243
- config[key] = parts.length === 1 ? parts[0] : parts.pop();
1244
- if (parts.length > 1) config[key + 'Target'] = parts.join(':');
1245
- } else if (key === 'target') {
1246
- let parts = [value];
1247
- while (i + 1 < tokens.length && tokens[i + 1] !== ':' && tokens[i + 2] !== ':') {
1248
- parts.push(tokens[i + 1]);
1249
- i++;
1250
- }
1251
- config[key] = parts.join(' ');
1252
- }
1253
- }
1165
+ swapStr = swapStr.trim();
1166
+ let style = this.config.defaultSwap
1167
+ if (swapStr && !/^\S*:/.test(swapStr)) {
1168
+ let m = swapStr.match(/^(\S+)\s*(.*)$/);
1169
+ style = m[1];
1170
+ swapStr = m[2];
1254
1171
  }
1255
- return config;
1172
+ return {style: this.#normalizeSwapStyle(style), ...this.#parseConfig(swapStr)};
1256
1173
  }
1257
1174
 
1258
- #processPartials(fragment, sourceElement) {
1175
+ #processPartials(fragment, ctx) {
1259
1176
  let tasks = [];
1260
1177
 
1261
- for (let partialElt of fragment.querySelectorAll('template[partial]')) {
1262
- let swapSpec = this.#parseSwapSpec(partialElt.getAttribute(this.#prefix('hx-swap')) || this.config.defaultSwap);
1263
-
1264
- tasks.push({
1265
- type: 'partial',
1266
- fragment: partialElt.content.cloneNode(true),
1267
- target: partialElt.getAttribute(this.#prefix('hx-target')),
1268
- swapSpec,
1269
- sourceElement
1270
- });
1271
- partialElt.remove();
1178
+ for (let templateElt of fragment.querySelectorAll('template[hx]')) {
1179
+ let type = templateElt.getAttribute('type');
1180
+
1181
+ if (type === 'partial') {
1182
+ let swapSpec = this.#parseSwapSpec(templateElt.getAttribute(this.#prefix('hx-swap')) || this.config.defaultSwap);
1183
+
1184
+ tasks.push({
1185
+ type: 'partial',
1186
+ fragment: templateElt.content.cloneNode(true),
1187
+ target: templateElt.getAttribute(this.#prefix('hx-target')),
1188
+ swapSpec,
1189
+ sourceElement: ctx.sourceElement
1190
+ });
1191
+ } else {
1192
+ this.#triggerExtensions(templateElt, 'htmx:process:' + type, { ctx, tasks });
1193
+ }
1194
+ templateElt.remove();
1272
1195
  }
1273
1196
 
1274
1197
  return tasks;
@@ -1281,30 +1204,16 @@ var htmx = (() => {
1281
1204
 
1282
1205
  #handleScroll(task) {
1283
1206
  if (task.swapSpec.scroll) {
1284
- let target;
1285
- let [selectorOrValue, value] = task.swapSpec.scroll.split(":");
1286
- if (value) {
1287
- target = this.#findExt(selectorOrValue);
1288
- } else {
1289
- target = task.target;
1290
- value = selectorOrValue
1291
- }
1292
- if (value === 'top') {
1207
+ let target = task.swapSpec.scrollTarget ? this.#findExt(task.swapSpec.scrollTarget) : task.target;
1208
+ if (task.swapSpec.scroll === 'top') {
1293
1209
  target.scrollTop = 0;
1294
- } else if (value === 'bottom'){
1210
+ } else if (task.swapSpec.scroll === 'bottom'){
1295
1211
  target.scrollTop = target.scrollHeight;
1296
1212
  }
1297
1213
  }
1298
1214
  if (task.swapSpec.show) {
1299
- let target;
1300
- let [selectorOrValue, value] = task.swapSpec.show.split(":");
1301
- if (value) {
1302
- target = this.#findExt(selectorOrValue);
1303
- } else {
1304
- target = task.target;
1305
- value = selectorOrValue
1306
- }
1307
- target.scrollIntoView(value === 'top')
1215
+ let target = task.swapSpec.showTarget ? this.#findExt(task.swapSpec.showTarget) : task.target;
1216
+ target.scrollIntoView(task.swapSpec.show === 'top')
1308
1217
  }
1309
1218
  }
1310
1219
 
@@ -1342,7 +1251,7 @@ var htmx = (() => {
1342
1251
 
1343
1252
  // Process OOB and partials
1344
1253
  let oobTasks = this.#processOOB(fragment, ctx.sourceElement, ctx.selectOOB);
1345
- let partialTasks = this.#processPartials(fragment, ctx.sourceElement);
1254
+ let partialTasks = this.#processPartials(fragment, ctx);
1346
1255
  tasks.push(...oobTasks, ...partialTasks);
1347
1256
 
1348
1257
  // Process main swap
@@ -1364,8 +1273,8 @@ var htmx = (() => {
1364
1273
 
1365
1274
  // insert non-transition tasks immediately or with delay
1366
1275
  for (let task of nonTransitionTasks) {
1367
- if (task.swapSpec?.swapDelay) {
1368
- setTimeout(() => this.#insertContent(task), task.swapSpec.swapDelay);
1276
+ if (task.swapSpec?.swap) {
1277
+ setTimeout(() => this.#insertContent(task), this.parseInterval(task.swapSpec.swap));
1369
1278
  } else {
1370
1279
  this.#insertContent(task)
1371
1280
  }
@@ -1412,7 +1321,7 @@ var htmx = (() => {
1412
1321
  let mainSwap = {
1413
1322
  type: 'main',
1414
1323
  fragment,
1415
- target: swapSpec.target || ctx.target,
1324
+ target: this.#resolveTarget(ctx.sourceElement || document.body, swapSpec.target || ctx.target),
1416
1325
  swapSpec,
1417
1326
  sourceElement: ctx.sourceElement,
1418
1327
  transition: (ctx.transition !== false) && (swapSpec.transition !== false)
@@ -1511,9 +1420,7 @@ var htmx = (() => {
1511
1420
  }
1512
1421
 
1513
1422
  timeout(time) {
1514
- if (typeof time === "string") {
1515
- time = this.parseInterval(time)
1516
- }
1423
+ time = this.parseInterval(time);
1517
1424
  if (time > 0) {
1518
1425
  return new Promise(resolve => setTimeout(resolve, time));
1519
1426
  }
@@ -1571,6 +1478,7 @@ var htmx = (() => {
1571
1478
  }
1572
1479
 
1573
1480
  parseInterval(str) {
1481
+ if (typeof str === 'number') return str;
1574
1482
  let m = {ms: 1, s: 1000, m: 60000};
1575
1483
  let [, n, u] = str?.match(/^([\d.]+)(ms|s|m)?$/) || [];
1576
1484
  let v = parseFloat(n) * (m[u] || 1);
@@ -1600,20 +1508,21 @@ var htmx = (() => {
1600
1508
  let sourceElt = typeof context.source === 'string' ?
1601
1509
  document.querySelector(context.source) : context.source;
1602
1510
 
1603
- // TODO we have a contradiction here: the tests say that we should default to the source element
1604
- // but the logic here targets the source element
1605
- let targetElt = context.target ?
1606
- this.#resolveTarget(sourceElt || document.body, context.target) : sourceElt;
1511
+ // If source selector was provided but didn't match, reject
1512
+ if (typeof context.source === 'string' && !sourceElt) {
1513
+ return Promise.reject(new Error('Source not found'));
1514
+ }
1607
1515
 
1608
- if (!targetElt) {
1516
+ // Resolve target, defaulting to body only if no source or target provided
1517
+ let target = this.#resolveTarget(document.body, context.target || sourceElt);
1518
+ if (!target) {
1609
1519
  return Promise.reject(new Error('Target not found'));
1610
1520
  }
1611
1521
 
1612
- // TODO is this logic correct?
1613
- sourceElt ||= targetElt || document.body;
1522
+ sourceElt ||= target;
1614
1523
 
1615
1524
  let ctx = this.#createRequestContext(sourceElt, context.event || {});
1616
- Object.assign(ctx, context, {target: targetElt});
1525
+ Object.assign(ctx, context, {target});
1617
1526
  Object.assign(ctx.request, {action: path, method: verb.toUpperCase()});
1618
1527
  if (context.headers) Object.assign(ctx.request.headers, context.headers);
1619
1528
 
@@ -1676,7 +1585,7 @@ var htmx = (() => {
1676
1585
  }
1677
1586
 
1678
1587
  let path = push || replace;
1679
- if (!path || path === 'false') return;
1588
+ if (!path || path === 'false' || path === false) return;
1680
1589
 
1681
1590
  if (path === 'true') {
1682
1591
  path = ctx.request.originalAction;
@@ -1766,11 +1675,9 @@ var htmx = (() => {
1766
1675
  }
1767
1676
 
1768
1677
  #collectFormData(elt, form, submitter) {
1769
- let formData = new FormData()
1770
- let included = new Set()
1771
- if (form) {
1772
- this.#addInputValues(form, included, formData)
1773
- } else if (elt.name) {
1678
+ let formData = form ? new FormData(form) : new FormData()
1679
+ let included = form ? new Set(form.elements) : new Set()
1680
+ if (!form && elt.name) {
1774
1681
  formData.append(elt.name, elt.value)
1775
1682
  included.add(elt);
1776
1683
  }
@@ -1792,21 +1699,21 @@ var htmx = (() => {
1792
1699
  let inputs = this.#queryEltAndDescendants(elt, 'input:not([disabled]), select:not([disabled]), textarea:not([disabled])');
1793
1700
 
1794
1701
  for (let input of inputs) {
1795
- // Skip elements without a name or already seen
1796
1702
  if (!input.name || included.has(input)) continue;
1797
1703
  included.add(input);
1798
1704
 
1799
- if (input.matches('input[type=checkbox], input[type=radio]')) {
1705
+ let type = input.type;
1706
+ if (type === 'checkbox' || type === 'radio') {
1800
1707
  // Only add if checked
1801
1708
  if (input.checked) {
1802
1709
  formData.append(input.name, input.value);
1803
1710
  }
1804
- } else if (input.matches('input[type=file]')) {
1711
+ } else if (type === 'file') {
1805
1712
  // Add all selected files
1806
1713
  for (let file of input.files) {
1807
1714
  formData.append(input.name, file);
1808
1715
  }
1809
- } else if (input.matches('select[multiple]')) {
1716
+ } else if (type === 'select-multiple') {
1810
1717
  // Add all selected options
1811
1718
  for (let option of input.selectedOptions) {
1812
1719
  formData.append(input.name, option.value);
@@ -1821,12 +1728,20 @@ var htmx = (() => {
1821
1728
  #handleHxVals(elt, body) {
1822
1729
  let hxValsValue = this.#attributeValue(elt, "hx-vals");
1823
1730
  if (hxValsValue) {
1824
- if (!hxValsValue.includes('{')) {
1825
- hxValsValue = `{${hxValsValue}}`
1826
- }
1827
- let obj = JSON.parse(hxValsValue);
1828
- for (let key in obj) {
1829
- body.append(key, obj[key])
1731
+ let javascriptContent = this.#extractJavascriptContent(hxValsValue);
1732
+ if (javascriptContent) {
1733
+ // Return promise for async evaluation
1734
+ return this.#executeJavaScriptAsync(elt, {}, javascriptContent, true).then(obj => {
1735
+ for (let key in obj) {
1736
+ body.append(key, obj[key])
1737
+ }
1738
+ });
1739
+ } else {
1740
+ // Synchronous path
1741
+ let obj = this.#parseConfig(hxValsValue);
1742
+ for (let key in obj) {
1743
+ body.append(key, obj[key])
1744
+ }
1830
1745
  }
1831
1746
  }
1832
1747
  }
@@ -1837,11 +1752,13 @@ var htmx = (() => {
1837
1752
  }
1838
1753
 
1839
1754
  #findAllExt(eltOrSelector, maybeSelector, global) {
1840
- let [elt, selector] = this.#normalizeElementAndSelector(eltOrSelector, maybeSelector)
1755
+ let selector = maybeSelector ?? eltOrSelector;
1756
+ let elt = maybeSelector ? this.#normalizeElement(eltOrSelector) : document;
1841
1757
  if (selector.startsWith('global ')) {
1842
1758
  return this.#findAllExt(elt, selector.slice(7), true);
1843
1759
  }
1844
- let parts = this.#tokenizeExtendedSelector(selector);
1760
+ let parts = selector ? selector.replace(/<[^>]+\/>/g, m => m.replace(/,/g, '%2C'))
1761
+ .split(',').map(p => p.replace(/%2C/g, ',')) : [];
1845
1762
  let result = []
1846
1763
  let unprocessedParts = []
1847
1764
  for (const part of parts) {
@@ -1887,28 +1804,6 @@ var htmx = (() => {
1887
1804
  return result
1888
1805
  }
1889
1806
 
1890
- #normalizeElementAndSelector(eltOrSelector, selector) {
1891
- if (selector === undefined) {
1892
- return [document, eltOrSelector];
1893
- } else {
1894
- return [this.#normalizeElement(eltOrSelector), selector];
1895
- }
1896
- }
1897
-
1898
- #tokenizeExtendedSelector(selector) {
1899
- let parts = [], depth = 0, start = 0;
1900
- for (let i = 0; i <= selector.length; i++) {
1901
- let c = selector[i];
1902
- if (c === '<') depth++;
1903
- else if (c === '/' && selector[i + 1] === '>') depth--;
1904
- else if ((c === ',' && !depth) || i === selector.length) {
1905
- if (i > start) parts.push(selector.substring(start, i));
1906
- start = i + 1;
1907
- }
1908
- }
1909
- return parts;
1910
- }
1911
-
1912
1807
  #scanForwardQuery(start, match, global) {
1913
1808
  return this.#scanUntilComparison(this.#getRootNode(start, global).querySelectorAll(match), start, Node.DOCUMENT_POSITION_PRECEDING);
1914
1809
  }
@@ -2167,13 +2062,13 @@ var htmx = (() => {
2167
2062
  let noSwapStrings = this.config.noSwap.map(x => x + "");
2168
2063
  let str = status + ""
2169
2064
  for (let pattern of [str, str.slice(0, 2) + 'x', str[0] + 'xx']) {
2170
- let swap = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
2171
2065
  if (noSwapStrings.includes(pattern)) {
2172
2066
  ctx.swap = "none";
2173
2067
  return
2174
2068
  }
2175
- if (swap) {
2176
- ctx.swap = swap;
2069
+ let statusValue = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
2070
+ if (statusValue) {
2071
+ Object.assign(ctx, this.#parseConfig(statusValue));
2177
2072
  return;
2178
2073
  }
2179
2074
  }