htmx.org 4.0.0-alpha2 → 4.0.0-alpha4

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
@@ -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,12 +103,13 @@ var htmx = (() => {
103
103
  includeIndicatorCSS: true,
104
104
  defaultTimeout: 60000, /* 60 second default timeout */
105
105
  extensions: '',
106
- streams: {
107
- mode: 'once',
108
- maxRetries: Infinity,
109
- initialDelay: 500,
110
- maxDelay: 30000,
111
- pauseHidden: false
106
+ sse: {
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],
@@ -116,7 +117,8 @@ var htmx = (() => {
116
117
  }
117
118
  let metaConfig = document.querySelector('meta[name="htmx:config"]');
118
119
  if (metaConfig) {
119
- let overrides = JSON.parse(metaConfig.content);
120
+ let content = metaConfig.content;
121
+ let overrides = this.#parseConfig(content);
120
122
  // Deep merge nested config objects
121
123
  for (let key in overrides) {
122
124
  let val = overrides[key];
@@ -146,7 +148,7 @@ var htmx = (() => {
146
148
  }
147
149
  }
148
150
 
149
- defineExtension(name, extension) {
151
+ registerExtension(name, extension) {
150
152
  if (this.#approvedExt && !this.#approvedExt.split(/,\s*/).includes(name)) return false;
151
153
  if (this.#registeredExt.has(name)) return false;
152
154
  this.#registeredExt.add(name);
@@ -215,62 +217,29 @@ var htmx = (() => {
215
217
  return returnElt ? elt : defaultVal;
216
218
  }
217
219
 
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;
220
+ #parseConfig(configString) {
221
+ if (configString[0] === '{') return JSON.parse(configString);
222
+ let configPattern = /([^\s,]+?)(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<([^>]+)\/>|([^\s,]+)))?(?=\s|,|$)/g;
223
+ 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
+ if (value === 'true') value = true;
227
+ else if (value === 'false') value = false;
228
+ else if (/^\d+$/.test(value)) value = parseInt(value);
229
+ keyPath.slice(0, -1).reduce((obj, key) => obj[key] ??= {}, result)[keyPath.at(-1)] = value;
230
+ return result;
231
+ }, {});
247
232
  }
248
233
 
249
234
  #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;
235
+ return spec.split(',').map(s => {
236
+ let m = s.match(/^\s*(\S+\[[^\]]*\]|\S+)\s*(.*?)\s*$/);
237
+ if (!m || !m[1]) return null;
238
+ if (m[1].includes('[') && !m[1].includes(']')) throw "unterminated:" + m[1];
239
+ let result = m[2] ? this.#parseConfig(m[2]) : {};
240
+ result.name = m[1];
241
+ return result;
242
+ }).filter(s => s);
274
243
  }
275
244
 
276
245
  #determineMethodAndAction(elt, evt) {
@@ -309,7 +278,6 @@ var htmx = (() => {
309
278
  elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt)}
310
279
  elt.setAttribute('data-htmx-powered', 'true');
311
280
  this.#initializeTriggers(elt);
312
- this.#initializeStreamConfig(elt);
313
281
  this.#initializeAbortListener(elt)
314
282
  this.#trigger(elt, "htmx:after:init", {}, true)
315
283
  this.#trigger(elt, "load", {}, false)
@@ -329,45 +297,52 @@ var htmx = (() => {
329
297
 
330
298
  #createRequestContext(sourceElement, sourceEvent) {
331
299
  let {action, method} = this.#determineMethodAndAction(sourceElement, sourceEvent);
300
+ let [fullAction, anchor] = (action || '').split('#');
301
+ let ac = new AbortController();
332
302
  let ctx = {
333
303
  sourceElement,
334
304
  sourceEvent,
335
305
  status: "created",
336
306
  select: this.#attributeValue(sourceElement, "hx-select"),
337
307
  selectOOB: this.#attributeValue(sourceElement, "hx-select-oob"),
338
- target: this.#attributeValue(sourceElement, "hx-target"),
308
+ target: this.#resolveTarget(sourceElement, this.#attributeValue(sourceElement, "hx-target")),
339
309
  swap: this.#attributeValue(sourceElement, "hx-swap", this.config.defaultSwap),
340
310
  push: this.#attributeValue(sourceElement, "hx-push-url"),
341
311
  replace: this.#attributeValue(sourceElement, "hx-replace-url"),
342
312
  transition: this.config.transitions,
313
+ confirm: this.#attributeValue(sourceElement, "hx-confirm"),
343
314
  request: {
344
315
  validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') ? "true" : "false"),
345
- action,
316
+ action: fullAction,
317
+ anchor,
346
318
  method,
347
- headers: this.#determineHeaders(sourceElement)
319
+ headers: this.#determineHeaders(sourceElement),
320
+ abort: ac.abort.bind(ac),
321
+ credentials: "same-origin",
322
+ signal: ac.signal,
323
+ mode: this.config.mode
348
324
  }
349
325
  };
350
326
 
351
327
  // Apply hx-config overrides
352
328
  let configAttr = this.#attributeValue(sourceElement, "hx-config");
353
329
  if (configAttr) {
354
- let configOverrides = JSON.parse(configAttr);
355
- let requestConfig = ctx.request;
330
+ let configOverrides = this.#parseConfig(configAttr);
331
+ let req = ctx.request;
356
332
  for (let key in configOverrides) {
357
333
  if (key.startsWith('+')) {
358
334
  let actualKey = key.substring(1);
359
- if (requestConfig[actualKey] && typeof requestConfig[actualKey] === 'object') {
360
- Object.assign(requestConfig[actualKey], configOverrides[key]);
335
+ if (req[actualKey] && typeof req[actualKey] === 'object') {
336
+ Object.assign(req[actualKey], configOverrides[key]);
361
337
  } else {
362
- requestConfig[actualKey] = configOverrides[key];
338
+ req[actualKey] = configOverrides[key];
363
339
  }
364
340
  } else {
365
- requestConfig[key] = configOverrides[key];
341
+ req[key] = configOverrides[key];
366
342
  }
367
343
  }
368
- if (requestConfig.etag) {
369
- sourceElement._htmx ||= {}
370
- sourceElement._htmx.etag ||= requestConfig.etag
344
+ if (req.etag) {
345
+ (sourceElement._htmx ||= {}).etag ||= req.etag
371
346
  }
372
347
  }
373
348
  if (sourceElement._htmx?.etag) {
@@ -379,6 +354,8 @@ var htmx = (() => {
379
354
  #determineHeaders(elt) {
380
355
  let headers = {
381
356
  "HX-Request": "true",
357
+ "HX-Source": elt.id || elt.name,
358
+ "HX-Current-URL": location.href,
382
359
  "Accept": "text/html, text/event-stream"
383
360
  };
384
361
  if (this.#isBoosted(elt)) {
@@ -386,7 +363,7 @@ var htmx = (() => {
386
363
  }
387
364
  let headersAttribute = this.#attributeValue(elt, "hx-headers");
388
365
  if (headersAttribute) {
389
- Object.assign(headers, JSON.parse(headersAttribute));
366
+ Object.assign(headers, this.#parseConfig(headersAttribute));
390
367
  }
391
368
  return headers;
392
369
  }
@@ -418,13 +395,11 @@ var htmx = (() => {
418
395
 
419
396
  if (this.#shouldCancel(evt)) evt.preventDefault()
420
397
 
421
- // Resolve swap target
422
- ctx.target = this.#resolveTarget(elt, ctx.target);
423
-
424
398
  // Build request body
425
399
  let form = elt.form || elt.closest("form")
426
400
  let body = this.#collectFormData(elt, form, evt.submitter)
427
- this.#handleHxVals(elt, body)
401
+ let valsResult = this.#handleHxVals(elt, body)
402
+ if (valsResult) await valsResult // Only await if it returned a promise
428
403
  if (ctx.values) {
429
404
  for (let k in ctx.values) {
430
405
  body.delete(k);
@@ -432,20 +407,11 @@ var htmx = (() => {
432
407
  }
433
408
  }
434
409
 
435
- // Setup abort controller and action
436
- let ac = new AbortController()
437
- let action = ctx.request.action.replace?.(/#.*$/, '')
438
- // TODO - consider how this works with hx-config, move most to #createRequestContext?
410
+ // Setup event-dependent request details
439
411
  Object.assign(ctx.request, {
440
- originalAction: ctx.request.action,
441
- action,
442
412
  form,
443
413
  submitter: evt.submitter,
444
- abort: ac.abort.bind(ac),
445
- body,
446
- credentials: "same-origin",
447
- signal: ac.signal,
448
- mode: this.config.mode
414
+ body
449
415
  })
450
416
 
451
417
  if (!this.#trigger(elt, "htmx:config:request", {ctx: ctx})) return
@@ -453,14 +419,22 @@ var htmx = (() => {
453
419
  if (ctx.request.validate && ctx.request.form && !ctx.request.form.reportValidity()) return
454
420
 
455
421
  let javascriptContent = this.#extractJavascriptContent(ctx.request.action);
456
- if (javascriptContent) {
422
+ if (javascriptContent != null) {
457
423
  let data = Object.fromEntries(ctx.request.body);
458
424
  await this.#executeJavaScriptAsync(ctx.sourceElement, data, javascriptContent, false);
459
425
  return
460
426
  } else if (/GET|DELETE/.test(ctx.request.method)) {
461
- let params = new URLSearchParams(ctx.request.body);
462
- if (params.size) ctx.request.action += (/\?/.test(ctx.request.action) ? "&" : "?") + params
463
- ctx.request.body = null
427
+ let url = new URL(ctx.request.action, document.baseURI);
428
+
429
+ for (let key of ctx.request.body.keys()) {
430
+ url.searchParams.delete(key);
431
+ }
432
+ for (let [key, value] of ctx.request.body) {
433
+ url.searchParams.append(key, value);
434
+ }
435
+
436
+ ctx.request.action = url.pathname + url.search;
437
+ ctx.request.body = null;
464
438
  } else if (this.#attributeValue(elt, "hx-encoding") !== "multipart/form-data") {
465
439
  ctx.request.body = new URLSearchParams(ctx.request.body);
466
440
  }
@@ -484,21 +458,19 @@ var htmx = (() => {
484
458
  let disableElements = this.#disableElements(elt, disableSelector);
485
459
 
486
460
  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
461
+ // Handle confirmation
462
+ if (ctx.confirm) {
463
+ let issueRequest = null;
464
+ let confirmed = await new Promise(resolve => {
465
+ issueRequest = resolve;
466
+ if (this.#trigger(elt, "htmx:confirm", {ctx, issueRequest: (skip) => issueRequest?.(skip !== false)})) {
467
+ let js = this.#extractJavascriptContent(ctx.confirm);
468
+ resolve(js ? this.#executeJavaScriptAsync(elt, {}, js, true) : window.confirm(ctx.confirm));
494
469
  }
495
- } else {
496
- if (!window.confirm(confirmVal)) {
497
- return;
498
- }
499
- }
470
+ });
471
+ if (!confirmed) return;
500
472
  }
501
-
473
+
502
474
  ctx.fetch ||= window.fetch.bind(window)
503
475
  if (!this.#trigger(elt, "htmx:before:request", {ctx})) return;
504
476
 
@@ -527,7 +499,7 @@ var htmx = (() => {
527
499
  } else {
528
500
  // HTTP response
529
501
  if (ctx.status === "issuing") {
530
- if (ctx.hx.retarget) ctx.target = this.#resolveTarget(elt, ctx.hx.retarget);
502
+ if (ctx.hx.retarget) ctx.target = ctx.hx.retarget;
531
503
  if (ctx.hx.reswap) ctx.swap = ctx.hx.reswap;
532
504
  if (ctx.hx.reselect) ctx.select = ctx.hx.reselect;
533
505
  ctx.status = "response received";
@@ -578,8 +550,8 @@ var htmx = (() => {
578
550
  }
579
551
  if (ctx.hx.location) {
580
552
  let path = ctx.hx.location, opts = {};
581
- if (path[0] === '{') {
582
- opts = JSON.parse(path);
553
+ if (path[0] === '{' || /[\s,]/.test(path)) {
554
+ opts = this.#parseConfig(path);
583
555
  path = opts.path;
584
556
  delete opts.path;
585
557
  }
@@ -594,7 +566,7 @@ var htmx = (() => {
594
566
  }
595
567
 
596
568
  async #handleSSE(ctx, elt, response) {
597
- let config = elt._htmx?.streamConfig || {...this.config.streams};
569
+ let config = {...this.config.sse, ...ctx.request.sse}
598
570
 
599
571
  let waitForVisible = () => new Promise(r => {
600
572
  let onVisible = () => !document.hidden && (document.removeEventListener('visibilitychange', onVisible), r());
@@ -606,14 +578,19 @@ var htmx = (() => {
606
578
  while (elt.isConnected) {
607
579
  // Handle reconnection for subsequent iterations
608
580
  if (attempt > 0) {
609
- if (config.mode !== 'continuous' || attempt > config.maxRetries) break;
581
+ if (!config.reconnect || attempt > config.reconnectMaxAttempts) break;
610
582
 
611
- if (config.pauseHidden && document.hidden) {
583
+ if (config.pauseInBackground && document.hidden) {
612
584
  await waitForVisible();
613
585
  if (!elt.isConnected) break;
614
586
  }
615
587
 
616
- let delay = Math.min(config.initialDelay * Math.pow(2, attempt - 1), config.maxDelay);
588
+ let delay = Math.min(this.parseInterval(config.reconnectDelay) * Math.pow(2, attempt - 1), this.parseInterval(config.reconnectMaxDelay));
589
+ if (config.reconnectJitter > 0) {
590
+ let jitterRange = delay * config.reconnectJitter;
591
+ let jitter = (Math.random() * 2 - 1) * jitterRange;
592
+ delay = Math.max(0, delay + jitter);
593
+ }
617
594
  let reconnect = {attempt, delay, lastEventId, cancelled: false};
618
595
 
619
596
  ctx.status = "reconnecting to stream";
@@ -646,7 +623,7 @@ var htmx = (() => {
646
623
  for await (const sseMessage of this.#parseSSE(currentResponse)) {
647
624
  if (!elt.isConnected) break;
648
625
 
649
- if (config.pauseHidden && document.hidden) {
626
+ if (config.pauseInBackground && document.hidden) {
650
627
  await waitForVisible();
651
628
  if (!elt.isConnected) break;
652
629
  }
@@ -744,7 +721,7 @@ var htmx = (() => {
744
721
  #initTimeout(ctx) {
745
722
  let timeoutInterval;
746
723
  if (ctx.request.timeout) {
747
- timeoutInterval = typeof ctx.request.timeout == "string" ? this.parseInterval(ctx.request.timeout) : ctx.request.timeout;
724
+ timeoutInterval = this.parseInterval(ctx.request.timeout);
748
725
  } else {
749
726
  timeoutInterval = this.config.defaultTimeout;
750
727
  }
@@ -943,36 +920,6 @@ var htmx = (() => {
943
920
  }
944
921
  }
945
922
 
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
-
972
- if (!elt._htmx) elt._htmx = {};
973
- elt._htmx.streamConfig = streamConfig;
974
- }
975
-
976
923
  #extractFilter(str) {
977
924
  let match = str.match(/^([^\[]*)\[([^\]]*)]/);
978
925
  if (!match) return [str, null];
@@ -981,7 +928,7 @@ var htmx = (() => {
981
928
 
982
929
  #handleTriggerHeader(value, elt) {
983
930
  if (value[0] === '{') {
984
- let triggers = JSON.parse(value);
931
+ let triggers = this.#parseConfig(value);
985
932
  for (let name in triggers) {
986
933
  let detail = triggers[name];
987
934
  if (detail?.target) elt = this.find(detail.target) || elt;
@@ -1150,7 +1097,7 @@ var htmx = (() => {
1150
1097
  }
1151
1098
 
1152
1099
  #makeFragment(text) {
1153
- let response = text.replace(/<hx-partial(\s+|>)/gi, '<template partial$1').replace(/<\/hx-partial>/gi, '</template>');
1100
+ let response = text.replace(/<hx-([a-z]+)(\s+|>)/gi, '<template hx type="$1"$2').replace(/<\/hx-[a-z]+>/gi, '</template>');
1154
1101
  let title = '';
1155
1102
  response = response.replace(/<title[^>]*>[\s\S]*?<\/title>/i, m => (title = this.#parseHTML(m).title, ''));
1156
1103
  let responseWithNoHead = response.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i, '');
@@ -1223,52 +1170,36 @@ var htmx = (() => {
1223
1170
  }
1224
1171
 
1225
1172
  #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
- }
1173
+ swapStr = swapStr.trim();
1174
+ let style = this.config.defaultSwap
1175
+ if (swapStr && !/^\S*:/.test(swapStr)) {
1176
+ let m = swapStr.match(/^(\S+)\s*(.*)$/);
1177
+ style = m[1];
1178
+ swapStr = m[2];
1254
1179
  }
1255
- return config;
1180
+ return {style: this.#normalizeSwapStyle(style), ...this.#parseConfig(swapStr)};
1256
1181
  }
1257
1182
 
1258
- #processPartials(fragment, sourceElement) {
1183
+ #processPartials(fragment, ctx) {
1259
1184
  let tasks = [];
1260
1185
 
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();
1186
+ for (let templateElt of fragment.querySelectorAll('template[hx]')) {
1187
+ let type = templateElt.getAttribute('type');
1188
+
1189
+ if (type === 'partial') {
1190
+ let swapSpec = this.#parseSwapSpec(templateElt.getAttribute(this.#prefix('hx-swap')) || this.config.defaultSwap);
1191
+
1192
+ tasks.push({
1193
+ type: 'partial',
1194
+ fragment: templateElt.content.cloneNode(true),
1195
+ target: templateElt.getAttribute(this.#prefix('hx-target')),
1196
+ swapSpec,
1197
+ sourceElement: ctx.sourceElement
1198
+ });
1199
+ } else {
1200
+ this.#triggerExtensions(templateElt, 'htmx:process:' + type, { ctx, tasks });
1201
+ }
1202
+ templateElt.remove();
1272
1203
  }
1273
1204
 
1274
1205
  return tasks;
@@ -1281,37 +1212,22 @@ var htmx = (() => {
1281
1212
 
1282
1213
  #handleScroll(task) {
1283
1214
  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') {
1215
+ let target = task.swapSpec.scrollTarget ? this.#findExt(task.swapSpec.scrollTarget) : task.target;
1216
+ if (task.swapSpec.scroll === 'top') {
1293
1217
  target.scrollTop = 0;
1294
- } else if (value === 'bottom'){
1218
+ } else if (task.swapSpec.scroll === 'bottom'){
1295
1219
  target.scrollTop = target.scrollHeight;
1296
1220
  }
1297
1221
  }
1298
1222
  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')
1223
+ let target = task.swapSpec.showTarget ? this.#findExt(task.swapSpec.showTarget) : task.target;
1224
+ target.scrollIntoView(task.swapSpec.show === 'top')
1308
1225
  }
1309
1226
  }
1310
1227
 
1311
1228
  #handleAnchorScroll(ctx) {
1312
- let anchor = ctx.request?.originalAction?.split('#')[1];
1313
- if (anchor) {
1314
- document.getElementById(anchor)?.scrollIntoView({block: 'start', behavior: 'auto'});
1229
+ if (ctx.request?.anchor) {
1230
+ document.getElementById(ctx.request.anchor)?.scrollIntoView({block: 'start', behavior: 'auto'});
1315
1231
  }
1316
1232
  }
1317
1233
 
@@ -1342,7 +1258,7 @@ var htmx = (() => {
1342
1258
 
1343
1259
  // Process OOB and partials
1344
1260
  let oobTasks = this.#processOOB(fragment, ctx.sourceElement, ctx.selectOOB);
1345
- let partialTasks = this.#processPartials(fragment, ctx.sourceElement);
1261
+ let partialTasks = this.#processPartials(fragment, ctx);
1346
1262
  tasks.push(...oobTasks, ...partialTasks);
1347
1263
 
1348
1264
  // Process main swap
@@ -1364,8 +1280,8 @@ var htmx = (() => {
1364
1280
 
1365
1281
  // insert non-transition tasks immediately or with delay
1366
1282
  for (let task of nonTransitionTasks) {
1367
- if (task.swapSpec?.swapDelay) {
1368
- setTimeout(() => this.#insertContent(task), task.swapSpec.swapDelay);
1283
+ if (task.swapSpec?.swap) {
1284
+ setTimeout(() => this.#insertContent(task), this.parseInterval(task.swapSpec.swap));
1369
1285
  } else {
1370
1286
  this.#insertContent(task)
1371
1287
  }
@@ -1412,7 +1328,7 @@ var htmx = (() => {
1412
1328
  let mainSwap = {
1413
1329
  type: 'main',
1414
1330
  fragment,
1415
- target: swapSpec.target || ctx.target,
1331
+ target: this.#resolveTarget(ctx.sourceElement || document.body, swapSpec.target || ctx.target),
1416
1332
  swapSpec,
1417
1333
  sourceElement: ctx.sourceElement,
1418
1334
  transition: (ctx.transition !== false) && (swapSpec.transition !== false)
@@ -1492,7 +1408,7 @@ var htmx = (() => {
1492
1408
  console.log(eventName, detail, on)
1493
1409
  }
1494
1410
  on = this.#normalizeElement(on)
1495
- this.#triggerExtensions(on, this.#maybeAdjustMetaCharacter(eventName), detail);
1411
+ this.#triggerExtensions(on, eventName, detail);
1496
1412
  return this.trigger(on, eventName, detail, bubbles)
1497
1413
  }
1498
1414
 
@@ -1511,9 +1427,7 @@ var htmx = (() => {
1511
1427
  }
1512
1428
 
1513
1429
  timeout(time) {
1514
- if (typeof time === "string") {
1515
- time = this.parseInterval(time)
1516
- }
1430
+ time = this.parseInterval(time);
1517
1431
  if (time > 0) {
1518
1432
  return new Promise(resolve => setTimeout(resolve, time));
1519
1433
  }
@@ -1536,7 +1450,7 @@ var htmx = (() => {
1536
1450
  }
1537
1451
 
1538
1452
  onLoad(callback) {
1539
- this.on("htmx:after:init", (evt) => {
1453
+ this.on("htmx:after:process", (evt) => {
1540
1454
  callback(evt.target)
1541
1455
  })
1542
1456
  }
@@ -1571,6 +1485,7 @@ var htmx = (() => {
1571
1485
  }
1572
1486
 
1573
1487
  parseInterval(str) {
1488
+ if (typeof str === 'number') return str;
1574
1489
  let m = {ms: 1, s: 1000, m: 60000};
1575
1490
  let [, n, u] = str?.match(/^([\d.]+)(ms|s|m)?$/) || [];
1576
1491
  let v = parseFloat(n) * (m[u] || 1);
@@ -1600,20 +1515,21 @@ var htmx = (() => {
1600
1515
  let sourceElt = typeof context.source === 'string' ?
1601
1516
  document.querySelector(context.source) : context.source;
1602
1517
 
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;
1518
+ // If source selector was provided but didn't match, reject
1519
+ if (typeof context.source === 'string' && !sourceElt) {
1520
+ return Promise.reject(new Error('Source not found'));
1521
+ }
1607
1522
 
1608
- if (!targetElt) {
1523
+ // Resolve target, defaulting to body only if no source or target provided
1524
+ let target = this.#resolveTarget(document.body, context.target || sourceElt);
1525
+ if (!target) {
1609
1526
  return Promise.reject(new Error('Target not found'));
1610
1527
  }
1611
1528
 
1612
- // TODO is this logic correct?
1613
- sourceElt ||= targetElt || document.body;
1529
+ sourceElt ||= target;
1614
1530
 
1615
1531
  let ctx = this.#createRequestContext(sourceElt, context.event || {});
1616
- Object.assign(ctx, context, {target: targetElt});
1532
+ Object.assign(ctx, context, {target});
1617
1533
  Object.assign(ctx.request, {action: path, method: verb.toUpperCase()});
1618
1534
  if (context.headers) Object.assign(ctx.request.headers, context.headers);
1619
1535
 
@@ -1676,10 +1592,10 @@ var htmx = (() => {
1676
1592
  }
1677
1593
 
1678
1594
  let path = push || replace;
1679
- if (!path || path === 'false') return;
1595
+ if (!path || path === 'false' || path === false) return;
1680
1596
 
1681
1597
  if (path === 'true') {
1682
- path = ctx.request.originalAction;
1598
+ path = ctx.request.action + (ctx.request.anchor ? '#' + ctx.request.anchor : '');
1683
1599
  }
1684
1600
 
1685
1601
  let type = push ? 'push' : 'replace';
@@ -1766,11 +1682,9 @@ var htmx = (() => {
1766
1682
  }
1767
1683
 
1768
1684
  #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) {
1685
+ let formData = form ? new FormData(form) : new FormData()
1686
+ let included = form ? new Set(form.elements) : new Set()
1687
+ if (!form && elt.name) {
1774
1688
  formData.append(elt.name, elt.value)
1775
1689
  included.add(elt);
1776
1690
  }
@@ -1792,21 +1706,21 @@ var htmx = (() => {
1792
1706
  let inputs = this.#queryEltAndDescendants(elt, 'input:not([disabled]), select:not([disabled]), textarea:not([disabled])');
1793
1707
 
1794
1708
  for (let input of inputs) {
1795
- // Skip elements without a name or already seen
1796
1709
  if (!input.name || included.has(input)) continue;
1797
1710
  included.add(input);
1798
1711
 
1799
- if (input.matches('input[type=checkbox], input[type=radio]')) {
1712
+ let type = input.type;
1713
+ if (type === 'checkbox' || type === 'radio') {
1800
1714
  // Only add if checked
1801
1715
  if (input.checked) {
1802
1716
  formData.append(input.name, input.value);
1803
1717
  }
1804
- } else if (input.matches('input[type=file]')) {
1718
+ } else if (type === 'file') {
1805
1719
  // Add all selected files
1806
1720
  for (let file of input.files) {
1807
1721
  formData.append(input.name, file);
1808
1722
  }
1809
- } else if (input.matches('select[multiple]')) {
1723
+ } else if (type === 'select-multiple') {
1810
1724
  // Add all selected options
1811
1725
  for (let option of input.selectedOptions) {
1812
1726
  formData.append(input.name, option.value);
@@ -1821,12 +1735,20 @@ var htmx = (() => {
1821
1735
  #handleHxVals(elt, body) {
1822
1736
  let hxValsValue = this.#attributeValue(elt, "hx-vals");
1823
1737
  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])
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
+ }
1830
1752
  }
1831
1753
  }
1832
1754
  }
@@ -1837,11 +1759,13 @@ var htmx = (() => {
1837
1759
  }
1838
1760
 
1839
1761
  #findAllExt(eltOrSelector, maybeSelector, global) {
1840
- let [elt, selector] = this.#normalizeElementAndSelector(eltOrSelector, maybeSelector)
1762
+ let selector = maybeSelector ?? eltOrSelector;
1763
+ let elt = maybeSelector ? this.#normalizeElement(eltOrSelector) : document;
1841
1764
  if (selector.startsWith('global ')) {
1842
1765
  return this.#findAllExt(elt, selector.slice(7), true);
1843
1766
  }
1844
- let parts = this.#tokenizeExtendedSelector(selector);
1767
+ let parts = selector ? selector.replace(/<[^>]+\/>/g, m => m.replace(/,/g, '%2C'))
1768
+ .split(',').map(p => p.replace(/%2C/g, ',')) : [];
1845
1769
  let result = []
1846
1770
  let unprocessedParts = []
1847
1771
  for (const part of parts) {
@@ -1887,28 +1811,6 @@ var htmx = (() => {
1887
1811
  return result
1888
1812
  }
1889
1813
 
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
1814
  #scanForwardQuery(start, match, global) {
1913
1815
  return this.#scanUntilComparison(this.#getRootNode(start, global).querySelectorAll(match), start, Node.DOCUMENT_POSITION_PRECEDING);
1914
1816
  }
@@ -2167,13 +2069,13 @@ var htmx = (() => {
2167
2069
  let noSwapStrings = this.config.noSwap.map(x => x + "");
2168
2070
  let str = status + ""
2169
2071
  for (let pattern of [str, str.slice(0, 2) + 'x', str[0] + 'xx']) {
2170
- let swap = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
2171
2072
  if (noSwapStrings.includes(pattern)) {
2172
2073
  ctx.swap = "none";
2173
2074
  return
2174
2075
  }
2175
- if (swap) {
2176
- ctx.swap = swap;
2076
+ let statusValue = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
2077
+ if (statusValue) {
2078
+ Object.assign(ctx, this.#parseConfig(statusValue));
2177
2079
  return;
2178
2080
  }
2179
2081
  }