htmx.org 4.0.0-alpha1 → 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
@@ -59,7 +59,8 @@ var htmx = (() => {
59
59
  class Htmx {
60
60
 
61
61
  #extMethods = new Map();
62
- #approvedExt = new Set();
62
+ #approvedExt = '';
63
+ #registeredExt = new Set();
63
64
  #internalAPI;
64
65
  #actionSelector
65
66
  #boostSelector = "a,form";
@@ -72,7 +73,7 @@ var htmx = (() => {
72
73
  this.#initHtmxConfig();
73
74
  this.#initRequestIndicatorCss();
74
75
  this.#actionSelector = `[${this.#prefix("hx-action")}],[${this.#prefix("hx-get")}],[${this.#prefix("hx-post")}],[${this.#prefix("hx-put")}],[${this.#prefix("hx-patch")}],[${this.#prefix("hx-delete")}]`;
75
- this.#hxOnQuery = new XPathEvaluator().createExpression(`.//*[@*[ starts-with(name(), "${this.#prefix("hx-on:")}")]]`);
76
+ this.#hxOnQuery = new XPathEvaluator().createExpression(`.//*[@*[ starts-with(name(), "${this.#prefix("hx-on")}")]]`);
76
77
  this.#internalAPI = {
77
78
  attributeValue: this.#attributeValue.bind(this),
78
79
  parseTriggerSpecs: this.#parseTriggerSpecs.bind(this),
@@ -89,6 +90,7 @@ var htmx = (() => {
89
90
 
90
91
  #initHtmxConfig() {
91
92
  this.config = {
93
+ version: '4.0.0-alpha3',
92
94
  logAll: false,
93
95
  prefix: "",
94
96
  transitions: true,
@@ -101,7 +103,7 @@ var htmx = (() => {
101
103
  includeIndicatorCSS: true,
102
104
  defaultTimeout: 60000, /* 60 second default timeout */
103
105
  extensions: '',
104
- streams: {
106
+ sse: {
105
107
  mode: 'once',
106
108
  maxRetries: Infinity,
107
109
  initialDelay: 500,
@@ -109,11 +111,13 @@ var htmx = (() => {
109
111
  pauseHidden: false
110
112
  },
111
113
  morphIgnore: ["data-htmx-powered"],
112
- noSwap: [204],
114
+ noSwap: [204, 304],
115
+ implicitInheritance: false
113
116
  }
114
117
  let metaConfig = document.querySelector('meta[name="htmx:config"]');
115
118
  if (metaConfig) {
116
- let overrides = JSON.parse(metaConfig.content);
119
+ let content = metaConfig.content;
120
+ let overrides = this.#parseConfig(content);
117
121
  // Deep merge nested config objects
118
122
  for (let key in overrides) {
119
123
  let val = overrides[key];
@@ -124,7 +128,7 @@ var htmx = (() => {
124
128
  }
125
129
  }
126
130
  }
127
- this.#approvedExt = new Set(this.config.extensions.split(',').map(s => s.trim()).filter(Boolean));
131
+ this.#approvedExt = this.config.extensions;
128
132
  }
129
133
 
130
134
  #initRequestIndicatorCss() {
@@ -144,7 +148,9 @@ var htmx = (() => {
144
148
  }
145
149
 
146
150
  defineExtension(name, extension) {
147
- if (!this.#approvedExt.delete(name)) return false;
151
+ if (this.#approvedExt && !this.#approvedExt.split(/,\s*/).includes(name)) return false;
152
+ if (this.#registeredExt.has(name)) return false;
153
+ this.#registeredExt.add(name);
148
154
  if (extension.init) extension.init(this.#internalAPI);
149
155
  Object.entries(extension).forEach(([key, value]) => {
150
156
  if(!this.#extMethods.get(key)?.push(value)) this.#extMethods.set(key, [value]);
@@ -174,102 +180,78 @@ var htmx = (() => {
174
180
  style === 'append' ? 'beforeend' : style;
175
181
  }
176
182
 
177
- #attributeValue(elt, name, defaultVal) {
183
+ #attributeValue(elt, name, defaultVal, returnElt) {
178
184
  name = this.#prefix(name);
179
- let appendName = name + ":append";
180
- let inheritName = name + ":inherited";
181
- let inheritAppendName = name + ":inherited:append";
185
+ let appendName = name + this.#maybeAdjustMetaCharacter(":append");
186
+ let inheritName = name + (this.config.implicitInheritance ? "" : this.#maybeAdjustMetaCharacter(":inherited"));
187
+ let inheritAppendName = name + this.#maybeAdjustMetaCharacter(":inherited:append");
182
188
 
183
- if (elt.hasAttribute(name) || elt.hasAttribute(inheritName)) {
184
- return elt.getAttribute(name) || elt.getAttribute(inheritName);
189
+ if (elt.hasAttribute(name)) {
190
+ return returnElt ? elt : elt.getAttribute(name);
191
+ }
192
+
193
+ if (elt.hasAttribute(inheritName)) {
194
+ return returnElt ? elt : elt.getAttribute(inheritName);
185
195
  }
186
196
 
187
197
  if (elt.hasAttribute(appendName) || elt.hasAttribute(inheritAppendName)) {
188
198
  let appendValue = elt.getAttribute(appendName) || elt.getAttribute(inheritAppendName);
189
199
  let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
190
200
  if (parent) {
191
- let inheritedValue = this.#attributeValue(parent, name);
192
- return inheritedValue ? inheritedValue + "," + appendValue : appendValue;
201
+ let inherited = this.#attributeValue(parent, name, undefined, returnElt);
202
+ return returnElt ? inherited : (inherited ? inherited + "," + appendValue : appendValue);
203
+ } else {
204
+ return returnElt ? elt : appendValue;
193
205
  }
194
- return appendValue;
195
206
  }
196
207
 
197
208
  let parent = elt.parentNode?.closest?.(`[${CSS.escape(inheritName)}],[${CSS.escape(inheritAppendName)}]`);
198
209
  if (parent) {
199
- return this.#attributeValue(parent, name);
200
- }
201
- return defaultVal;
202
- }
203
-
204
- #tokenize(str) {
205
- let tokens = [], i = 0;
206
- while (i < str.length) {
207
- let c = str[i];
208
- if (c === '"' || c === "'") {
209
- let q = c, s = c;
210
- i++;
211
- while (i < str.length) {
212
- c = str[i];
213
- s += c;
214
- i++;
215
- if (c === '\\' && i < str.length) {
216
- s += str[i];
217
- i++;
218
- } else if (c === q) break;
219
- }
220
- tokens.push(s);
221
- } else if (/\s/.test(c)) {
222
- while (i < str.length && /\s/.test(str[i])) i++;
223
- } else if (c === ':' || c === ',') {
224
- tokens.push(c);
225
- i++;
226
- } else {
227
- let t = '';
228
- while (i < str.length && !/[\s"':,]/.test(str[i])) t += str[i++];
229
- tokens.push(t);
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})
230
213
  }
214
+ return val;
231
215
  }
232
- return tokens;
216
+ return returnElt ? elt : defaultVal;
233
217
  }
234
218
 
235
- #parseTriggerSpecs(spec) {
236
- let specs = []
237
- let currentSpec = null
238
- let tokens = this.#tokenize(spec);
239
- for (let i = 0; i < tokens.length; i++) {
240
- let token = tokens[i];
241
- if (token === ",") {
242
- currentSpec = null;
243
- } else if (!currentSpec) {
244
- while (token.includes("[") && !token.includes("]") && i + 1 < tokens.length) {
245
- token += tokens[++i];
246
- }
247
- if (token.includes("[") && !token.includes("]")) {
248
- throw "unterminated:" + token;
249
- }
250
- currentSpec = {name: token};
251
- specs.push(currentSpec);
252
- } else if (tokens[i + 1] === ":") {
253
- currentSpec[token] = tokens[i += 2];
254
- } else {
255
- currentSpec[token] = true;
256
- }
257
- }
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
+ }, {});
231
+ }
258
232
 
259
- return specs;
233
+ #parseTriggerSpecs(spec) {
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);
260
242
  }
261
243
 
262
244
  #determineMethodAndAction(elt, evt) {
263
245
  if (this.#isBoosted(elt)) {
264
246
  return this.#boostedMethodAndAction(elt, evt)
265
247
  } else {
266
- let method = this.#attributeValue(elt, "hx-method") || "get";
248
+ let method = this.#attributeValue(elt, "hx-method") || "GET"
267
249
  let action = this.#attributeValue(elt, "hx-action");
268
250
  if (!action) {
269
251
  for (let verb of this.#verbs) {
270
- let verbAttribute = this.#attributeValue(elt, "hx-" + verb);
271
- if (verbAttribute) {
272
- action = verbAttribute;
252
+ let verbAction = this.#attributeValue(elt, "hx-" + verb);
253
+ if (verbAction) {
254
+ action = verbAction;
273
255
  method = verb;
274
256
  break;
275
257
  }
@@ -295,7 +277,6 @@ var htmx = (() => {
295
277
  elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt)}
296
278
  elt.setAttribute('data-htmx-powered', 'true');
297
279
  this.#initializeTriggers(elt);
298
- this.#initializeStreamConfig(elt);
299
280
  this.#initializeAbortListener(elt)
300
281
  this.#trigger(elt, "htmx:after:init", {}, true)
301
282
  this.#trigger(elt, "load", {}, false)
@@ -321,11 +302,12 @@ var htmx = (() => {
321
302
  status: "created",
322
303
  select: this.#attributeValue(sourceElement, "hx-select"),
323
304
  selectOOB: this.#attributeValue(sourceElement, "hx-select-oob"),
324
- target: this.#attributeValue(sourceElement, "hx-target"),
305
+ target: this.#resolveTarget(sourceElement, this.#attributeValue(sourceElement, "hx-target")),
325
306
  swap: this.#attributeValue(sourceElement, "hx-swap", this.config.defaultSwap),
326
307
  push: this.#attributeValue(sourceElement, "hx-push-url"),
327
308
  replace: this.#attributeValue(sourceElement, "hx-replace-url"),
328
309
  transition: this.config.transitions,
310
+ confirm: this.#attributeValue(sourceElement, "hx-confirm"),
329
311
  request: {
330
312
  validate: "true" === this.#attributeValue(sourceElement, "hx-validate", sourceElement.matches('form') ? "true" : "false"),
331
313
  action,
@@ -337,28 +319,35 @@ var htmx = (() => {
337
319
  // Apply hx-config overrides
338
320
  let configAttr = this.#attributeValue(sourceElement, "hx-config");
339
321
  if (configAttr) {
340
- let configOverrides = JSON.parse(configAttr);
341
- let requestConfig = ctx.request;
322
+ let configOverrides = this.#parseConfig(configAttr);
323
+ let req = ctx.request;
342
324
  for (let key in configOverrides) {
343
325
  if (key.startsWith('+')) {
344
326
  let actualKey = key.substring(1);
345
- if (requestConfig[actualKey] && typeof ctx[actualKey] === 'object') {
346
- Object.assign(ctx[actualKey], configOverrides[key]);
327
+ if (req[actualKey] && typeof req[actualKey] === 'object') {
328
+ Object.assign(req[actualKey], configOverrides[key]);
347
329
  } else {
348
- requestConfig[actualKey] = configOverrides[key];
330
+ req[actualKey] = configOverrides[key];
349
331
  }
350
332
  } else {
351
- requestConfig[key] = configOverrides[key];
333
+ req[key] = configOverrides[key];
352
334
  }
353
335
  }
336
+ if (req.etag) {
337
+ (sourceElement._htmx ||= {}).etag ||= req.etag
338
+ }
339
+ }
340
+ if (sourceElement._htmx?.etag) {
341
+ ctx.request.headers["If-none-match"] = sourceElement._htmx.etag
354
342
  }
355
-
356
343
  return ctx;
357
344
  }
358
345
 
359
346
  #determineHeaders(elt) {
360
347
  let headers = {
361
348
  "HX-Request": "true",
349
+ "HX-Source": elt.id || elt.name,
350
+ "HX-Current-URL": location.href,
362
351
  "Accept": "text/html, text/event-stream"
363
352
  };
364
353
  if (this.#isBoosted(elt)) {
@@ -366,7 +355,7 @@ var htmx = (() => {
366
355
  }
367
356
  let headersAttribute = this.#attributeValue(elt, "hx-headers");
368
357
  if (headersAttribute) {
369
- Object.assign(headers, JSON.parse(headersAttribute));
358
+ Object.assign(headers, this.#parseConfig(headersAttribute));
370
359
  }
371
360
  return headers;
372
361
  }
@@ -375,11 +364,7 @@ var htmx = (() => {
375
364
  if (selector instanceof Element) {
376
365
  return selector;
377
366
  } else if (selector === 'this') {
378
- if (elt.hasAttribute(this.#prefix("hx-target"))) {
379
- return elt;
380
- } else {
381
- return elt.closest(`[${this.#prefix("hx-target")}\\:inherited='this']`)
382
- }
367
+ return this.#attributeValue(elt, "hx-target", undefined, true);
383
368
  } else if (selector != null) {
384
369
  return this.find(elt, selector);
385
370
  } else if (this.#isBoosted(elt)) {
@@ -390,10 +375,10 @@ var htmx = (() => {
390
375
  }
391
376
 
392
377
  #isBoosted(elt) {
393
- return elt._htmx?.boosted;
378
+ return elt?._htmx?.boosted;
394
379
  }
395
380
 
396
- async __handleTriggerEvent(ctx) {
381
+ async #handleTriggerEvent(ctx) {
397
382
  let elt = ctx.sourceElement
398
383
  let evt = ctx.sourceEvent
399
384
  if (!elt.isConnected) return
@@ -402,13 +387,11 @@ var htmx = (() => {
402
387
 
403
388
  if (this.#shouldCancel(evt)) evt.preventDefault()
404
389
 
405
- // Resolve swap target
406
- ctx.target = this.#resolveTarget(elt, ctx.target);
407
-
408
390
  // Build request body
409
391
  let form = elt.form || elt.closest("form")
410
392
  let body = this.#collectFormData(elt, form, evt.submitter)
411
- this.#handleHxVals(elt, body)
393
+ let valsResult = this.#handleHxVals(elt, body)
394
+ if (valsResult) await valsResult // Only await if it returned a promise
412
395
  if (ctx.values) {
413
396
  for (let k in ctx.values) {
414
397
  body.delete(k);
@@ -419,7 +402,7 @@ var htmx = (() => {
419
402
  // Setup abort controller and action
420
403
  let ac = new AbortController()
421
404
  let action = ctx.request.action.replace?.(/#.*$/, '')
422
- // TODO - consider how this works with hx-config, move most to __createRequestContext?
405
+ // TODO - consider how this works with hx-config, move most to #createRequestContext?
423
406
  Object.assign(ctx.request, {
424
407
  originalAction: ctx.request.action,
425
408
  action,
@@ -452,7 +435,7 @@ var htmx = (() => {
452
435
  await this.#issueRequest(ctx);
453
436
  }
454
437
 
455
- async __issueRequest(ctx) {
438
+ async #issueRequest(ctx) {
456
439
  let elt = ctx.sourceElement
457
440
  let syncStrategy = this.#determineSyncStrategy(elt);
458
441
  let requestQueue = this.#getRequestQueue(elt);
@@ -468,25 +451,23 @@ var htmx = (() => {
468
451
  let disableElements = this.#disableElements(elt, disableSelector);
469
452
 
470
453
  try {
471
- // Confirm dialog
472
- let confirmVal = this.#attributeValue(elt, 'hx-confirm')
473
- if (confirmVal) {
474
- let js = this.#extractJavascriptContent(confirmVal);
475
- if (js) {
476
- if (!await this.#executeJavaScriptAsync(ctx.elt, {}, js, true)) {
477
- return
478
- }
479
- } else {
480
- if (!window.confirm(confirmVal)) {
481
- 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));
482
462
  }
483
- }
463
+ });
464
+ if (!confirmed) return;
484
465
  }
485
-
486
- ctx.fetch ||= window.fetch
466
+
467
+ ctx.fetch ||= window.fetch.bind(window)
487
468
  if (!this.#trigger(elt, "htmx:before:request", {ctx})) return;
488
469
 
489
- let response = await (ctx.fetchOverride || ctx.fetch(ctx.request.action, ctx.request));
470
+ let response = await ctx.fetch(ctx.request.action, ctx.request);
490
471
 
491
472
  ctx.response = {
492
473
  raw: response,
@@ -494,9 +475,13 @@ var htmx = (() => {
494
475
  headers: response.headers,
495
476
  }
496
477
  this.#extractHxHeaders(ctx);
478
+ ctx.isSSE = response.headers.get("Content-Type")?.includes('text/event-stream');
479
+ if (!ctx.isSSE) {
480
+ ctx.text = await response.text();
481
+ }
497
482
  if (!this.#trigger(elt, "htmx:after:request", {ctx})) return;
498
483
 
499
- if(this.#handleHxHeadersAndMaybeReturnEarly(ctx)){
484
+ if(this.#handleHeadersAndMaybeReturnEarly(ctx)){
500
485
  return
501
486
  }
502
487
 
@@ -506,16 +491,13 @@ var htmx = (() => {
506
491
  await this.#handleSSE(ctx, elt, response);
507
492
  } else {
508
493
  // HTTP response
509
- ctx.text = await response.text();
510
494
  if (ctx.status === "issuing") {
511
- if (ctx.hx.retarget) ctx.target = this.#resolveTarget(elt, ctx.hx.retarget);
495
+ if (ctx.hx.retarget) ctx.target = ctx.hx.retarget;
512
496
  if (ctx.hx.reswap) ctx.swap = ctx.hx.reswap;
513
497
  if (ctx.hx.reselect) ctx.select = ctx.hx.reselect;
514
498
  ctx.status = "response received";
515
499
  this.#handleStatusCodes(ctx);
516
- this.#handleHistoryUpdate(ctx);
517
500
  await this.swap(ctx);
518
- this.#handleAnchorScroll(ctx)
519
501
  ctx.status = "swapped";
520
502
  }
521
503
  }
@@ -537,7 +519,7 @@ var htmx = (() => {
537
519
  }
538
520
 
539
521
  // Extract HX-* headers into ctx.hx
540
- #extractHxHeaders(ctx, response) {
522
+ #extractHxHeaders(ctx) {
541
523
  ctx.hx = {}
542
524
  for (let [k, v] of ctx.response.raw.headers) {
543
525
  if (k.toLowerCase().startsWith('hx-')) {
@@ -547,7 +529,7 @@ var htmx = (() => {
547
529
  }
548
530
 
549
531
  // returns true if the header aborts the current response handling
550
- #handleHxHeadersAndMaybeReturnEarly(ctx) {
532
+ #handleHeadersAndMaybeReturnEarly(ctx) {
551
533
  if (ctx.hx.trigger) {
552
534
  this.#handleTriggerHeader(ctx.hx.trigger, ctx.sourceElement);
553
535
  }
@@ -561,8 +543,8 @@ var htmx = (() => {
561
543
  }
562
544
  if (ctx.hx.location) {
563
545
  let path = ctx.hx.location, opts = {};
564
- if (path[0] === '{') {
565
- opts = JSON.parse(path);
546
+ if (path[0] === '{' || /[\s,]/.test(path)) {
547
+ opts = this.#parseConfig(path);
566
548
  path = opts.path;
567
549
  delete opts.path;
568
550
  }
@@ -570,10 +552,16 @@ var htmx = (() => {
570
552
  this.ajax('GET', path, opts);
571
553
  return true // TODO this seems legit
572
554
  }
555
+ if(ctx.response?.headers?.get?.("Etag")) {
556
+ ctx.sourceElement._htmx ||= {}
557
+ ctx.sourceElement._htmx.etag = ctx.response.headers.get("Etag");
558
+ }
573
559
  }
574
560
 
575
- async __handleSSE(ctx, elt, response) {
576
- let config = elt._htmx?.streamConfig || {...this.config.streams};
561
+ 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';
577
565
 
578
566
  let waitForVisible = () => new Promise(r => {
579
567
  let onVisible = () => !document.hidden && (document.removeEventListener('visibilitychange', onVisible), r());
@@ -592,7 +580,7 @@ var htmx = (() => {
592
580
  if (!elt.isConnected) break;
593
581
  }
594
582
 
595
- 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));
596
584
  let reconnect = {attempt, delay, lastEventId, cancelled: false};
597
585
 
598
586
  ctx.status = "reconnecting to stream";
@@ -650,9 +638,7 @@ var htmx = (() => {
650
638
  ctx.status = "stream message received";
651
639
 
652
640
  if (!ctx.response.cancelled) {
653
- this.#handleHistoryUpdate(ctx);
654
641
  await this.swap(ctx);
655
- this.#handleAnchorScroll(ctx);
656
642
  ctx.status = "swapped";
657
643
  }
658
644
  this.#trigger(elt, "htmx:after:sse:message", {ctx, message: msg});
@@ -669,30 +655,63 @@ var htmx = (() => {
669
655
  }
670
656
  }
671
657
 
672
- async* __parseSSE(res) {
673
- let r = res.body.getReader(), d = new TextDecoder(), b = '', m = {data: '', event: '', id: '', retry: null},
674
- ls, i, n, f, v;
658
+ async* #parseSSE(response) {
659
+ let reader = response.body.getReader();
660
+ let decoder = new TextDecoder();
661
+ let buffer = '';
662
+ let message = {data: '', event: '', id: '', retry: null};
663
+
675
664
  try {
676
- while (1) {
677
- let {done, value} = await r.read();
665
+ while (true) {
666
+ let {done, value} = await reader.read();
678
667
  if (done) break;
679
- for (let l of (b += d.decode(value, {stream: 1}), ls = b.split('\n'), b = ls.pop() || '', ls))
680
- !l || l === '\r' ? m.data && (yield m, m = {data: '', event: '', id: '', retry: null}) :
681
- (i = l.indexOf(':')) > 0 && (f = l.slice(0, i), v = l.slice(i + 1).trimStart(),
682
- f === 'data' ? m.data += (m.data ? '\n' : '') + v :
683
- f === 'event' ? m.event = v :
684
- f === 'id' ? m.id = v :
685
- f === 'retry' && (n = parseInt(v, 10), !isNaN(n)) ? m.retry = n : 0);
668
+
669
+ // Decode chunk and add to buffer
670
+ buffer += decoder.decode(value, {stream: true});
671
+ let lines = buffer.split('\n');
672
+ // Keep incomplete line in buffer
673
+ buffer = lines.pop() || '';
674
+
675
+ for (let line of lines) {
676
+ // Empty line or carriage return indicates end of message
677
+ if (!line || line === '\r') {
678
+ if (message.data) {
679
+ yield message;
680
+ message = {data: '', event: '', id: '', retry: null};
681
+ }
682
+ continue;
683
+ }
684
+
685
+ // Parse field: value
686
+ let colonIndex = line.indexOf(':');
687
+ if (colonIndex <= 0) continue;
688
+
689
+ let field = line.slice(0, colonIndex);
690
+ let value = line.slice(colonIndex + 1).trimStart();
691
+
692
+ if (field === 'data') {
693
+ message.data += (message.data ? '\n' : '') + value;
694
+ } else if (field === 'event') {
695
+ message.event = value;
696
+ } else if (field === 'id') {
697
+ message.id = value;
698
+ } else if (field === 'retry') {
699
+ let retryValue = parseInt(value, 10);
700
+ if (!isNaN(retryValue)) {
701
+ message.retry = retryValue;
702
+ }
703
+ }
704
+ }
686
705
  }
687
706
  } finally {
688
- r.releaseLock();
707
+ reader.releaseLock();
689
708
  }
690
709
  }
691
710
 
692
711
  #initTimeout(ctx) {
693
712
  let timeoutInterval;
694
713
  if (ctx.request.timeout) {
695
- timeoutInterval = typeof ctx.request.timeout == "string" ? this.parseInterval(ctx.request.timeout) : ctx.request.timeout;
714
+ timeoutInterval = this.parseInterval(ctx.request.timeout);
696
715
  } else {
697
716
  timeoutInterval = this.config.defaultTimeout;
698
717
  }
@@ -891,35 +910,7 @@ var htmx = (() => {
891
910
  }
892
911
  }
893
912
 
894
- #initializeStreamConfig(elt) {
895
- let streamSpec = this.#attributeValue(elt, 'hx-stream');
896
- if (!streamSpec) return;
897
-
898
- // Start with global defaults
899
- let streamConfig = {...this.config.streams};
900
- let tokens = this.#tokenize(streamSpec);
901
-
902
- for (let i = 0; i < tokens.length; i++) {
903
- let token = tokens[i];
904
- // Main value: once or continuous
905
- if (token === 'once' || token === 'continuous') {
906
- streamConfig.mode = token;
907
- } else if (token === 'pauseHidden') {
908
- streamConfig.pauseHidden = true;
909
- } else if (tokens[i + 1] === ':') {
910
- let key = token, value = tokens[i + 2];
911
- if (key === 'mode') streamConfig.mode = value;
912
- else if (key === 'maxRetries') streamConfig.maxRetries = parseInt(value);
913
- else if (key === 'initialDelay') streamConfig.initialDelay = this.parseInterval(value);
914
- else if (key === 'maxDelay') streamConfig.maxDelay = this.parseInterval(value);
915
- else if (key === 'pauseHidden') streamConfig.pauseHidden = value === 'true';
916
- i += 2;
917
- }
918
- }
919
913
 
920
- if (!elt._htmx) elt._htmx = {};
921
- elt._htmx.streamConfig = streamConfig;
922
- }
923
914
 
924
915
  #extractFilter(str) {
925
916
  let match = str.match(/^([^\[]*)\[([^\]]*)]/);
@@ -929,7 +920,7 @@ var htmx = (() => {
929
920
 
930
921
  #handleTriggerHeader(value, elt) {
931
922
  if (value[0] === '{') {
932
- let triggers = JSON.parse(value);
923
+ let triggers = this.#parseConfig(value);
933
924
  for (let name in triggers) {
934
925
  let detail = triggers[name];
935
926
  if (detail?.target) elt = this.find(detail.target) || elt;
@@ -961,7 +952,7 @@ var htmx = (() => {
961
952
  return bound;
962
953
  }
963
954
 
964
- async __executeJavaScriptAsync(thisArg, obj, code, expression = true) {
955
+ async #executeJavaScriptAsync(thisArg, obj, code, expression = true) {
965
956
  let args = {}
966
957
  Object.assign(args, this.#apiMethods(thisArg))
967
958
  Object.assign(args, obj)
@@ -1002,23 +993,45 @@ var htmx = (() => {
1002
993
  }
1003
994
 
1004
995
  #maybeBoost(elt) {
1005
- if (this.#attributeValue(elt, "hx-boost") === "true") {
1006
- if (this.#shouldInitialize(elt)) {
1007
- elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt), requests: [], boosted: true}
1008
- elt.setAttribute('data-htmx-powered', 'true');
1009
- if (elt.matches('a') && !elt.hasAttribute("target")) {
1010
- elt.addEventListener('click', (click) => {
1011
- elt._htmx.eventHandler(click)
1012
- })
1013
- } else {
1014
- elt.addEventListener('submit', (submit) => {
1015
- elt._htmx.eventHandler(submit)
1016
- })
996
+ if (this.#attributeValue(elt, "hx-boost") === "true" && this.#shouldBoost(elt)) {
997
+ elt._htmx = {eventHandler: this.#createHtmxEventHandler(elt), requests: [], boosted: true}
998
+ elt.setAttribute('data-htmx-powered', 'true');
999
+ if (elt.matches('a') && !elt.hasAttribute("target")) {
1000
+ elt.addEventListener('click', (click) => {
1001
+ elt._htmx.eventHandler(click)
1002
+ })
1003
+ } else {
1004
+ elt.addEventListener('submit', (submit) => {
1005
+ elt._htmx.eventHandler(submit)
1006
+ })
1007
+ }
1008
+ this.#trigger(elt, "htmx:after:init", {}, true)
1009
+ }
1010
+ }
1011
+
1012
+ #shouldBoost(elt) {
1013
+ if (this.#shouldInitialize(elt)) {
1014
+ if (elt.tagName === "A") {
1015
+ if (elt.target === '' || elt.target === '_self') {
1016
+ return !elt.getAttribute('href')?.startsWith?.("#") && this.#isSameOrigin(elt.href)
1017
1017
  }
1018
+ } else if (elt.tagName === "FORM") {
1019
+ return elt.method !== 'dialog' && this.#isSameOrigin(elt.action);
1018
1020
  }
1019
1021
  }
1020
1022
  }
1021
1023
 
1024
+ #isSameOrigin(url) {
1025
+ try {
1026
+ // URL constructor handles both relative and absolute URLs
1027
+ const parsed = new URL(url, window.location.href);
1028
+ return parsed.origin === window.location.origin;
1029
+ } catch (e) {
1030
+ // If URL parsing fails, assume not same-origin
1031
+ return false;
1032
+ }
1033
+ }
1034
+
1022
1035
  #shouldInitialize(elt) {
1023
1036
  return !elt._htmx && !this.#ignore(elt);
1024
1037
  }
@@ -1076,8 +1089,9 @@ var htmx = (() => {
1076
1089
  }
1077
1090
 
1078
1091
  #makeFragment(text) {
1079
- let response = text.replace(/<partial(\s+|>)/gi, '<template partial$1').replace(/<\/partial>/gi, '</template>');
1080
- // TODO - store any head tag content on the fragment for head extension
1092
+ let response = text.replace(/<hx-([a-z]+)(\s+|>)/gi, '<template hx type="$1"$2').replace(/<\/hx-[a-z]+>/gi, '</template>');
1093
+ let title = '';
1094
+ response = response.replace(/<title[^>]*>[\s\S]*?<\/title>/i, m => (title = this.#parseHTML(m).title, ''));
1081
1095
  let responseWithNoHead = response.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i, '');
1082
1096
  let startTag = responseWithNoHead.match(/<([a-z][^\/>\x20\t\r\n\f]*)/i)?.[1]?.toLowerCase();
1083
1097
 
@@ -1092,49 +1106,28 @@ var htmx = (() => {
1092
1106
  doc = this.#parseHTML(`<template>${responseWithNoHead}</template>`);
1093
1107
  fragment = doc.querySelector('template').content;
1094
1108
  }
1109
+ this.#processScripts(fragment);
1095
1110
 
1096
1111
  return {
1097
1112
  fragment,
1098
- title: doc.title
1113
+ title
1099
1114
  };
1100
1115
  }
1101
1116
 
1102
1117
  #createOOBTask(tasks, elt, oobValue, sourceElement) {
1103
- // Handle legacy format: swapStyle:target (only if no spaces, which indicate modifiers)
1104
1118
  let target = elt.id ? '#' + CSS.escape(elt.id) : null;
1105
1119
  if (oobValue !== 'true' && oobValue && !oobValue.includes(' ')) {
1106
- let colonIdx = oobValue.indexOf(':');
1107
- if (colonIdx !== -1) {
1108
- target = oobValue.substring(colonIdx + 1);
1109
- oobValue = oobValue.substring(0, colonIdx);
1110
- }
1120
+ [oobValue, target = target] = oobValue.split(/:(.*)/);
1111
1121
  }
1112
1122
  if (oobValue === 'true' || !oobValue) oobValue = 'outerHTML';
1113
1123
 
1114
1124
  let swapSpec = this.#parseSwapSpec(oobValue);
1115
- if (swapSpec.target) target = swapSpec.target;
1116
-
1117
- let oobElementClone = elt.cloneNode(true);
1118
- let fragment;
1119
- if (swapSpec.strip === undefined && swapSpec.style !== 'outerHTML') {
1120
- swapSpec.strip = true;
1121
- }
1122
- if (swapSpec.strip) {
1123
- fragment = oobElementClone.content || oobElementClone;
1124
- } else {
1125
- fragment = document.createDocumentFragment();
1126
- fragment.appendChild(oobElementClone);
1127
- }
1128
- elt.remove();
1129
- if (!target && !oobValue.includes('target:')) return;
1130
-
1131
- tasks.push({
1132
- type: 'oob',
1133
- fragment,
1134
- target,
1135
- swapSpec,
1136
- sourceElement
1137
- });
1125
+ target = swapSpec.target || target;
1126
+ swapSpec.strip ??= !swapSpec.style.startsWith('outer');
1127
+ if (!target) return;
1128
+ let fragment = document.createDocumentFragment();
1129
+ fragment.append(elt);
1130
+ tasks.push({type: 'oob', fragment, target, swapSpec, sourceElement});
1138
1131
  }
1139
1132
 
1140
1133
  #processOOB(fragment, sourceElement, selectOOB) {
@@ -1143,9 +1136,7 @@ var htmx = (() => {
1143
1136
  // Process hx-select-oob first (select elements from response)
1144
1137
  if (selectOOB) {
1145
1138
  for (let spec of selectOOB.split(',')) {
1146
- let [selector, ...rest] = spec.split(':');
1147
- let oobValue = rest.length ? rest.join(':') : 'true';
1148
-
1139
+ let [selector, oobValue = 'true'] = spec.split(/:(.*)/);
1149
1140
  for (let elt of fragment.querySelectorAll(selector)) {
1150
1141
  this.#createOOBTask(tasks, elt, oobValue, sourceElement);
1151
1142
  }
@@ -1171,64 +1162,63 @@ var htmx = (() => {
1171
1162
  }
1172
1163
 
1173
1164
  #parseSwapSpec(swapStr) {
1174
- let tokens = this.#tokenize(swapStr);
1175
- let config = {style: tokens[1] === ':' ? this.config.defaultSwap : (tokens[0] || this.config.defaultSwap)};
1176
- config.style = this.#normalizeSwapStyle(config.style);
1177
- let startIdx = tokens[1] === ':' ? 0 : 1;
1178
-
1179
- for (let i = startIdx; i < tokens.length; i++) {
1180
- if (tokens[i + 1] === ':') {
1181
- let key = tokens[i], value = tokens[i = i + 2];
1182
- if (key === 'swap') config.swapDelay = this.parseInterval(value);
1183
- else if (key === 'transition' || key === 'ignoreTitle' || key === 'strip') config[key] = value === 'true';
1184
- else if (key === 'focus-scroll') config.focusScroll = value === 'true';
1185
- else if (key === 'scroll' || key === 'show') {
1186
- let parts = [value];
1187
- while (tokens[i + 1] === ':') {
1188
- parts.push(tokens[i + 2]);
1189
- i += 2;
1190
- }
1191
- config[key] = parts.length === 1 ? parts[0] : parts.pop();
1192
- if (parts.length > 1) config[key + 'Target'] = parts.join(':');
1193
- } else if (key === 'target') {
1194
- let parts = [value];
1195
- while (i + 1 < tokens.length && tokens[i + 1] !== ':' && tokens[i + 2] !== ':') {
1196
- parts.push(tokens[i + 1]);
1197
- i++;
1198
- }
1199
- config[key] = parts.join(' ');
1200
- }
1201
- }
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];
1202
1171
  }
1203
- return config;
1172
+ return {style: this.#normalizeSwapStyle(style), ...this.#parseConfig(swapStr)};
1204
1173
  }
1205
1174
 
1206
- #processPartials(fragment, sourceElement) {
1175
+ #processPartials(fragment, ctx) {
1207
1176
  let tasks = [];
1208
1177
 
1209
- for (let partialElt of fragment.querySelectorAll('template[partial]')) {
1210
- let swapSpec = this.#parseSwapSpec(partialElt.getAttribute(this.#prefix('hx-swap')) || this.config.defaultSwap);
1211
-
1212
- tasks.push({
1213
- type: 'partial',
1214
- fragment: partialElt.content.cloneNode(true),
1215
- target: partialElt.getAttribute(this.#prefix('hx-target')),
1216
- swapSpec,
1217
- sourceElement
1218
- });
1219
- 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();
1220
1195
  }
1221
1196
 
1222
1197
  return tasks;
1223
1198
  }
1224
1199
 
1225
- #handleScroll(target, scroll) {
1226
- if (scroll === 'top') target.scrollTop = 0;
1227
- else if (scroll === 'bottom') target.scrollTop = target.scrollHeight;
1200
+ #handleAutoFocus(elt) {
1201
+ let autofocus = this.find(elt, "[autofocus]");
1202
+ autofocus?.focus?.()
1203
+ }
1204
+
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;
1212
+ }
1213
+ }
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')
1217
+ }
1228
1218
  }
1229
1219
 
1230
1220
  #handleAnchorScroll(ctx) {
1231
- let anchor = ctx.request.originalAction?.split('#')[1];
1221
+ let anchor = ctx.request?.originalAction?.split('#')[1];
1232
1222
  if (anchor) {
1233
1223
  document.getElementById(anchor)?.scrollIntoView({block: 'start', behavior: 'auto'});
1234
1224
  }
@@ -1254,16 +1244,18 @@ var htmx = (() => {
1254
1244
  //============================================================================================
1255
1245
 
1256
1246
  async swap(ctx) {
1247
+ this.#handleHistoryUpdate(ctx);
1257
1248
  let {fragment, title} = this.#makeFragment(ctx.text);
1249
+ ctx.title = title;
1258
1250
  let tasks = [];
1259
1251
 
1260
1252
  // Process OOB and partials
1261
1253
  let oobTasks = this.#processOOB(fragment, ctx.sourceElement, ctx.selectOOB);
1262
- let partialTasks = this.#processPartials(fragment, ctx.sourceElement);
1254
+ let partialTasks = this.#processPartials(fragment, ctx);
1263
1255
  tasks.push(...oobTasks, ...partialTasks);
1264
1256
 
1265
1257
  // Process main swap
1266
- let mainSwap = this.#processMainSwap(ctx, fragment, partialTasks, title);
1258
+ let mainSwap = this.#processMainSwap(ctx, fragment, partialTasks);
1267
1259
  if (mainSwap) {
1268
1260
  tasks.push(mainSwap);
1269
1261
  }
@@ -1281,8 +1273,8 @@ var htmx = (() => {
1281
1273
 
1282
1274
  // insert non-transition tasks immediately or with delay
1283
1275
  for (let task of nonTransitionTasks) {
1284
- if (task.swapSpec?.swapDelay) {
1285
- setTimeout(() => this.#insertContent(task), task.swapSpec.swapDelay);
1276
+ if (task.swapSpec?.swap) {
1277
+ setTimeout(() => this.#insertContent(task), this.parseInterval(task.swapSpec.swap));
1286
1278
  } else {
1287
1279
  this.#insertContent(task)
1288
1280
  }
@@ -1299,7 +1291,7 @@ var htmx = (() => {
1299
1291
  }
1300
1292
 
1301
1293
  this.#trigger(document, "htmx:after:swap", {ctx});
1302
- if (mainSwap?.title) document.title = mainSwap.title;
1294
+ if (ctx.title && !mainSwap?.swapSpec?.ignoreTitle) document.title = ctx.title;
1303
1295
  await this.timeout(1);
1304
1296
  // invoke restore tasks
1305
1297
  for (let task of tasks) {
@@ -1307,38 +1299,32 @@ var htmx = (() => {
1307
1299
  restore()
1308
1300
  }
1309
1301
  }
1310
- this.#trigger(document, "htmx:after:restore", {ctx});
1302
+ this.#trigger(document, "htmx:after:restore", { ctx });
1303
+ this.#handleAnchorScroll(ctx);
1311
1304
  // TODO this stuff should be an extension
1312
1305
  // if (ctx.hx?.triggerafterswap) this.#handleTriggerHeader(ctx.hx.triggerafterswap, ctx.sourceElement);
1313
1306
  }
1314
1307
 
1315
- #processMainSwap(ctx, fragment, partialTasks, title) {
1308
+ #processMainSwap(ctx, fragment, partialTasks) {
1316
1309
  // Create main task if needed
1317
1310
  let swapSpec = this.#parseSwapSpec(ctx.swap || this.config.defaultSwap);
1318
1311
  // skip creating main swap if extracting partials resulted in empty response except for delete style
1319
1312
  if (swapSpec.style === 'delete' || /\S/.test(fragment.innerHTML || '') || !partialTasks.length) {
1320
- let resultFragment = document.createDocumentFragment();
1321
1313
  if (ctx.select) {
1322
- let selected = fragment.querySelector(ctx.select);
1323
- if (selected) {
1324
- if (swapSpec.strip === false) {
1325
- resultFragment.append(selected);
1326
- } else {
1327
- resultFragment.append(...selected.childNodes);
1328
- }
1329
- }
1330
- } else {
1331
- resultFragment.append(...fragment.childNodes);
1314
+ let selected = fragment.querySelectorAll(ctx.select);
1315
+ fragment = document.createDocumentFragment();
1316
+ fragment.append(...selected);
1317
+ }
1318
+ if (this.#isBoosted(ctx.sourceElement)) {
1319
+ swapSpec.show ||= 'top';
1332
1320
  }
1333
-
1334
1321
  let mainSwap = {
1335
1322
  type: 'main',
1336
- fragment: resultFragment,
1337
- target: swapSpec.target || ctx.target,
1323
+ fragment,
1324
+ target: this.#resolveTarget(ctx.sourceElement || document.body, swapSpec.target || ctx.target),
1338
1325
  swapSpec,
1339
1326
  sourceElement: ctx.sourceElement,
1340
- transition: (ctx.transition !== false) && (swapSpec.transition !== false),
1341
- title
1327
+ transition: (ctx.transition !== false) && (swapSpec.transition !== false)
1342
1328
  };
1343
1329
  return mainSwap;
1344
1330
  }
@@ -1350,8 +1336,13 @@ var htmx = (() => {
1350
1336
  target = document.querySelector(target);
1351
1337
  }
1352
1338
  if (!target) return;
1339
+ if (swapSpec.strip && fragment.firstElementChild) {
1340
+ task.unstripped = fragment;
1341
+ fragment = document.createDocumentFragment();
1342
+ fragment.append(...(task.fragment.firstElementChild.content || task.fragment.firstElementChild).childNodes);
1343
+ }
1344
+
1353
1345
  let pantry = this.#handlePreservedElements(fragment);
1354
- this.#processScripts(fragment);
1355
1346
  let parentNode = target.parentNode;
1356
1347
  let newContent = [...fragment.childNodes]
1357
1348
  if (swapSpec.style === 'innerHTML') {
@@ -1391,22 +1382,26 @@ var htmx = (() => {
1391
1382
  return;
1392
1383
  } else if (swapSpec.style === 'none') {
1393
1384
  return;
1394
- } else if (!this.#triggerExtensions(target, 'htmx:handle:swap', task)) {
1395
- return;
1396
1385
  } else {
1386
+ task.target = target;
1387
+ task.fragment = fragment;
1388
+ if (!this.#triggerExtensions(target, 'htmx:handle:swap', task)) return;
1397
1389
  throw new Error(`Unknown swap style: ${swapSpec.style}`);
1398
1390
  }
1399
1391
  this.#restorePreservedElements(pantry);
1400
1392
  for (const elt of newContent) {
1401
- this.process(elt); // maybe only if isConnected?
1393
+ this.process(elt);
1394
+ this.#handleAutoFocus(elt);
1402
1395
  }
1403
- if (swapSpec.scroll) this.#handleScroll(target, swapSpec.scroll);
1396
+ this.#handleScroll(task);
1404
1397
  }
1405
1398
 
1406
1399
  #trigger(on, eventName, detail = {}, bubbles = true) {
1407
1400
  if (this.config.logAll) {
1408
1401
  console.log(eventName, detail, on)
1409
1402
  }
1403
+ on = this.#normalizeElement(on)
1404
+ this.#triggerExtensions(on, this.#maybeAdjustMetaCharacter(eventName), detail);
1410
1405
  return this.trigger(on, eventName, detail, bubbles)
1411
1406
  }
1412
1407
 
@@ -1425,9 +1420,7 @@ var htmx = (() => {
1425
1420
  }
1426
1421
 
1427
1422
  timeout(time) {
1428
- if (typeof time === "string") {
1429
- time = this.parseInterval(time)
1430
- }
1423
+ time = this.parseInterval(time);
1431
1424
  if (time > 0) {
1432
1425
  return new Promise(resolve => setTimeout(resolve, time));
1433
1426
  }
@@ -1449,8 +1442,19 @@ var htmx = (() => {
1449
1442
  })
1450
1443
  }
1451
1444
 
1452
- // on(elt, evt, callback)
1453
- // on(evt, callback)
1445
+ onLoad(callback) {
1446
+ this.on("htmx:after:init", (evt) => {
1447
+ callback(evt.target)
1448
+ })
1449
+ }
1450
+
1451
+ takeClass(element, className, container = element.parentElement) {
1452
+ for (let elt of this.findAll(this.#normalizeElement(container), "." + className)) {
1453
+ elt.classList.remove(className);
1454
+ }
1455
+ element.classList.add(className);
1456
+ }
1457
+
1454
1458
  on(eventOrElt, eventOrCallback, callback) {
1455
1459
  let event;
1456
1460
  let elt = document;
@@ -1474,6 +1478,7 @@ var htmx = (() => {
1474
1478
  }
1475
1479
 
1476
1480
  parseInterval(str) {
1481
+ if (typeof str === 'number') return str;
1477
1482
  let m = {ms: 1, s: 1000, m: 60000};
1478
1483
  let [, n, u] = str?.match(/^([\d.]+)(ms|s|m)?$/) || [];
1479
1484
  let v = parseFloat(n) * (m[u] || 1);
@@ -1482,7 +1487,6 @@ var htmx = (() => {
1482
1487
 
1483
1488
  trigger(on, eventName, detail = {}, bubbles = true) {
1484
1489
  on = this.#normalizeElement(on)
1485
- this.#triggerExtensions(on, eventName, detail);
1486
1490
  let evt = new CustomEvent(eventName, {
1487
1491
  detail,
1488
1492
  cancelable: true,
@@ -1503,17 +1507,22 @@ var htmx = (() => {
1503
1507
 
1504
1508
  let sourceElt = typeof context.source === 'string' ?
1505
1509
  document.querySelector(context.source) : context.source;
1506
- let targetElt = context.target ?
1507
- this.#resolveTarget(sourceElt || document.body, context.target) : sourceElt;
1508
1510
 
1509
- if ((context.target && !targetElt) || (context.source && !sourceElt)) {
1510
- return Promise.reject(new Error('Element not found'));
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
+ }
1515
+
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) {
1519
+ return Promise.reject(new Error('Target not found'));
1511
1520
  }
1512
1521
 
1513
- sourceElt = sourceElt || targetElt || document.body;
1522
+ sourceElt ||= target;
1514
1523
 
1515
1524
  let ctx = this.#createRequestContext(sourceElt, context.event || {});
1516
- Object.assign(ctx, context, {target: targetElt});
1525
+ Object.assign(ctx, context, {target});
1517
1526
  Object.assign(ctx.request, {action: path, method: verb.toUpperCase()});
1518
1527
  if (context.headers) Object.assign(ctx.request.headers, context.headers);
1519
1528
 
@@ -1526,11 +1535,13 @@ var htmx = (() => {
1526
1535
 
1527
1536
  #initHistoryHandling() {
1528
1537
  if (!this.config.history) return;
1529
- // Handle browser back/forward navigation
1538
+ if (!history.state) {
1539
+ history.replaceState({htmx: true}, '', location.pathname + location.search);
1540
+ }
1530
1541
  window.addEventListener('popstate', (event) => {
1531
1542
  if (event.state && event.state.htmx) {
1532
1543
  this.#restoreHistory();
1533
- }
1544
+ }
1534
1545
  });
1535
1546
  }
1536
1547
 
@@ -1554,10 +1565,11 @@ var htmx = (() => {
1554
1565
  } else {
1555
1566
  this.ajax('GET', path, {
1556
1567
  target: 'body',
1557
- swap: 'outerHTML',
1558
1568
  request: {headers: {'HX-History-Restore-Request': 'true'}}
1559
1569
  });
1560
1570
  }
1571
+ } else if (elt.tagName === "FORM") {
1572
+ return elt.method !== 'dialog' && this.#isSameOrigin(elt.action);
1561
1573
  }
1562
1574
  }
1563
1575
 
@@ -1573,7 +1585,7 @@ var htmx = (() => {
1573
1585
  }
1574
1586
 
1575
1587
  let path = push || replace;
1576
- if (!path || path === 'false') return;
1588
+ if (!path || path === 'false' || path === false) return;
1577
1589
 
1578
1590
  if (path === 'true') {
1579
1591
  path = ctx.request.originalAction;
@@ -1597,8 +1609,9 @@ var htmx = (() => {
1597
1609
 
1598
1610
  #handleHxOnAttributes(node) {
1599
1611
  for (let attr of node.getAttributeNames()) {
1600
- if (attr.startsWith(this.#prefix("hx-on:"))) {
1601
- let evtName = attr.substring(this.#prefix("hx-on:").length)
1612
+ var searchString = this.#maybeAdjustMetaCharacter(this.#prefix("hx-on:"));
1613
+ if (attr.startsWith(searchString)) {
1614
+ let evtName = attr.substring(searchString.length)
1602
1615
  let code = node.getAttribute(attr);
1603
1616
  node.addEventListener(evtName, async (evt) => {
1604
1617
  try {
@@ -1662,11 +1675,9 @@ var htmx = (() => {
1662
1675
  }
1663
1676
 
1664
1677
  #collectFormData(elt, form, submitter) {
1665
- let formData = new FormData()
1666
- let included = new Set()
1667
- if (form) {
1668
- this.#addInputValues(form, included, formData)
1669
- } 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) {
1670
1681
  formData.append(elt.name, elt.value)
1671
1682
  included.add(elt);
1672
1683
  }
@@ -1688,21 +1699,21 @@ var htmx = (() => {
1688
1699
  let inputs = this.#queryEltAndDescendants(elt, 'input:not([disabled]), select:not([disabled]), textarea:not([disabled])');
1689
1700
 
1690
1701
  for (let input of inputs) {
1691
- // Skip elements without a name or already seen
1692
1702
  if (!input.name || included.has(input)) continue;
1693
1703
  included.add(input);
1694
1704
 
1695
- if (input.matches('input[type=checkbox], input[type=radio]')) {
1705
+ let type = input.type;
1706
+ if (type === 'checkbox' || type === 'radio') {
1696
1707
  // Only add if checked
1697
1708
  if (input.checked) {
1698
1709
  formData.append(input.name, input.value);
1699
1710
  }
1700
- } else if (input.matches('input[type=file]')) {
1711
+ } else if (type === 'file') {
1701
1712
  // Add all selected files
1702
1713
  for (let file of input.files) {
1703
1714
  formData.append(input.name, file);
1704
1715
  }
1705
- } else if (input.matches('select[multiple]')) {
1716
+ } else if (type === 'select-multiple') {
1706
1717
  // Add all selected options
1707
1718
  for (let option of input.selectedOptions) {
1708
1719
  formData.append(input.name, option.value);
@@ -1717,12 +1728,20 @@ var htmx = (() => {
1717
1728
  #handleHxVals(elt, body) {
1718
1729
  let hxValsValue = this.#attributeValue(elt, "hx-vals");
1719
1730
  if (hxValsValue) {
1720
- if (!hxValsValue.includes('{')) {
1721
- hxValsValue = `{${hxValsValue}}`
1722
- }
1723
- let obj = JSON.parse(hxValsValue);
1724
- for (let key in obj) {
1725
- 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
+ }
1726
1745
  }
1727
1746
  }
1728
1747
  }
@@ -1733,11 +1752,13 @@ var htmx = (() => {
1733
1752
  }
1734
1753
 
1735
1754
  #findAllExt(eltOrSelector, maybeSelector, global) {
1736
- let [elt, selector] = this.#normalizeElementAndSelector(eltOrSelector, maybeSelector)
1755
+ let selector = maybeSelector ?? eltOrSelector;
1756
+ let elt = maybeSelector ? this.#normalizeElement(eltOrSelector) : document;
1737
1757
  if (selector.startsWith('global ')) {
1738
1758
  return this.#findAllExt(elt, selector.slice(7), true);
1739
1759
  }
1740
- 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, ',')) : [];
1741
1762
  let result = []
1742
1763
  let unprocessedParts = []
1743
1764
  for (const part of parts) {
@@ -1783,28 +1804,6 @@ var htmx = (() => {
1783
1804
  return result
1784
1805
  }
1785
1806
 
1786
- #normalizeElementAndSelector(eltOrSelector, selector) {
1787
- if (selector === undefined) {
1788
- return [document, eltOrSelector];
1789
- } else {
1790
- return [this.#normalizeElement(eltOrSelector), selector];
1791
- }
1792
- }
1793
-
1794
- #tokenizeExtendedSelector(selector) {
1795
- let parts = [], depth = 0, start = 0;
1796
- for (let i = 0; i <= selector.length; i++) {
1797
- let c = selector[i];
1798
- if (c === '<') depth++;
1799
- else if (c === '/' && selector[i + 1] === '>') depth--;
1800
- else if ((c === ',' && !depth) || i === selector.length) {
1801
- if (i > start) parts.push(selector.substring(start, i));
1802
- start = i + 1;
1803
- }
1804
- }
1805
- return parts;
1806
- }
1807
-
1808
1807
  #scanForwardQuery(start, match, global) {
1809
1808
  return this.#scanUntilComparison(this.#getRootNode(start, global).querySelectorAll(match), start, Node.DOCUMENT_POSITION_PRECEDING);
1810
1809
  }
@@ -2060,16 +2059,18 @@ var htmx = (() => {
2060
2059
 
2061
2060
  #handleStatusCodes(ctx) {
2062
2061
  let status = ctx.response.raw.status;
2063
- if (this.config.noSwap.includes(status)) {
2064
- ctx.swap = "none";
2065
- }
2062
+ let noSwapStrings = this.config.noSwap.map(x => x + "");
2066
2063
  let str = status + ""
2067
2064
  for (let pattern of [str, str.slice(0, 2) + 'x', str[0] + 'xx']) {
2068
- let swap = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
2069
- if (swap) {
2070
- ctx.swap = swap
2065
+ if (noSwapStrings.includes(pattern)) {
2066
+ ctx.swap = "none";
2071
2067
  return
2072
2068
  }
2069
+ let statusValue = this.#attributeValue(ctx.sourceElement, "hx-status:" + pattern);
2070
+ if (statusValue) {
2071
+ Object.assign(ctx, this.#parseConfig(statusValue));
2072
+ return;
2073
+ }
2073
2074
  }
2074
2075
  }
2075
2076
 
@@ -2083,7 +2084,7 @@ var htmx = (() => {
2083
2084
  });
2084
2085
  }
2085
2086
 
2086
- async __processTransitionQueue() {
2087
+ async #processTransitionQueue() {
2087
2088
  if (this.#transitionQueue.length === 0 || this.#processingTransition) {
2088
2089
  return;
2089
2090
  }
@@ -2093,9 +2094,8 @@ var htmx = (() => {
2093
2094
 
2094
2095
  try {
2095
2096
  if (document.startViewTransition) {
2096
- let finished = document.startViewTransition(task).finished;
2097
- this.#trigger(document, "htmx:before:viewTransition", {task, finished})
2098
- await finished;
2097
+ this.#trigger(document, "htmx:before:viewTransition", {task})
2098
+ await document.startViewTransition(task).finished;
2099
2099
  this.#trigger(document, "htmx:after:viewTransition", {task})
2100
2100
  } else {
2101
2101
  task();
@@ -2133,8 +2133,17 @@ var htmx = (() => {
2133
2133
  return cssOrElement
2134
2134
  }
2135
2135
  }
2136
+
2137
+ #maybeAdjustMetaCharacter(string) {
2138
+ if (this.config.metaCharacter) {
2139
+ return string.replace(/:/g, this.config.metaCharacter);
2140
+ } else {
2141
+ return string;
2142
+ }
2143
+ }
2136
2144
  }
2137
2145
 
2138
2146
  return new Htmx()
2139
2147
  })()
2148
+
2140
2149
  export default htmx