htmx.org 2.0.0-alpha1 → 2.0.0-alpha2

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.amd.js CHANGED
@@ -1,31 +1,36 @@
1
1
  define(() => {
2
- const htmx = (function () {
2
+ var htmx = (function() {
3
3
  'use strict'
4
4
 
5
5
  // Public API
6
6
  //* * @type {import("./htmx").HtmxApi} */
7
- // TODO: list all methods in public API
8
7
  const htmx = {
8
+ /* Event processing */
9
9
  onLoad: onLoadHelper,
10
10
  process: processNode,
11
11
  on: addEventListenerImpl,
12
12
  off: removeEventListenerImpl,
13
13
  trigger: triggerEvent,
14
14
  ajax: ajaxHelper,
15
+ /* DOM querying helpers */
15
16
  find,
16
17
  findAll,
17
18
  closest,
18
- values: function (elt, type) {
19
+ values: function(elt, type) {
19
20
  const inputValues = getInputValues(elt, type || 'post')
20
21
  return inputValues.values
21
22
  },
23
+ /* DOM manipulation helpers */
22
24
  remove: removeElement,
23
25
  addClass: addClassToElement,
24
26
  removeClass: removeClassFromElement,
25
27
  toggleClass: toggleClassOnElement,
26
28
  takeClass: takeClassForElement,
29
+ swap,
30
+ /* Extension entrypoints */
27
31
  defineExtension,
28
32
  removeExtension,
33
+ /* Debugging */
29
34
  logAll,
30
35
  logNone,
31
36
  logger: null,
@@ -51,7 +56,6 @@ const htmx = (function () {
51
56
  wsReconnectDelay: 'full-jitter',
52
57
  wsBinaryType: 'blob',
53
58
  disableSelector: '[hx-disable], [data-hx-disable]',
54
- useTemplateFragments: false,
55
59
  scrollBehavior: 'instant',
56
60
  defaultFocusScroll: false,
57
61
  getCacheBusterParam: false,
@@ -60,11 +64,17 @@ const htmx = (function () {
60
64
  selfRequestsOnly: true,
61
65
  ignoreTitle: false,
62
66
  scrollIntoViewOnBoost: true,
63
- triggerSpecsCache: null
67
+ triggerSpecsCache: null,
68
+ disableInheritance: false,
69
+ responseHandling: [
70
+ { code: '204', swap: false },
71
+ { code: '[23]..', swap: true },
72
+ { code: '[45]..', swap: false, error: true }
73
+ ]
64
74
  },
65
75
  parseInterval,
66
76
  _: internalEval,
67
- version: '1.9.10'
77
+ version: '2.0a'
68
78
  }
69
79
 
70
80
  /** @type {import("./htmx").HtmxInternalApi} */
@@ -74,6 +84,7 @@ const htmx = (function () {
74
84
  canAccessLocalStorage,
75
85
  findThisElement,
76
86
  filterValues,
87
+ swap,
77
88
  hasAttribute,
78
89
  getAttributeValue,
79
90
  getClosestAttributeValue,
@@ -90,7 +101,6 @@ const htmx = (function () {
90
101
  makeSettleInfo,
91
102
  oobSwap,
92
103
  querySelectorExt,
93
- selectAndSwap,
94
104
  settleImmediately,
95
105
  shouldCancel,
96
106
  triggerEvent,
@@ -99,13 +109,11 @@ const htmx = (function () {
99
109
  }
100
110
 
101
111
  const VERBS = ['get', 'post', 'put', 'delete', 'patch']
102
- const VERB_SELECTOR = VERBS.map(function (verb) {
112
+ const VERB_SELECTOR = VERBS.map(function(verb) {
103
113
  return '[hx-' + verb + '], [data-hx-' + verb + ']'
104
114
  }).join(', ')
105
115
 
106
116
  const HEAD_TAG_REGEX = makeTagRegEx('head')
107
- const TITLE_TAG_REGEX = makeTagRegEx('title')
108
- const SVG_TAGS_REGEX = makeTagRegEx('svg', true)
109
117
 
110
118
  //= ===================================================================
111
119
  // Utilities
@@ -116,12 +124,12 @@ const htmx = (function () {
116
124
  * @param {boolean} global
117
125
  * @returns {RegExp}
118
126
  */
119
- function makeTagRegEx (tag, global = false) {
127
+ function makeTagRegEx(tag, global = false) {
120
128
  return new RegExp(`<${tag}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${tag}>`,
121
129
  global ? 'gim' : 'im')
122
130
  }
123
131
 
124
- function parseInterval (str) {
132
+ function parseInterval(str) {
125
133
  if (str == undefined) {
126
134
  return undefined
127
135
  }
@@ -140,16 +148,16 @@ const htmx = (function () {
140
148
  }
141
149
 
142
150
  /**
143
- * @param {HTMLElement} elt
151
+ * @param {Element} elt
144
152
  * @param {string} name
145
153
  * @returns {(string | null)}
146
154
  */
147
- function getRawAttribute (elt, name) {
155
+ function getRawAttribute(elt, name) {
148
156
  return elt.getAttribute && elt.getAttribute(name)
149
157
  }
150
158
 
151
159
  // resolve with both hx and data-hx prefixes
152
- function hasAttribute (elt, qualifiedName) {
160
+ function hasAttribute(elt, qualifiedName) {
153
161
  return elt.hasAttribute && (elt.hasAttribute(qualifiedName) ||
154
162
  elt.hasAttribute('data-' + qualifiedName))
155
163
  }
@@ -160,7 +168,7 @@ const htmx = (function () {
160
168
  * @param {string} qualifiedName
161
169
  * @returns {(string | null)}
162
170
  */
163
- function getAttributeValue (elt, qualifiedName) {
171
+ function getAttributeValue(elt, qualifiedName) {
164
172
  return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, 'data-' + qualifiedName)
165
173
  }
166
174
 
@@ -168,7 +176,7 @@ const htmx = (function () {
168
176
  * @param {HTMLElement} elt
169
177
  * @returns {HTMLElement | ShadowRoot | null}
170
178
  */
171
- function parentElt (elt) {
179
+ function parentElt(elt) {
172
180
  const parent = elt.parentElement
173
181
  if (!parent && elt.parentNode instanceof ShadowRoot) return elt.parentNode
174
182
  return parent
@@ -177,14 +185,14 @@ const htmx = (function () {
177
185
  /**
178
186
  * @returns {Document}
179
187
  */
180
- function getDocument () {
188
+ function getDocument() {
181
189
  return document
182
190
  }
183
191
 
184
192
  /**
185
193
  * @returns {Document | ShadowRoot}
186
194
  */
187
- function getRootNode (elt, global) {
195
+ function getRootNode(elt, global) {
188
196
  return elt.getRootNode ? elt.getRootNode({ composed: global }) : getDocument()
189
197
  }
190
198
 
@@ -193,7 +201,7 @@ const htmx = (function () {
193
201
  * @param {(e:HTMLElement) => boolean} condition
194
202
  * @returns {HTMLElement | null}
195
203
  */
196
- function getClosestMatch (elt, condition) {
204
+ function getClosestMatch(elt, condition) {
197
205
  while (elt && !condition(elt)) {
198
206
  elt = parentElt(elt)
199
207
  }
@@ -201,14 +209,23 @@ const htmx = (function () {
201
209
  return elt || null
202
210
  }
203
211
 
204
- function getAttributeValueWithDisinheritance (initialElement, ancestor, attributeName) {
212
+ function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName) {
205
213
  const attributeValue = getAttributeValue(ancestor, attributeName)
206
214
  const disinherit = getAttributeValue(ancestor, 'hx-disinherit')
207
- if (initialElement !== ancestor && disinherit && (disinherit === '*' || disinherit.split(' ').indexOf(attributeName) >= 0)) {
208
- return 'unset'
209
- } else {
210
- return attributeValue
215
+ var inherit = getAttributeValue(ancestor, 'hx-inherit')
216
+ if (initialElement !== ancestor) {
217
+ if (htmx.config.disableInheritance) {
218
+ if (inherit && (inherit === '*' || inherit.split(' ').indexOf(attributeName) >= 0)) {
219
+ return attributeValue
220
+ } else {
221
+ return null
222
+ }
223
+ }
224
+ if (disinherit && (disinherit === '*' || disinherit.split(' ').indexOf(attributeName) >= 0)) {
225
+ return 'unset'
226
+ }
211
227
  }
228
+ return attributeValue
212
229
  }
213
230
 
214
231
  /**
@@ -216,9 +233,9 @@ const htmx = (function () {
216
233
  * @param {string} attributeName
217
234
  * @returns {string | null}
218
235
  */
219
- function getClosestAttributeValue (elt, attributeName) {
236
+ function getClosestAttributeValue(elt, attributeName) {
220
237
  let closestAttr = null
221
- getClosestMatch(elt, function (e) {
238
+ getClosestMatch(elt, function(e) {
222
239
  return closestAttr = getAttributeValueWithDisinheritance(elt, e, attributeName)
223
240
  })
224
241
  if (closestAttr !== 'unset') {
@@ -231,7 +248,7 @@ const htmx = (function () {
231
248
  * @param {string} selector
232
249
  * @returns {boolean}
233
250
  */
234
- function matches (elt, selector) {
251
+ function matches(elt, selector) {
235
252
  // @ts-ignore: non-standard properties for browser compatibility
236
253
  // noinspection JSUnresolvedVariable
237
254
  const matchesFunction = elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector
@@ -242,7 +259,7 @@ const htmx = (function () {
242
259
  * @param {string} str
243
260
  * @returns {string}
244
261
  */
245
- function getStartTag (str) {
262
+ function getStartTag(str) {
246
263
  const tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i
247
264
  const match = tagMatcher.exec(str)
248
265
  if (match) {
@@ -256,74 +273,107 @@ const htmx = (function () {
256
273
  *
257
274
  * @param {string} resp
258
275
  * @param {number} depth
259
- * @returns {Element}
276
+ * @returns {Document}
260
277
  */
261
- function parseHTML (resp, depth) {
278
+ function parseHTML(resp) {
262
279
  const parser = new DOMParser()
263
- const responseDoc = parser.parseFromString(resp, 'text/html')
280
+ return parser.parseFromString(resp, 'text/html')
281
+ }
264
282
 
265
- /** @type {Element} */
266
- let responseNode = responseDoc.body
267
- while (depth > 0) {
268
- depth--
269
- // @ts-ignore
270
- responseNode = responseNode.firstChild
283
+ function takeChildrenFor(fragment, elt) {
284
+ while (elt.childNodes.length > 0) {
285
+ fragment.append(elt.childNodes[0])
271
286
  }
272
- if (responseNode == null) {
273
- // @ts-ignore
274
- responseNode = getDocument().createDocumentFragment()
287
+ }
288
+
289
+ function duplicateScript(script) {
290
+ const newScript = getDocument().createElement('script')
291
+ forEach(script.attributes, function(attr) {
292
+ newScript.setAttribute(attr.name, attr.value)
293
+ })
294
+ newScript.textContent = script.textContent
295
+ newScript.async = false
296
+ if (htmx.config.inlineScriptNonce) {
297
+ newScript.nonce = htmx.config.inlineScriptNonce
275
298
  }
276
- return responseNode
299
+ return newScript
277
300
  }
278
301
 
279
- function aFullPageResponse (resp) {
280
- return /<body/.test(resp)
302
+ function isJavaScriptScriptNode(script) {
303
+ return script.matches('script') && (script.type === 'text/javascript' || script.type === 'module' || script.type === '')
304
+ }
305
+
306
+ // we have to make new copies of script tags that we are going to insert because
307
+ // SOME browsers (not saying who, but it involves an element and an animal) don't
308
+ // execute scripts created in <template> tags when they are inserted into the DOM
309
+ // and all the others do lmao
310
+ function normalizeScriptTags(fragment) {
311
+ Array.from(fragment.querySelectorAll('script')).forEach((script) => {
312
+ if (isJavaScriptScriptNode(script)) {
313
+ const newScript = duplicateScript(script)
314
+ const parent = script.parentNode
315
+ try {
316
+ parent.insertBefore(newScript, script)
317
+ } catch (e) {
318
+ logError(e)
319
+ } finally {
320
+ script.remove()
321
+ }
322
+ }
323
+ })
281
324
  }
282
325
 
283
326
  /**
284
- *
285
- * @param {string} response
286
- * @returns {Element}
327
+ * @param {string} response HTML
328
+ * @returns {DocumentFragment & {title: string}} a document fragment representing the response HTML, including
329
+ * a `title` property for any title information found
287
330
  */
288
- function makeFragment (response) {
289
- const partialResponse = !aFullPageResponse(response)
290
- const startTag = getStartTag(response)
291
- let content = response
292
- if (startTag === 'head') {
293
- content = content.replace(HEAD_TAG_REGEX, '')
294
- }
295
- if (htmx.config.useTemplateFragments && partialResponse) {
296
- const documentFragment = parseHTML('<body><template>' + content + '</template></body>', 0)
297
- // @ts-ignore type mismatch between DocumentFragment and Element.
298
- // TODO: Are these close enough for htmx to use interchangeably?
299
- return documentFragment.querySelector('template').content
300
- }
301
- switch (startTag) {
302
- case 'thead':
303
- case 'tbody':
304
- case 'tfoot':
305
- case 'colgroup':
306
- case 'caption':
307
- return parseHTML('<table>' + content + '</table>', 1)
308
- case 'col':
309
- return parseHTML('<table><colgroup>' + content + '</colgroup></table>', 2)
310
- case 'tr':
311
- return parseHTML('<table><tbody>' + content + '</tbody></table>', 2)
312
- case 'td':
313
- case 'th':
314
- return parseHTML('<table><tbody><tr>' + content + '</tr></tbody></table>', 3)
315
- case 'script':
316
- case 'style':
317
- return parseHTML('<div>' + content + '</div>', 1)
318
- default:
319
- return parseHTML(content, 0)
331
+ function makeFragment(response) {
332
+ // strip head tag to determine shape of response we are dealing with
333
+ const responseWithNoHead = response.replace(HEAD_TAG_REGEX, '')
334
+ const startTag = getStartTag(responseWithNoHead)
335
+ let fragment = null
336
+ if (startTag === 'html') {
337
+ // if it is a full document, parse it and return the body
338
+ fragment = new DocumentFragment()
339
+ const doc = parseHTML(response)
340
+ takeChildrenFor(fragment, doc.body)
341
+ fragment.title = doc.title
342
+ } else if (startTag === 'body') {
343
+ // parse body w/o wrapping in template
344
+ fragment = new DocumentFragment()
345
+ const doc = parseHTML(responseWithNoHead)
346
+ takeChildrenFor(fragment, doc.body)
347
+ fragment.title = doc.title
348
+ } else {
349
+ // otherwise we have non-body partial HTML content, so wrap it in a template to maximize parsing flexibility
350
+ const doc = parseHTML('<body><template class="internal-htmx-wrapper">' + responseWithNoHead + '</template></body>')
351
+ fragment = doc.querySelector('template').content
352
+ // extract title into fragment for later processing
353
+ fragment.title = doc.title
354
+
355
+ // for legacy reasons we support a title tag at the root level of non-body responses, so we need to handle it
356
+ var titleElement = fragment.querySelector('title')
357
+ if (titleElement && titleElement.parentNode === fragment) {
358
+ titleElement.remove()
359
+ fragment.title = titleElement.innerText
360
+ }
320
361
  }
362
+ if (fragment) {
363
+ if (htmx.config.allowScriptTags) {
364
+ normalizeScriptTags(fragment)
365
+ } else {
366
+ // remove all script tags if scripts are disabled
367
+ fragment.querySelectorAll('script').forEach((script) => script.remove())
368
+ }
369
+ }
370
+ return fragment
321
371
  }
322
372
 
323
373
  /**
324
374
  * @param {Function} func
325
375
  */
326
- function maybeCall (func) {
376
+ function maybeCall(func) {
327
377
  if (func) {
328
378
  func()
329
379
  }
@@ -334,7 +384,7 @@ const htmx = (function () {
334
384
  * @param {string} type
335
385
  * @returns
336
386
  */
337
- function isType (o, type) {
387
+ function isType(o, type) {
338
388
  return Object.prototype.toString.call(o) === '[object ' + type + ']'
339
389
  }
340
390
 
@@ -342,7 +392,7 @@ const htmx = (function () {
342
392
  * @param {*} o
343
393
  * @returns {o is Function}
344
394
  */
345
- function isFunction (o) {
395
+ function isFunction(o) {
346
396
  return isType(o, 'Function')
347
397
  }
348
398
 
@@ -350,7 +400,7 @@ const htmx = (function () {
350
400
  * @param {*} o
351
401
  * @returns {o is Object}
352
402
  */
353
- function isRawObject (o) {
403
+ function isRawObject(o) {
354
404
  return isType(o, 'Object')
355
405
  }
356
406
 
@@ -359,7 +409,7 @@ const htmx = (function () {
359
409
  * @param {HTMLElement} elt
360
410
  * @returns {*}
361
411
  */
362
- function getInternalData (elt) {
412
+ function getInternalData(elt) {
363
413
  const dataProp = 'htmx-internal-data'
364
414
  let data = elt[dataProp]
365
415
  if (!data) {
@@ -373,7 +423,7 @@ const htmx = (function () {
373
423
  * @param {ArrayLike} arr
374
424
  * @returns {any[]}
375
425
  */
376
- function toArray (arr) {
426
+ function toArray(arr) {
377
427
  const returnArr = []
378
428
  if (arr) {
379
429
  for (let i = 0; i < arr.length; i++) {
@@ -383,7 +433,17 @@ const htmx = (function () {
383
433
  return returnArr
384
434
  }
385
435
 
386
- function forEach (arr, func) {
436
+ /**
437
+ * @template T
438
+ * @callback forEachCallback
439
+ * @param {T} value
440
+ */
441
+ /**
442
+ * @template T
443
+ * @param {{[index: number]: T, length: number}} arr
444
+ * @param {forEachCallback<T>} func
445
+ */
446
+ function forEach(arr, func) {
387
447
  if (arr) {
388
448
  for (let i = 0; i < arr.length; i++) {
389
449
  func(arr[i])
@@ -391,14 +451,14 @@ const htmx = (function () {
391
451
  }
392
452
  }
393
453
 
394
- function isScrolledIntoView (el) {
454
+ function isScrolledIntoView(el) {
395
455
  const rect = el.getBoundingClientRect()
396
456
  const elemTop = rect.top
397
457
  const elemBottom = rect.bottom
398
458
  return elemTop < window.innerHeight && elemBottom >= 0
399
459
  }
400
460
 
401
- function bodyContains (elt) {
461
+ function bodyContains(elt) {
402
462
  // IE Fix
403
463
  if (elt.getRootNode && elt.getRootNode() instanceof window.ShadowRoot) {
404
464
  return getDocument().body.contains(elt.getRootNode().host)
@@ -407,7 +467,7 @@ const htmx = (function () {
407
467
  }
408
468
  }
409
469
 
410
- function splitOnWhitespace (trigger) {
470
+ function splitOnWhitespace(trigger) {
411
471
  return trigger.trim().split(/\s+/)
412
472
  }
413
473
 
@@ -418,7 +478,7 @@ const htmx = (function () {
418
478
  * @param {Object} obj2
419
479
  * @returns {Object}
420
480
  */
421
- function mergeObjects (obj1, obj2) {
481
+ function mergeObjects(obj1, obj2) {
422
482
  for (const key in obj2) {
423
483
  if (obj2.hasOwnProperty(key)) {
424
484
  obj1[key] = obj2[key]
@@ -427,7 +487,7 @@ const htmx = (function () {
427
487
  return obj1
428
488
  }
429
489
 
430
- function parseJSON (jString) {
490
+ function parseJSON(jString) {
431
491
  try {
432
492
  return JSON.parse(jString)
433
493
  } catch (error) {
@@ -436,7 +496,7 @@ const htmx = (function () {
436
496
  }
437
497
  }
438
498
 
439
- function canAccessLocalStorage () {
499
+ function canAccessLocalStorage() {
440
500
  const test = 'htmx:localStorageTest'
441
501
  try {
442
502
  localStorage.setItem(test, test)
@@ -447,7 +507,7 @@ const htmx = (function () {
447
507
  }
448
508
  }
449
509
 
450
- function normalizePath (path) {
510
+ function normalizePath(path) {
451
511
  try {
452
512
  const url = new URL(path)
453
513
  if (url) {
@@ -468,32 +528,32 @@ const htmx = (function () {
468
528
  // public API
469
529
  //= =========================================================================================
470
530
 
471
- function internalEval (str) {
472
- return maybeEval(getDocument().body, function () {
531
+ function internalEval(str) {
532
+ return maybeEval(getDocument().body, function() {
473
533
  return eval(str)
474
534
  })
475
535
  }
476
536
 
477
- function onLoadHelper (callback) {
478
- const value = htmx.on('htmx:load', function (evt) {
537
+ function onLoadHelper(callback) {
538
+ const value = htmx.on('htmx:load', function(evt) {
479
539
  callback(evt.detail.elt)
480
540
  })
481
541
  return value
482
542
  }
483
543
 
484
- function logAll () {
485
- htmx.logger = function (elt, event, data) {
544
+ function logAll() {
545
+ htmx.logger = function(elt, event, data) {
486
546
  if (console) {
487
547
  console.log(event, elt, data)
488
548
  }
489
549
  }
490
550
  }
491
551
 
492
- function logNone () {
552
+ function logNone() {
493
553
  htmx.logger = null
494
554
  }
495
555
 
496
- function find (eltOrSelector, selector) {
556
+ function find(eltOrSelector, selector) {
497
557
  if (selector) {
498
558
  return eltOrSelector.querySelector(selector)
499
559
  } else {
@@ -501,7 +561,7 @@ const htmx = (function () {
501
561
  }
502
562
  }
503
563
 
504
- function findAll (eltOrSelector, selector) {
564
+ function findAll(eltOrSelector, selector) {
505
565
  if (selector) {
506
566
  return eltOrSelector.querySelectorAll(selector)
507
567
  } else {
@@ -509,10 +569,10 @@ const htmx = (function () {
509
569
  }
510
570
  }
511
571
 
512
- function removeElement (elt, delay) {
572
+ function removeElement(elt, delay) {
513
573
  elt = resolveTarget(elt)
514
574
  if (delay) {
515
- setTimeout(function () {
575
+ setTimeout(function() {
516
576
  removeElement(elt)
517
577
  elt = null
518
578
  }, delay)
@@ -521,10 +581,10 @@ const htmx = (function () {
521
581
  }
522
582
  }
523
583
 
524
- function addClassToElement (elt, clazz, delay) {
584
+ function addClassToElement(elt, clazz, delay) {
525
585
  elt = resolveTarget(elt)
526
586
  if (delay) {
527
- setTimeout(function () {
587
+ setTimeout(function() {
528
588
  addClassToElement(elt, clazz)
529
589
  elt = null
530
590
  }, delay)
@@ -533,10 +593,10 @@ const htmx = (function () {
533
593
  }
534
594
  }
535
595
 
536
- function removeClassFromElement (elt, clazz, delay) {
596
+ function removeClassFromElement(elt, clazz, delay) {
537
597
  elt = resolveTarget(elt)
538
598
  if (delay) {
539
- setTimeout(function () {
599
+ setTimeout(function() {
540
600
  removeClassFromElement(elt, clazz)
541
601
  elt = null
542
602
  }, delay)
@@ -551,20 +611,20 @@ const htmx = (function () {
551
611
  }
552
612
  }
553
613
 
554
- function toggleClassOnElement (elt, clazz) {
614
+ function toggleClassOnElement(elt, clazz) {
555
615
  elt = resolveTarget(elt)
556
616
  elt.classList.toggle(clazz)
557
617
  }
558
618
 
559
- function takeClassForElement (elt, clazz) {
619
+ function takeClassForElement(elt, clazz) {
560
620
  elt = resolveTarget(elt)
561
- forEach(elt.parentElement.children, function (child) {
621
+ forEach(elt.parentElement.children, function(child) {
562
622
  removeClassFromElement(child, clazz)
563
623
  })
564
624
  addClassToElement(elt, clazz)
565
625
  }
566
626
 
567
- function closest (elt, selector) {
627
+ function closest(elt, selector) {
568
628
  elt = resolveTarget(elt)
569
629
  if (elt.closest) {
570
630
  return elt.closest(selector)
@@ -580,15 +640,15 @@ const htmx = (function () {
580
640
  }
581
641
  }
582
642
 
583
- function startsWith (str, prefix) {
643
+ function startsWith(str, prefix) {
584
644
  return str.substring(0, prefix.length) === prefix
585
645
  }
586
646
 
587
- function endsWith (str, suffix) {
647
+ function endsWith(str, suffix) {
588
648
  return str.substring(str.length - suffix.length) === suffix
589
649
  }
590
650
 
591
- function normalizeSelector (selector) {
651
+ function normalizeSelector(selector) {
592
652
  const trimmedSelector = selector.trim()
593
653
  if (startsWith(trimmedSelector, '<') && endsWith(trimmedSelector, '/>')) {
594
654
  return trimmedSelector.substring(1, trimmedSelector.length - 2)
@@ -597,7 +657,7 @@ const htmx = (function () {
597
657
  }
598
658
  }
599
659
 
600
- function querySelectorAllExt (elt, selector, global) {
660
+ function querySelectorAllExt(elt, selector, global) {
601
661
  if (selector.indexOf('closest ') === 0) {
602
662
  return [closest(elt, normalizeSelector(selector.substr(8)))]
603
663
  } else if (selector.indexOf('find ') === 0) {
@@ -625,7 +685,7 @@ const htmx = (function () {
625
685
  }
626
686
  }
627
687
 
628
- var scanForwardQuery = function (start, match, global) {
688
+ var scanForwardQuery = function(start, match, global) {
629
689
  const results = getRootNode(start, global).querySelectorAll(match)
630
690
  for (let i = 0; i < results.length; i++) {
631
691
  const elt = results[i]
@@ -635,7 +695,7 @@ const htmx = (function () {
635
695
  }
636
696
  }
637
697
 
638
- var scanBackwardsQuery = function (start, match, global) {
698
+ var scanBackwardsQuery = function(start, match, global) {
639
699
  const results = getRootNode(start, global).querySelectorAll(match)
640
700
  for (let i = results.length - 1; i >= 0; i--) {
641
701
  const elt = results[i]
@@ -645,7 +705,7 @@ const htmx = (function () {
645
705
  }
646
706
  }
647
707
 
648
- function querySelectorExt (eltOrSelector, selector) {
708
+ function querySelectorExt(eltOrSelector, selector) {
649
709
  if (selector) {
650
710
  return querySelectorAllExt(eltOrSelector, selector)[0]
651
711
  } else {
@@ -653,15 +713,22 @@ const htmx = (function () {
653
713
  }
654
714
  }
655
715
 
656
- function resolveTarget (arg2, context) {
716
+ /**
717
+ *
718
+ * @param {string|Element} arg2
719
+ * @param {Element} [context]
720
+ * @returns {Element}
721
+ */
722
+ function resolveTarget(arg2, context) {
657
723
  if (isType(arg2, 'String')) {
658
724
  return find(context || document, arg2)
659
725
  } else {
726
+ // @ts-ignore
660
727
  return arg2
661
728
  }
662
729
  }
663
730
 
664
- function processEventArgs (arg1, arg2, arg3) {
731
+ function processEventArgs(arg1, arg2, arg3) {
665
732
  if (isFunction(arg2)) {
666
733
  return {
667
734
  target: getDocument().body,
@@ -677,8 +744,8 @@ const htmx = (function () {
677
744
  }
678
745
  }
679
746
 
680
- function addEventListenerImpl (arg1, arg2, arg3) {
681
- ready(function () {
747
+ function addEventListenerImpl(arg1, arg2, arg3) {
748
+ ready(function() {
682
749
  const eventArgs = processEventArgs(arg1, arg2, arg3)
683
750
  eventArgs.target.addEventListener(eventArgs.event, eventArgs.listener)
684
751
  })
@@ -686,8 +753,8 @@ const htmx = (function () {
686
753
  return b ? arg2 : arg3
687
754
  }
688
755
 
689
- function removeEventListenerImpl (arg1, arg2, arg3) {
690
- ready(function () {
756
+ function removeEventListenerImpl(arg1, arg2, arg3) {
757
+ ready(function() {
691
758
  const eventArgs = processEventArgs(arg1, arg2, arg3)
692
759
  eventArgs.target.removeEventListener(eventArgs.event, eventArgs.listener)
693
760
  })
@@ -699,7 +766,7 @@ const htmx = (function () {
699
766
  //= ===================================================================
700
767
 
701
768
  const DUMMY_ELT = getDocument().createElement('output') // dummy element for bad selectors
702
- function findAttributeTargets (elt, attrName) {
769
+ function findAttributeTargets(elt, attrName) {
703
770
  const attrTarget = getClosestAttributeValue(elt, attrName)
704
771
  if (attrTarget) {
705
772
  if (attrTarget === 'this') {
@@ -716,13 +783,13 @@ const htmx = (function () {
716
783
  }
717
784
  }
718
785
 
719
- function findThisElement (elt, attribute) {
720
- return getClosestMatch(elt, function (elt) {
786
+ function findThisElement(elt, attribute) {
787
+ return getClosestMatch(elt, function(elt) {
721
788
  return getAttributeValue(elt, attribute) != null
722
789
  })
723
790
  }
724
791
 
725
- function getTarget (elt) {
792
+ function getTarget(elt) {
726
793
  const targetStr = getClosestAttributeValue(elt, 'hx-target')
727
794
  if (targetStr) {
728
795
  if (targetStr === 'this') {
@@ -740,7 +807,7 @@ const htmx = (function () {
740
807
  }
741
808
  }
742
809
 
743
- function shouldSettleAttribute (name) {
810
+ function shouldSettleAttribute(name) {
744
811
  const attributesToSettle = htmx.config.attributesToSettle
745
812
  for (let i = 0; i < attributesToSettle.length; i++) {
746
813
  if (name === attributesToSettle[i]) {
@@ -750,20 +817,20 @@ const htmx = (function () {
750
817
  return false
751
818
  }
752
819
 
753
- function cloneAttributes (mergeTo, mergeFrom) {
754
- forEach(mergeTo.attributes, function (attr) {
820
+ function cloneAttributes(mergeTo, mergeFrom) {
821
+ forEach(mergeTo.attributes, function(attr) {
755
822
  if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) {
756
823
  mergeTo.removeAttribute(attr.name)
757
824
  }
758
825
  })
759
- forEach(mergeFrom.attributes, function (attr) {
826
+ forEach(mergeFrom.attributes, function(attr) {
760
827
  if (shouldSettleAttribute(attr.name)) {
761
828
  mergeTo.setAttribute(attr.name, attr.value)
762
829
  }
763
830
  })
764
831
  }
765
832
 
766
- function isInlineSwap (swapStyle, target) {
833
+ function isInlineSwap(swapStyle, target) {
767
834
  const extensions = getExtensions(target)
768
835
  for (let i = 0; i < extensions.length; i++) {
769
836
  const extension = extensions[i]
@@ -781,11 +848,11 @@ const htmx = (function () {
781
848
  /**
782
849
  *
783
850
  * @param {string} oobValue
784
- * @param {HTMLElement} oobElement
851
+ * @param {Element} oobElement
785
852
  * @param {*} settleInfo
786
853
  * @returns
787
854
  */
788
- function oobSwap (oobValue, oobElement, settleInfo) {
855
+ function oobSwap(oobValue, oobElement, settleInfo) {
789
856
  let selector = '#' + getRawAttribute(oobElement, 'id')
790
857
  let swapStyle = 'outerHTML'
791
858
  if (oobValue === 'true') {
@@ -801,7 +868,7 @@ const htmx = (function () {
801
868
  if (targets) {
802
869
  forEach(
803
870
  targets,
804
- function (target) {
871
+ function(target) {
805
872
  let fragment
806
873
  const oobElementClone = oobElement.cloneNode(true)
807
874
  fragment = getDocument().createDocumentFragment()
@@ -815,9 +882,9 @@ const htmx = (function () {
815
882
 
816
883
  target = beforeSwapDetails.target // allow re-targeting
817
884
  if (beforeSwapDetails.shouldSwap) {
818
- swap(swapStyle, target, target, fragment, settleInfo)
885
+ swapWithStyle(swapStyle, target, target, fragment, settleInfo)
819
886
  }
820
- forEach(settleInfo.elts, function (elt) {
887
+ forEach(settleInfo.elts, function(elt) {
821
888
  triggerEvent(elt, 'htmx:oobAfterSwap', beforeSwapDetails)
822
889
  })
823
890
  }
@@ -830,33 +897,8 @@ const htmx = (function () {
830
897
  return oobValue
831
898
  }
832
899
 
833
- function handleOutOfBandSwaps (elt, fragment, settleInfo) {
834
- const oobSelects = getClosestAttributeValue(elt, 'hx-select-oob')
835
- if (oobSelects) {
836
- const oobSelectValues = oobSelects.split(',')
837
- for (let i = 0; i < oobSelectValues.length; i++) {
838
- const oobSelectValue = oobSelectValues[i].split(':', 2)
839
- let id = oobSelectValue[0].trim()
840
- if (id.indexOf('#') === 0) {
841
- id = id.substring(1)
842
- }
843
- const oobValue = oobSelectValue[1] || 'true'
844
- const oobElement = fragment.querySelector('#' + id)
845
- if (oobElement) {
846
- oobSwap(oobValue, oobElement, settleInfo)
847
- }
848
- }
849
- }
850
- forEach(findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]'), function (oobElement) {
851
- const oobValue = getAttributeValue(oobElement, 'hx-swap-oob')
852
- if (oobValue != null) {
853
- oobSwap(oobValue, oobElement, settleInfo)
854
- }
855
- })
856
- }
857
-
858
- function handlePreservedElements (fragment) {
859
- forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function (preservedElt) {
900
+ function handlePreservedElements(fragment) {
901
+ forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function(preservedElt) {
860
902
  const id = getAttributeValue(preservedElt, 'id')
861
903
  const oldElt = getDocument().getElementById(id)
862
904
  if (oldElt != null) {
@@ -865,8 +907,8 @@ const htmx = (function () {
865
907
  })
866
908
  }
867
909
 
868
- function handleAttributes (parentNode, fragment, settleInfo) {
869
- forEach(fragment.querySelectorAll('[id]'), function (newNode) {
910
+ function handleAttributes(parentNode, fragment, settleInfo) {
911
+ forEach(fragment.querySelectorAll('[id]'), function(newNode) {
870
912
  const id = getRawAttribute(newNode, 'id')
871
913
  if (id && id.length > 0) {
872
914
  const normalizedId = id.replace("'", "\\'")
@@ -875,7 +917,7 @@ const htmx = (function () {
875
917
  if (oldNode && oldNode !== parentNode) {
876
918
  const newAttributes = newNode.cloneNode()
877
919
  cloneAttributes(newNode, oldNode)
878
- settleInfo.tasks.push(function () {
920
+ settleInfo.tasks.push(function() {
879
921
  cloneAttributes(newNode, newAttributes)
880
922
  })
881
923
  }
@@ -883,17 +925,16 @@ const htmx = (function () {
883
925
  })
884
926
  }
885
927
 
886
- function makeAjaxLoadTask (child) {
887
- return function () {
928
+ function makeAjaxLoadTask(child) {
929
+ return function() {
888
930
  removeClassFromElement(child, htmx.config.addedClass)
889
931
  processNode(child)
890
- processScripts(child)
891
932
  processFocus(child)
892
933
  triggerEvent(child, 'htmx:load')
893
934
  }
894
935
  }
895
936
 
896
- function processFocus (child) {
937
+ function processFocus(child) {
897
938
  const autofocus = '[autofocus]'
898
939
  const autoFocusedElt = matches(child, autofocus) ? child : child.querySelector(autofocus)
899
940
  if (autoFocusedElt != null) {
@@ -901,7 +942,7 @@ const htmx = (function () {
901
942
  }
902
943
  }
903
944
 
904
- function insertNodesBefore (parentNode, insertBefore, fragment, settleInfo) {
945
+ function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
905
946
  handleAttributes(parentNode, fragment, settleInfo)
906
947
  while (fragment.childNodes.length > 0) {
907
948
  const child = fragment.firstChild
@@ -915,7 +956,7 @@ const htmx = (function () {
915
956
 
916
957
  // based on https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0,
917
958
  // derived from Java's string hashcode implementation
918
- function stringHash (string, hash) {
959
+ function stringHash(string, hash) {
919
960
  let char = 0
920
961
  while (char < string.length) {
921
962
  hash = (hash << 5) - hash + string.charCodeAt(char++) | 0 // bitwise or ensures we have a 32-bit int
@@ -923,7 +964,7 @@ const htmx = (function () {
923
964
  return hash
924
965
  }
925
966
 
926
- function attributeHash (elt) {
967
+ function attributeHash(elt) {
927
968
  let hash = 0
928
969
  // IE fix
929
970
  if (elt.attributes) {
@@ -938,7 +979,7 @@ const htmx = (function () {
938
979
  return hash
939
980
  }
940
981
 
941
- function deInitOnHandlers (elt) {
982
+ function deInitOnHandlers(elt) {
942
983
  const internalData = getInternalData(elt)
943
984
  if (internalData.onHandlers) {
944
985
  for (let i = 0; i < internalData.onHandlers.length; i++) {
@@ -949,76 +990,72 @@ const htmx = (function () {
949
990
  }
950
991
  }
951
992
 
952
- function deInitNode (element) {
993
+ function deInitNode(element) {
953
994
  const internalData = getInternalData(element)
954
995
  if (internalData.timeout) {
955
996
  clearTimeout(internalData.timeout)
956
997
  }
957
998
  if (internalData.listenerInfos) {
958
- forEach(internalData.listenerInfos, function (info) {
999
+ forEach(internalData.listenerInfos, function(info) {
959
1000
  if (info.on) {
960
1001
  info.on.removeEventListener(info.trigger, info.listener)
961
1002
  }
962
1003
  })
963
1004
  }
964
1005
  deInitOnHandlers(element)
965
- forEach(Object.keys(internalData), function (key) { delete internalData[key] })
1006
+ forEach(Object.keys(internalData), function(key) { delete internalData[key] })
966
1007
  }
967
1008
 
968
- function cleanUpElement (element) {
1009
+ function cleanUpElement(element) {
969
1010
  triggerEvent(element, 'htmx:beforeCleanupElement')
970
1011
  deInitNode(element)
971
1012
  if (element.children) { // IE
972
- forEach(element.children, function (child) { cleanUpElement(child) })
1013
+ forEach(element.children, function(child) { cleanUpElement(child) })
973
1014
  }
974
1015
  }
975
1016
 
976
- function swapOuterHTML (target, fragment, settleInfo) {
977
- if (target.tagName === 'BODY') {
978
- return swapInnerHTML(target, fragment, settleInfo)
1017
+ function swapOuterHTML(target, fragment, settleInfo) {
1018
+ // @type {HTMLElement}
1019
+ let newElt
1020
+ const eltBeforeNewContent = target.previousSibling
1021
+ insertNodesBefore(parentElt(target), target, fragment, settleInfo)
1022
+ if (eltBeforeNewContent == null) {
1023
+ newElt = parentElt(target).firstChild
979
1024
  } else {
980
- // @type {HTMLElement}
981
- let newElt
982
- const eltBeforeNewContent = target.previousSibling
983
- insertNodesBefore(parentElt(target), target, fragment, settleInfo)
984
- if (eltBeforeNewContent == null) {
985
- newElt = parentElt(target).firstChild
986
- } else {
987
- newElt = eltBeforeNewContent.nextSibling
988
- }
989
- settleInfo.elts = settleInfo.elts.filter(function (e) { return e != target })
990
- while (newElt && newElt !== target) {
991
- if (newElt.nodeType === Node.ELEMENT_NODE) {
992
- settleInfo.elts.push(newElt)
993
- }
994
- newElt = newElt.nextElementSibling
1025
+ newElt = eltBeforeNewContent.nextSibling
1026
+ }
1027
+ settleInfo.elts = settleInfo.elts.filter(function(e) { return e !== target })
1028
+ while (newElt && newElt !== target) {
1029
+ if (newElt.nodeType === Node.ELEMENT_NODE) {
1030
+ settleInfo.elts.push(newElt)
995
1031
  }
996
- cleanUpElement(target)
997
- parentElt(target).removeChild(target)
1032
+ newElt = newElt.nextElementSibling
998
1033
  }
1034
+ cleanUpElement(target)
1035
+ target.remove()
999
1036
  }
1000
1037
 
1001
- function swapAfterBegin (target, fragment, settleInfo) {
1038
+ function swapAfterBegin(target, fragment, settleInfo) {
1002
1039
  return insertNodesBefore(target, target.firstChild, fragment, settleInfo)
1003
1040
  }
1004
1041
 
1005
- function swapBeforeBegin (target, fragment, settleInfo) {
1042
+ function swapBeforeBegin(target, fragment, settleInfo) {
1006
1043
  return insertNodesBefore(parentElt(target), target, fragment, settleInfo)
1007
1044
  }
1008
1045
 
1009
- function swapBeforeEnd (target, fragment, settleInfo) {
1046
+ function swapBeforeEnd(target, fragment, settleInfo) {
1010
1047
  return insertNodesBefore(target, null, fragment, settleInfo)
1011
1048
  }
1012
1049
 
1013
- function swapAfterEnd (target, fragment, settleInfo) {
1050
+ function swapAfterEnd(target, fragment, settleInfo) {
1014
1051
  return insertNodesBefore(parentElt(target), target.nextSibling, fragment, settleInfo)
1015
1052
  }
1016
- function swapDelete (target, fragment, settleInfo) {
1053
+ function swapDelete(target, fragment, settleInfo) {
1017
1054
  cleanUpElement(target)
1018
1055
  return parentElt(target).removeChild(target)
1019
1056
  }
1020
1057
 
1021
- function swapInnerHTML (target, fragment, settleInfo) {
1058
+ function swapInnerHTML(target, fragment, settleInfo) {
1022
1059
  const firstChild = target.firstChild
1023
1060
  insertNodesBefore(target, firstChild, fragment, settleInfo)
1024
1061
  if (firstChild) {
@@ -1031,19 +1068,14 @@ const htmx = (function () {
1031
1068
  }
1032
1069
  }
1033
1070
 
1034
- function maybeSelectFromResponse (elt, fragment, selectOverride) {
1035
- const selector = selectOverride || getClosestAttributeValue(elt, 'hx-select')
1036
- if (selector) {
1037
- const newFragment = getDocument().createDocumentFragment()
1038
- forEach(fragment.querySelectorAll(selector), function (node) {
1039
- newFragment.appendChild(node)
1040
- })
1041
- fragment = newFragment
1042
- }
1043
- return fragment
1044
- }
1045
-
1046
- function swap (swapStyle, elt, target, fragment, settleInfo) {
1071
+ /**
1072
+ * @param {string} swapStyle
1073
+ * @param {HTMLElement} elt
1074
+ * @param {HTMLElement} target
1075
+ * @param {Node} fragment
1076
+ * @param {{ tasks: (() => void)[]; }} settleInfo
1077
+ */
1078
+ function swapWithStyle(swapStyle, elt, target, fragment, settleInfo) {
1047
1079
  switch (swapStyle) {
1048
1080
  case 'none':
1049
1081
  return
@@ -1090,33 +1122,176 @@ const htmx = (function () {
1090
1122
  if (swapStyle === 'innerHTML') {
1091
1123
  swapInnerHTML(target, fragment, settleInfo)
1092
1124
  } else {
1093
- swap(htmx.config.defaultSwapStyle, elt, target, fragment, settleInfo)
1125
+ swapWithStyle(htmx.config.defaultSwapStyle, elt, target, fragment, settleInfo)
1094
1126
  }
1095
1127
  }
1096
1128
  }
1097
1129
 
1098
- function findTitle (content) {
1099
- if (content.indexOf('<title') > -1) {
1100
- const contentWithSvgsRemoved = content.replace(SVG_TAGS_REGEX, '')
1101
- const result = contentWithSvgsRemoved.match(TITLE_TAG_REGEX)
1102
- if (result) {
1103
- return result[2]
1130
+ function findAndSwapOobElements(fragment, settleInfo) {
1131
+ forEach(findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]'), function(oobElement) {
1132
+ const oobValue = getAttributeValue(oobElement, 'hx-swap-oob')
1133
+ if (oobValue != null) {
1134
+ oobSwap(oobValue, oobElement, settleInfo)
1104
1135
  }
1105
- }
1136
+ })
1106
1137
  }
1107
1138
 
1108
- function selectAndSwap (swapStyle, target, elt, responseText, settleInfo, selectOverride) {
1109
- settleInfo.title = findTitle(responseText)
1110
- let fragment = makeFragment(responseText)
1111
- if (fragment) {
1112
- handleOutOfBandSwaps(elt, fragment, settleInfo)
1113
- fragment = maybeSelectFromResponse(elt, fragment, selectOverride)
1114
- handlePreservedElements(fragment)
1115
- return swap(swapStyle, elt, target, fragment, settleInfo)
1139
+ /**
1140
+ * @callback swapCallback
1141
+ */
1142
+
1143
+ /**
1144
+ * @typedef {Object} SwapOptions
1145
+ * @property {?string} select
1146
+ * @property {?string} selectOOB
1147
+ * @property {?*} eventInfo
1148
+ * @property {?*} anchor
1149
+ * @property {?HTMLElement} contextElement
1150
+ * @property {?swapCallback} afterSwapCallback
1151
+ * @property {?swapCallback} afterSettleCallback
1152
+ */
1153
+
1154
+ /**
1155
+ * Implements complete swapping pipeline, including: focus and selection preservation,
1156
+ * title updates, scroll, OOB swapping, normal swapping and settling
1157
+ * @param {string|Element} target
1158
+ * @param {string} content
1159
+ * @param {import("./htmx").HtmxSwapSpecification} swapSpec
1160
+ * @param {SwapOptions} swapOptions
1161
+ */
1162
+ function swap(target, content, swapSpec, swapOptions) {
1163
+ if (!swapOptions) {
1164
+ swapOptions = {}
1165
+ }
1166
+
1167
+ target = resolveTarget(target)
1168
+
1169
+ // preserve focus and selection
1170
+ const activeElt = document.activeElement
1171
+ let selectionInfo = {}
1172
+ try {
1173
+ selectionInfo = {
1174
+ elt: activeElt,
1175
+ // @ts-ignore
1176
+ start: activeElt ? activeElt.selectionStart : null,
1177
+ // @ts-ignore
1178
+ end: activeElt ? activeElt.selectionEnd : null
1179
+ }
1180
+ } catch (e) {
1181
+ // safari issue - see https://github.com/microsoft/playwright/issues/5894
1182
+ }
1183
+ const settleInfo = makeSettleInfo(target)
1184
+
1185
+ let fragment = makeFragment(content)
1186
+
1187
+ settleInfo.title = fragment.title
1188
+
1189
+ // select-oob swaps
1190
+ if (swapOptions.selectOOB) {
1191
+ const oobSelectValues = swapOptions.selectOOB.split(',')
1192
+ for (let i = 0; i < oobSelectValues.length; i++) {
1193
+ const oobSelectValue = oobSelectValues[i].split(':', 2)
1194
+ let id = oobSelectValue[0].trim()
1195
+ if (id.indexOf('#') === 0) {
1196
+ id = id.substring(1)
1197
+ }
1198
+ const oobValue = oobSelectValue[1] || 'true'
1199
+ const oobElement = fragment.querySelector('#' + id)
1200
+ if (oobElement) {
1201
+ oobSwap(oobValue, oobElement, settleInfo)
1202
+ }
1203
+ }
1204
+ }
1205
+ // oob swaps
1206
+ findAndSwapOobElements(fragment, settleInfo)
1207
+ forEach(findAll(fragment, 'template'), function(template) {
1208
+ findAndSwapOobElements(template.content, settleInfo)
1209
+ if (template.content.childElementCount === 0) {
1210
+ // Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
1211
+ template.remove()
1212
+ }
1213
+ })
1214
+
1215
+ // normal swap
1216
+ if (swapOptions.select) {
1217
+ const newFragment = getDocument().createDocumentFragment()
1218
+ forEach(fragment.querySelectorAll(swapOptions.select), function(node) {
1219
+ newFragment.appendChild(node)
1220
+ })
1221
+ fragment = newFragment
1222
+ }
1223
+ handlePreservedElements(fragment)
1224
+ swapWithStyle(swapSpec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo)
1225
+
1226
+ // apply saved focus and selection information to swapped content
1227
+ if (selectionInfo.elt &&
1228
+ !bodyContains(selectionInfo.elt) &&
1229
+ getRawAttribute(selectionInfo.elt, 'id')) {
1230
+ const newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, 'id'))
1231
+ const focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }
1232
+ if (newActiveElt) {
1233
+ // @ts-ignore
1234
+ if (selectionInfo.start && newActiveElt.setSelectionRange) {
1235
+ try {
1236
+ // @ts-ignore
1237
+ newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end)
1238
+ } catch (e) {
1239
+ // the setSelectionRange method is present on fields that don't support it, so just let this fail
1240
+ }
1241
+ }
1242
+ newActiveElt.focus(focusOptions)
1243
+ }
1244
+ }
1245
+
1246
+ target.classList.remove(htmx.config.swappingClass)
1247
+ forEach(settleInfo.elts, function(elt) {
1248
+ if (elt.classList) {
1249
+ elt.classList.add(htmx.config.settlingClass)
1250
+ }
1251
+ triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo)
1252
+ })
1253
+ if (swapOptions.afterSwapCallback) {
1254
+ swapOptions.afterSwapCallback()
1255
+ }
1256
+
1257
+ // merge in new title after swap but before settle
1258
+ if (!swapSpec.ignoreTitle) {
1259
+ handleTitle(settleInfo.title)
1260
+ }
1261
+
1262
+ // settle
1263
+ const doSettle = function() {
1264
+ forEach(settleInfo.tasks, function(task) {
1265
+ task.call()
1266
+ })
1267
+ forEach(settleInfo.elts, function(elt) {
1268
+ if (elt.classList) {
1269
+ elt.classList.remove(htmx.config.settlingClass)
1270
+ }
1271
+ triggerEvent(elt, 'htmx:afterSettle', swapOptions.eventInfo)
1272
+ })
1273
+
1274
+ if (swapOptions.anchor) {
1275
+ const anchorTarget = resolveTarget('#' + swapOptions.anchor)
1276
+ if (anchorTarget) {
1277
+ anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
1278
+ }
1279
+ }
1280
+
1281
+ updateScrollState(settleInfo.elts, swapSpec)
1282
+ if (swapOptions.afterSettleCallback) {
1283
+ swapOptions.afterSettleCallback()
1284
+ }
1285
+ }
1286
+
1287
+ if (swapSpec.settleDelay > 0) {
1288
+ setTimeout(doSettle, swapSpec.settleDelay)
1289
+ } else {
1290
+ doSettle()
1116
1291
  }
1117
1292
  }
1118
1293
 
1119
- function handleTrigger (xhr, header, elt) {
1294
+ function handleTriggerHeader(xhr, header, elt) {
1120
1295
  const triggerBody = xhr.getResponseHeader(header)
1121
1296
  if (triggerBody.indexOf('{') === 0) {
1122
1297
  const triggers = parseJSON(triggerBody)
@@ -1145,7 +1320,7 @@ const htmx = (function () {
1145
1320
  const NOT_WHITESPACE = /[^\s]/
1146
1321
  const COMBINED_SELECTOR_START = /[{(]/
1147
1322
  const COMBINED_SELECTOR_END = /[})]/
1148
- function tokenizeString (str) {
1323
+ function tokenizeString(str) {
1149
1324
  const tokens = []
1150
1325
  let position = 0
1151
1326
  while (position < str.length) {
@@ -1175,7 +1350,7 @@ const htmx = (function () {
1175
1350
  return tokens
1176
1351
  }
1177
1352
 
1178
- function isPossibleRelativeReference (token, last, paramName) {
1353
+ function isPossibleRelativeReference(token, last, paramName) {
1179
1354
  return SYMBOL_START.exec(token.charAt(0)) &&
1180
1355
  token !== 'true' &&
1181
1356
  token !== 'false' &&
@@ -1184,7 +1359,7 @@ const htmx = (function () {
1184
1359
  last !== '.'
1185
1360
  }
1186
1361
 
1187
- function maybeGenerateConditional (elt, tokens, paramName) {
1362
+ function maybeGenerateConditional(elt, tokens, paramName) {
1188
1363
  if (tokens[0] === '[') {
1189
1364
  tokens.shift()
1190
1365
  let bracketCount = 1
@@ -1201,10 +1376,10 @@ const htmx = (function () {
1201
1376
  tokens.shift()
1202
1377
  conditionalSource += ')})'
1203
1378
  try {
1204
- const conditionFunction = maybeEval(elt, function () {
1379
+ const conditionFunction = maybeEval(elt, function() {
1205
1380
  return Function(conditionalSource)()
1206
1381
  },
1207
- function () { return true })
1382
+ function() { return true })
1208
1383
  conditionFunction.source = conditionalSource
1209
1384
  return conditionFunction
1210
1385
  } catch (e) {
@@ -1225,7 +1400,7 @@ const htmx = (function () {
1225
1400
  }
1226
1401
  }
1227
1402
 
1228
- function consumeUntil (tokens, match) {
1403
+ function consumeUntil(tokens, match) {
1229
1404
  let result = ''
1230
1405
  while (tokens.length > 0 && !match.test(tokens[0])) {
1231
1406
  result += tokens.shift()
@@ -1233,7 +1408,7 @@ const htmx = (function () {
1233
1408
  return result
1234
1409
  }
1235
1410
 
1236
- function consumeCSSSelector (tokens) {
1411
+ function consumeCSSSelector(tokens) {
1237
1412
  let result
1238
1413
  if (tokens.length > 0 && COMBINED_SELECTOR_START.test(tokens[0])) {
1239
1414
  tokens.shift()
@@ -1253,7 +1428,7 @@ const htmx = (function () {
1253
1428
  * @param {cache} cache for trigger specs
1254
1429
  * @returns {import("./htmx").HtmxTriggerSpecification[]}
1255
1430
  */
1256
- function parseAndCacheTrigger (elt, explicitTrigger, cache) {
1431
+ function parseAndCacheTrigger(elt, explicitTrigger, cache) {
1257
1432
  const triggerSpecs = []
1258
1433
  const tokens = tokenizeString(explicitTrigger)
1259
1434
  do {
@@ -1342,7 +1517,7 @@ const htmx = (function () {
1342
1517
  * @param {HTMLElement} elt
1343
1518
  * @returns {import("./htmx").HtmxTriggerSpecification[]}
1344
1519
  */
1345
- function getTriggerSpecs (elt) {
1520
+ function getTriggerSpecs(elt) {
1346
1521
  const explicitTrigger = getAttributeValue(elt, 'hx-trigger')
1347
1522
  let triggerSpecs = []
1348
1523
  if (explicitTrigger) {
@@ -1363,13 +1538,13 @@ const htmx = (function () {
1363
1538
  }
1364
1539
  }
1365
1540
 
1366
- function cancelPolling (elt) {
1541
+ function cancelPolling(elt) {
1367
1542
  getInternalData(elt).cancelled = true
1368
1543
  }
1369
1544
 
1370
- function processPolling (elt, handler, spec) {
1545
+ function processPolling(elt, handler, spec) {
1371
1546
  const nodeData = getInternalData(elt)
1372
- nodeData.timeout = setTimeout(function () {
1547
+ nodeData.timeout = setTimeout(function() {
1373
1548
  if (bodyContains(elt) && nodeData.cancelled !== true) {
1374
1549
  if (!maybeFilterEvent(spec, elt, makeEvent('hx:poll:trigger', {
1375
1550
  triggerSpec: spec,
@@ -1382,13 +1557,13 @@ const htmx = (function () {
1382
1557
  }, spec.pollInterval)
1383
1558
  }
1384
1559
 
1385
- function isLocalLink (elt) {
1560
+ function isLocalLink(elt) {
1386
1561
  return location.hostname === elt.hostname &&
1387
1562
  getRawAttribute(elt, 'href') &&
1388
1563
  getRawAttribute(elt, 'href').indexOf('#') !== 0
1389
1564
  }
1390
1565
 
1391
- function boostElement (elt, nodeData, triggerSpecs) {
1566
+ function boostElement(elt, nodeData, triggerSpecs) {
1392
1567
  if ((elt.tagName === 'A' && isLocalLink(elt) && (elt.target === '' || elt.target === '_self')) || elt.tagName === 'FORM') {
1393
1568
  nodeData.boosted = true
1394
1569
  let verb, path
@@ -1402,8 +1577,8 @@ const htmx = (function () {
1402
1577
  }
1403
1578
  path = getRawAttribute(elt, 'action')
1404
1579
  }
1405
- triggerSpecs.forEach(function (triggerSpec) {
1406
- addEventListener(elt, function (elt, evt) {
1580
+ triggerSpecs.forEach(function(triggerSpec) {
1581
+ addEventListener(elt, function(elt, evt) {
1407
1582
  if (closest(elt, htmx.config.disableSelector)) {
1408
1583
  cleanUpElement(elt)
1409
1584
  return
@@ -1420,7 +1595,7 @@ const htmx = (function () {
1420
1595
  * @param {HTMLElement} elt
1421
1596
  * @returns
1422
1597
  */
1423
- function shouldCancel (evt, elt) {
1598
+ function shouldCancel(evt, elt) {
1424
1599
  if (evt.type === 'submit' || evt.type === 'click') {
1425
1600
  if (elt.tagName === 'FORM') {
1426
1601
  return true
@@ -1436,11 +1611,11 @@ const htmx = (function () {
1436
1611
  return false
1437
1612
  }
1438
1613
 
1439
- function ignoreBoostedAnchorCtrlClick (elt, evt) {
1614
+ function ignoreBoostedAnchorCtrlClick(elt, evt) {
1440
1615
  return getInternalData(elt).boosted && elt.tagName === 'A' && evt.type === 'click' && (evt.ctrlKey || evt.metaKey)
1441
1616
  }
1442
1617
 
1443
- function maybeFilterEvent (triggerSpec, elt, evt) {
1618
+ function maybeFilterEvent(triggerSpec, elt, evt) {
1444
1619
  const eventFilter = triggerSpec.eventFilter
1445
1620
  if (eventFilter) {
1446
1621
  try {
@@ -1453,7 +1628,7 @@ const htmx = (function () {
1453
1628
  return false
1454
1629
  }
1455
1630
 
1456
- function addEventListener (elt, handler, nodeData, triggerSpec, explicitCancel) {
1631
+ function addEventListener(elt, handler, nodeData, triggerSpec, explicitCancel) {
1457
1632
  const elementData = getInternalData(elt)
1458
1633
  let eltsToListenOn
1459
1634
  if (triggerSpec.from) {
@@ -1463,13 +1638,13 @@ const htmx = (function () {
1463
1638
  }
1464
1639
  // store the initial values of the elements, so we can tell if they change
1465
1640
  if (triggerSpec.changed) {
1466
- eltsToListenOn.forEach(function (eltToListenOn) {
1641
+ eltsToListenOn.forEach(function(eltToListenOn) {
1467
1642
  const eltToListenOnData = getInternalData(eltToListenOn)
1468
1643
  eltToListenOnData.lastValue = eltToListenOn.value
1469
1644
  })
1470
1645
  }
1471
- forEach(eltsToListenOn, function (eltToListenOn) {
1472
- const eventListener = function (evt) {
1646
+ forEach(eltsToListenOn, function(eltToListenOn) {
1647
+ const eventListener = function(evt) {
1473
1648
  if (!bodyContains(elt)) {
1474
1649
  eltToListenOn.removeEventListener(triggerSpec.trigger, eventListener)
1475
1650
  return
@@ -1522,12 +1697,12 @@ const htmx = (function () {
1522
1697
  if (triggerSpec.throttle > 0) {
1523
1698
  if (!elementData.throttle) {
1524
1699
  handler(elt, evt)
1525
- elementData.throttle = setTimeout(function () {
1700
+ elementData.throttle = setTimeout(function() {
1526
1701
  elementData.throttle = null
1527
1702
  }, triggerSpec.throttle)
1528
1703
  }
1529
1704
  } else if (triggerSpec.delay > 0) {
1530
- elementData.delayed = setTimeout(function () { handler(elt, evt) }, triggerSpec.delay)
1705
+ elementData.delayed = setTimeout(function() { handler(elt, evt) }, triggerSpec.delay)
1531
1706
  } else {
1532
1707
  triggerEvent(elt, 'htmx:trigger')
1533
1708
  handler(elt, evt)
@@ -1548,16 +1723,16 @@ const htmx = (function () {
1548
1723
 
1549
1724
  let windowIsScrolling = false // used by initScrollHandler
1550
1725
  let scrollHandler = null
1551
- function initScrollHandler () {
1726
+ function initScrollHandler() {
1552
1727
  if (!scrollHandler) {
1553
- scrollHandler = function () {
1728
+ scrollHandler = function() {
1554
1729
  windowIsScrolling = true
1555
1730
  }
1556
1731
  window.addEventListener('scroll', scrollHandler)
1557
- setInterval(function () {
1732
+ setInterval(function() {
1558
1733
  if (windowIsScrolling) {
1559
1734
  windowIsScrolling = false
1560
- forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
1735
+ forEach(getDocument().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"), function(elt) {
1561
1736
  maybeReveal(elt)
1562
1737
  })
1563
1738
  }
@@ -1565,7 +1740,7 @@ const htmx = (function () {
1565
1740
  }
1566
1741
  }
1567
1742
 
1568
- function maybeReveal (elt) {
1743
+ function maybeReveal(elt) {
1569
1744
  if (!hasAttribute(elt, 'data-hx-revealed') && isScrolledIntoView(elt)) {
1570
1745
  elt.setAttribute('data-hx-revealed', 'true')
1571
1746
  const nodeData = getInternalData(elt)
@@ -1573,15 +1748,15 @@ const htmx = (function () {
1573
1748
  triggerEvent(elt, 'revealed')
1574
1749
  } else {
1575
1750
  // if the node isn't initialized, wait for it before triggering the request
1576
- elt.addEventListener('htmx:afterProcessNode', function (evt) { triggerEvent(elt, 'revealed') }, { once: true })
1751
+ elt.addEventListener('htmx:afterProcessNode', function(evt) { triggerEvent(elt, 'revealed') }, { once: true })
1577
1752
  }
1578
1753
  }
1579
1754
  }
1580
1755
 
1581
1756
  //= ===================================================================
1582
1757
 
1583
- function loadImmediately (elt, handler, nodeData, delay) {
1584
- const load = function () {
1758
+ function loadImmediately(elt, handler, nodeData, delay) {
1759
+ const load = function() {
1585
1760
  if (!nodeData.loaded) {
1586
1761
  nodeData.loaded = true
1587
1762
  handler(elt)
@@ -1594,16 +1769,16 @@ const htmx = (function () {
1594
1769
  }
1595
1770
  }
1596
1771
 
1597
- function processVerbs (elt, nodeData, triggerSpecs) {
1772
+ function processVerbs(elt, nodeData, triggerSpecs) {
1598
1773
  let explicitAction = false
1599
- forEach(VERBS, function (verb) {
1774
+ forEach(VERBS, function(verb) {
1600
1775
  if (hasAttribute(elt, 'hx-' + verb)) {
1601
1776
  const path = getAttributeValue(elt, 'hx-' + verb)
1602
1777
  explicitAction = true
1603
1778
  nodeData.path = path
1604
1779
  nodeData.verb = verb
1605
- triggerSpecs.forEach(function (triggerSpec) {
1606
- addTriggerHandler(elt, triggerSpec, nodeData, function (elt, evt) {
1780
+ triggerSpecs.forEach(function(triggerSpec) {
1781
+ addTriggerHandler(elt, triggerSpec, nodeData, function(elt, evt) {
1607
1782
  if (closest(elt, htmx.config.disableSelector)) {
1608
1783
  cleanUpElement(elt)
1609
1784
  return
@@ -1616,7 +1791,7 @@ const htmx = (function () {
1616
1791
  return explicitAction
1617
1792
  }
1618
1793
 
1619
- function addTriggerHandler (elt, triggerSpec, nodeData, handler) {
1794
+ function addTriggerHandler(elt, triggerSpec, nodeData, handler) {
1620
1795
  if (triggerSpec.trigger === 'revealed') {
1621
1796
  initScrollHandler()
1622
1797
  addEventListener(elt, handler, nodeData, triggerSpec)
@@ -1629,7 +1804,7 @@ const htmx = (function () {
1629
1804
  if (triggerSpec.threshold) {
1630
1805
  observerOptions.threshold = parseFloat(triggerSpec.threshold)
1631
1806
  }
1632
- const observer = new IntersectionObserver(function (entries) {
1807
+ const observer = new IntersectionObserver(function(entries) {
1633
1808
  for (let i = 0; i < entries.length; i++) {
1634
1809
  const entry = entries[i]
1635
1810
  if (entry.isIntersecting) {
@@ -1652,42 +1827,7 @@ const htmx = (function () {
1652
1827
  }
1653
1828
  }
1654
1829
 
1655
- function evalScript (script) {
1656
- if (htmx.config.allowScriptTags && (script.type === 'text/javascript' || script.type === 'module' || script.type === '')) {
1657
- const newScript = getDocument().createElement('script')
1658
- forEach(script.attributes, function (attr) {
1659
- newScript.setAttribute(attr.name, attr.value)
1660
- })
1661
- newScript.textContent = script.textContent
1662
- newScript.async = false
1663
- if (htmx.config.inlineScriptNonce) {
1664
- newScript.nonce = htmx.config.inlineScriptNonce
1665
- }
1666
- const parent = script.parentElement
1667
-
1668
- try {
1669
- parent.insertBefore(newScript, script)
1670
- } catch (e) {
1671
- logError(e)
1672
- } finally {
1673
- // remove old script element, but only if it is still in DOM
1674
- if (script.parentElement) {
1675
- script.parentElement.removeChild(script)
1676
- }
1677
- }
1678
- }
1679
- }
1680
-
1681
- function processScripts (elt) {
1682
- if (matches(elt, 'script')) {
1683
- evalScript(elt)
1684
- }
1685
- forEach(findAll(elt, 'script'), function (script) {
1686
- evalScript(script)
1687
- })
1688
- }
1689
-
1690
- function shouldProcessHxOn (elt) {
1830
+ function shouldProcessHxOn(elt) {
1691
1831
  const attributes = elt.attributes
1692
1832
  for (let j = 0; j < attributes.length; j++) {
1693
1833
  const attrName = attributes[j].name
@@ -1699,7 +1839,7 @@ const htmx = (function () {
1699
1839
  return false
1700
1840
  }
1701
1841
 
1702
- function findHxOnWildcardElements (elt) {
1842
+ function findHxOnWildcardElements(elt) {
1703
1843
  let node = null
1704
1844
  const elements = []
1705
1845
 
@@ -1715,7 +1855,7 @@ const htmx = (function () {
1715
1855
  return elements
1716
1856
  }
1717
1857
 
1718
- function findElementsToProcess (elt) {
1858
+ function findElementsToProcess(elt) {
1719
1859
  if (elt.querySelectorAll) {
1720
1860
  const boostedSelector = ', [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]'
1721
1861
  const results = elt.querySelectorAll(VERB_SELECTOR + boostedSelector + ", form, [type='submit']," +
@@ -1728,20 +1868,20 @@ const htmx = (function () {
1728
1868
 
1729
1869
  // Handle submit buttons/inputs that have the form attribute set
1730
1870
  // see https://developer.mozilla.org/docs/Web/HTML/Element/button
1731
- function maybeSetLastButtonClicked (evt) {
1871
+ function maybeSetLastButtonClicked(evt) {
1732
1872
  const elt = closest(evt.target, "button, input[type='submit']")
1733
1873
  const internalData = getRelatedFormData(evt)
1734
1874
  if (internalData) {
1735
1875
  internalData.lastButtonClicked = elt
1736
1876
  }
1737
1877
  };
1738
- function maybeUnsetLastButtonClicked (evt) {
1878
+ function maybeUnsetLastButtonClicked(evt) {
1739
1879
  const internalData = getRelatedFormData(evt)
1740
1880
  if (internalData) {
1741
1881
  internalData.lastButtonClicked = null
1742
1882
  }
1743
1883
  }
1744
- function getRelatedFormData (evt) {
1884
+ function getRelatedFormData(evt) {
1745
1885
  const elt = closest(evt.target, "button, input[type='submit']")
1746
1886
  if (!elt) {
1747
1887
  return
@@ -1752,7 +1892,7 @@ const htmx = (function () {
1752
1892
  }
1753
1893
  return getInternalData(form)
1754
1894
  }
1755
- function initButtonTracking (elt) {
1895
+ function initButtonTracking(elt) {
1756
1896
  // need to handle both click and focus in:
1757
1897
  // focusin - in case someone tabs in to a button and hits the space bar
1758
1898
  // click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724
@@ -1761,7 +1901,7 @@ const htmx = (function () {
1761
1901
  elt.addEventListener('focusout', maybeUnsetLastButtonClicked)
1762
1902
  }
1763
1903
 
1764
- function countCurlies (line) {
1904
+ function countCurlies(line) {
1765
1905
  const tokens = tokenizeString(line)
1766
1906
  let netCurlies = 0
1767
1907
  for (let i = 0; i < tokens.length; i++) {
@@ -1775,14 +1915,14 @@ const htmx = (function () {
1775
1915
  return netCurlies
1776
1916
  }
1777
1917
 
1778
- function addHxOnEventHandler (elt, eventName, code) {
1918
+ function addHxOnEventHandler(elt, eventName, code) {
1779
1919
  const nodeData = getInternalData(elt)
1780
1920
  if (!Array.isArray(nodeData.onHandlers)) {
1781
1921
  nodeData.onHandlers = []
1782
1922
  }
1783
1923
  let func
1784
- const listener = function (e) {
1785
- return maybeEval(elt, function () {
1924
+ const listener = function(e) {
1925
+ return maybeEval(elt, function() {
1786
1926
  if (!func) {
1787
1927
  func = new Function('event', code)
1788
1928
  }
@@ -1793,7 +1933,7 @@ const htmx = (function () {
1793
1933
  nodeData.onHandlers.push({ event: eventName, listener })
1794
1934
  }
1795
1935
 
1796
- function processHxOnWildcard (elt) {
1936
+ function processHxOnWildcard(elt) {
1797
1937
  // wipe any previous on handlers so that this function takes precedence
1798
1938
  deInitOnHandlers(elt)
1799
1939
 
@@ -1820,7 +1960,7 @@ const htmx = (function () {
1820
1960
  }
1821
1961
  }
1822
1962
 
1823
- function initNode (elt) {
1963
+ function initNode(elt) {
1824
1964
  if (closest(elt, htmx.config.disableSelector)) {
1825
1965
  cleanUpElement(elt)
1826
1966
  return
@@ -1845,9 +1985,9 @@ const htmx = (function () {
1845
1985
  if (getClosestAttributeValue(elt, 'hx-boost') === 'true') {
1846
1986
  boostElement(elt, nodeData, triggerSpecs)
1847
1987
  } else if (hasAttribute(elt, 'hx-trigger')) {
1848
- triggerSpecs.forEach(function (triggerSpec) {
1988
+ triggerSpecs.forEach(function(triggerSpec) {
1849
1989
  // For "naked" triggers, don't do anything at all
1850
- addTriggerHandler(elt, triggerSpec, nodeData, function () {
1990
+ addTriggerHandler(elt, triggerSpec, nodeData, function() {
1851
1991
  })
1852
1992
  })
1853
1993
  }
@@ -1863,14 +2003,14 @@ const htmx = (function () {
1863
2003
  }
1864
2004
  }
1865
2005
 
1866
- function processNode (elt) {
2006
+ function processNode(elt) {
1867
2007
  elt = resolveTarget(elt)
1868
2008
  if (closest(elt, htmx.config.disableSelector)) {
1869
2009
  cleanUpElement(elt)
1870
2010
  return
1871
2011
  }
1872
2012
  initNode(elt)
1873
- forEach(findElementsToProcess(elt), function (child) { initNode(child) })
2013
+ forEach(findElementsToProcess(elt), function(child) { initNode(child) })
1874
2014
  forEach(findHxOnWildcardElements(elt), processHxOnWildcard)
1875
2015
  }
1876
2016
 
@@ -1878,11 +2018,11 @@ const htmx = (function () {
1878
2018
  // Event/Log Support
1879
2019
  //= ===================================================================
1880
2020
 
1881
- function kebabEventName (str) {
2021
+ function kebabEventName(str) {
1882
2022
  return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
1883
2023
  }
1884
2024
 
1885
- function makeEvent (eventName, detail) {
2025
+ function makeEvent(eventName, detail) {
1886
2026
  let evt
1887
2027
  if (window.CustomEvent && typeof window.CustomEvent === 'function') {
1888
2028
  // TODO: `composed: true` here is a hack to make global event handlers work with events in shadow DOM
@@ -1895,11 +2035,11 @@ const htmx = (function () {
1895
2035
  return evt
1896
2036
  }
1897
2037
 
1898
- function triggerErrorEvent (elt, eventName, detail) {
2038
+ function triggerErrorEvent(elt, eventName, detail) {
1899
2039
  triggerEvent(elt, eventName, mergeObjects({ error: eventName }, detail))
1900
2040
  }
1901
2041
 
1902
- function ignoreEventForLogging (eventName) {
2042
+ function ignoreEventForLogging(eventName) {
1903
2043
  return eventName === 'htmx:afterProcessNode'
1904
2044
  }
1905
2045
 
@@ -1912,8 +2052,8 @@ const htmx = (function () {
1912
2052
  * @param {(extension:import("./htmx").HtmxExtension) => void} toDo
1913
2053
  * @returns void
1914
2054
  */
1915
- function withExtensions (elt, toDo) {
1916
- forEach(getExtensions(elt), function (extension) {
2055
+ function withExtensions(elt, toDo) {
2056
+ forEach(getExtensions(elt), function(extension) {
1917
2057
  try {
1918
2058
  toDo(extension)
1919
2059
  } catch (e) {
@@ -1922,7 +2062,7 @@ const htmx = (function () {
1922
2062
  })
1923
2063
  }
1924
2064
 
1925
- function logError (msg) {
2065
+ function logError(msg) {
1926
2066
  if (console.error) {
1927
2067
  console.error(msg)
1928
2068
  } else if (console.log) {
@@ -1930,7 +2070,7 @@ const htmx = (function () {
1930
2070
  }
1931
2071
  }
1932
2072
 
1933
- function triggerEvent (elt, eventName, detail) {
2073
+ function triggerEvent(elt, eventName, detail) {
1934
2074
  elt = resolveTarget(elt)
1935
2075
  if (detail == null) {
1936
2076
  detail = {}
@@ -1950,7 +2090,7 @@ const htmx = (function () {
1950
2090
  const kebabedEvent = makeEvent(kebabName, event.detail)
1951
2091
  eventResult = eventResult && elt.dispatchEvent(kebabedEvent)
1952
2092
  }
1953
- withExtensions(elt, function (extension) {
2093
+ withExtensions(elt, function(extension) {
1954
2094
  eventResult = eventResult && (extension.onEvent(eventName, event) !== false && !event.defaultPrevented)
1955
2095
  })
1956
2096
  return eventResult
@@ -1961,16 +2101,21 @@ const htmx = (function () {
1961
2101
  //= ===================================================================
1962
2102
  let currentPathForHistory = location.pathname + location.search
1963
2103
 
1964
- function getHistoryElement () {
2104
+ function getHistoryElement() {
1965
2105
  const historyElt = getDocument().querySelector('[hx-history-elt],[data-hx-history-elt]')
1966
2106
  return historyElt || getDocument().body
1967
2107
  }
1968
2108
 
1969
- function saveToHistoryCache (url, content, title, scroll) {
2109
+ function saveToHistoryCache(url, rootElt) {
1970
2110
  if (!canAccessLocalStorage()) {
1971
2111
  return
1972
2112
  }
1973
2113
 
2114
+ // get state to save
2115
+ const innerHTML = cleanInnerHtmlForHistory(rootElt)
2116
+ const title = getDocument().title
2117
+ const scroll = window.scrollY
2118
+
1974
2119
  if (htmx.config.historyCacheSize <= 0) {
1975
2120
  // make sure that an eventually already existing cache is purged
1976
2121
  localStorage.removeItem('htmx-history-cache')
@@ -1986,12 +2131,17 @@ const htmx = (function () {
1986
2131
  break
1987
2132
  }
1988
2133
  }
1989
- const newHistoryItem = { url, content, title, scroll }
2134
+
2135
+ const newHistoryItem = { url, content: innerHTML, title, scroll }
2136
+
1990
2137
  triggerEvent(getDocument().body, 'htmx:historyItemCreated', { item: newHistoryItem, cache: historyCache })
2138
+
1991
2139
  historyCache.push(newHistoryItem)
1992
2140
  while (historyCache.length > htmx.config.historyCacheSize) {
1993
2141
  historyCache.shift()
1994
2142
  }
2143
+
2144
+ // keep trying to save the cache until it succeeds or is empty
1995
2145
  while (historyCache.length > 0) {
1996
2146
  try {
1997
2147
  localStorage.setItem('htmx-history-cache', JSON.stringify(historyCache))
@@ -2003,7 +2153,7 @@ const htmx = (function () {
2003
2153
  }
2004
2154
  }
2005
2155
 
2006
- function getCachedHistory (url) {
2156
+ function getCachedHistory(url) {
2007
2157
  if (!canAccessLocalStorage()) {
2008
2158
  return null
2009
2159
  }
@@ -2019,16 +2169,16 @@ const htmx = (function () {
2019
2169
  return null
2020
2170
  }
2021
2171
 
2022
- function cleanInnerHtmlForHistory (elt) {
2172
+ function cleanInnerHtmlForHistory(elt) {
2023
2173
  const className = htmx.config.requestClass
2024
2174
  const clone = elt.cloneNode(true)
2025
- forEach(findAll(clone, '.' + className), function (child) {
2175
+ forEach(findAll(clone, '.' + className), function(child) {
2026
2176
  removeClassFromElement(child, className)
2027
2177
  })
2028
2178
  return clone.innerHTML
2029
2179
  }
2030
2180
 
2031
- function saveCurrentPageToHistory () {
2181
+ function saveCurrentPageToHistory() {
2032
2182
  const elt = getHistoryElement()
2033
2183
  const path = currentPathForHistory || location.pathname + location.search
2034
2184
 
@@ -2046,13 +2196,13 @@ const htmx = (function () {
2046
2196
  }
2047
2197
  if (!disableHistoryCache) {
2048
2198
  triggerEvent(getDocument().body, 'htmx:beforeHistorySave', { path, historyElt: elt })
2049
- saveToHistoryCache(path, cleanInnerHtmlForHistory(elt), getDocument().title, window.scrollY)
2199
+ saveToHistoryCache(path, elt)
2050
2200
  }
2051
2201
 
2052
2202
  if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, getDocument().title, window.location.href)
2053
2203
  }
2054
2204
 
2055
- function pushUrlIntoHistory (path) {
2205
+ function pushUrlIntoHistory(path) {
2056
2206
  // remove the cache buster parameter, if any
2057
2207
  if (htmx.config.getCacheBusterParam) {
2058
2208
  path = path.replace(/org\.htmx\.cache-buster=[^&]*&?/, '')
@@ -2066,18 +2216,18 @@ const htmx = (function () {
2066
2216
  currentPathForHistory = path
2067
2217
  }
2068
2218
 
2069
- function replaceUrlInHistory (path) {
2219
+ function replaceUrlInHistory(path) {
2070
2220
  if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, '', path)
2071
2221
  currentPathForHistory = path
2072
2222
  }
2073
2223
 
2074
- function settleImmediately (tasks) {
2075
- forEach(tasks, function (task) {
2224
+ function settleImmediately(tasks) {
2225
+ forEach(tasks, function(task) {
2076
2226
  task.call()
2077
2227
  })
2078
2228
  }
2079
2229
 
2080
- function loadHistoryFromServer (path) {
2230
+ function loadHistoryFromServer(path) {
2081
2231
  const request = new XMLHttpRequest()
2082
2232
  const details = { path, xhr: request }
2083
2233
  triggerEvent(getDocument().body, 'htmx:historyCacheMiss', details)
@@ -2085,25 +2235,18 @@ const htmx = (function () {
2085
2235
  request.setRequestHeader('HX-Request', 'true')
2086
2236
  request.setRequestHeader('HX-History-Restore-Request', 'true')
2087
2237
  request.setRequestHeader('HX-Current-URL', getDocument().location.href)
2088
- request.onload = function () {
2238
+ request.onload = function() {
2089
2239
  if (this.status >= 200 && this.status < 400) {
2090
2240
  triggerEvent(getDocument().body, 'htmx:historyCacheMissLoad', details)
2091
- let fragment = makeFragment(this.response)
2241
+ const fragment = makeFragment(this.response)
2092
2242
  // @ts-ignore
2093
- fragment = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment
2243
+ const content = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment
2094
2244
  const historyElement = getHistoryElement()
2095
2245
  const settleInfo = makeSettleInfo(historyElement)
2096
- const title = findTitle(this.response)
2097
- if (title) {
2098
- const titleElt = find('title')
2099
- if (titleElt) {
2100
- titleElt.innerHTML = title
2101
- } else {
2102
- window.document.title = title
2103
- }
2104
- }
2246
+ handleTitle(fragment.title)
2247
+
2105
2248
  // @ts-ignore
2106
- swapInnerHTML(historyElement, fragment, settleInfo)
2249
+ swapInnerHTML(historyElement, content, settleInfo)
2107
2250
  settleImmediately(settleInfo.tasks)
2108
2251
  currentPathForHistory = path
2109
2252
  triggerEvent(getDocument().body, 'htmx:historyRestore', { path, cacheMiss: true, serverResponse: this.response })
@@ -2114,7 +2257,7 @@ const htmx = (function () {
2114
2257
  request.send()
2115
2258
  }
2116
2259
 
2117
- function restoreHistory (path) {
2260
+ function restoreHistory(path) {
2118
2261
  saveCurrentPageToHistory()
2119
2262
  path = path || location.pathname + location.search
2120
2263
  const cached = getCachedHistory(path)
@@ -2122,10 +2265,10 @@ const htmx = (function () {
2122
2265
  const fragment = makeFragment(cached.content)
2123
2266
  const historyElement = getHistoryElement()
2124
2267
  const settleInfo = makeSettleInfo(historyElement)
2268
+ handleTitle(fragment.title)
2125
2269
  swapInnerHTML(historyElement, fragment, settleInfo)
2126
2270
  settleImmediately(settleInfo.tasks)
2127
- document.title = cached.title
2128
- setTimeout(function () {
2271
+ setTimeout(function() {
2129
2272
  window.scrollTo(0, cached.scroll)
2130
2273
  }, 0) // next 'tick', so browser has time to render layout
2131
2274
  currentPathForHistory = path
@@ -2140,12 +2283,12 @@ const htmx = (function () {
2140
2283
  }
2141
2284
  }
2142
2285
 
2143
- function addRequestIndicatorClasses (elt) {
2286
+ function addRequestIndicatorClasses(elt) {
2144
2287
  let indicators = findAttributeTargets(elt, 'hx-indicator')
2145
2288
  if (indicators == null) {
2146
2289
  indicators = [elt]
2147
2290
  }
2148
- forEach(indicators, function (ic) {
2291
+ forEach(indicators, function(ic) {
2149
2292
  const internalData = getInternalData(ic)
2150
2293
  internalData.requestCount = (internalData.requestCount || 0) + 1
2151
2294
  ic.classList.add.call(ic.classList, htmx.config.requestClass)
@@ -2153,12 +2296,12 @@ const htmx = (function () {
2153
2296
  return indicators
2154
2297
  }
2155
2298
 
2156
- function disableElements (elt) {
2299
+ function disableElements(elt) {
2157
2300
  let disabledElts = findAttributeTargets(elt, 'hx-disabled-elt')
2158
2301
  if (disabledElts == null) {
2159
2302
  disabledElts = []
2160
2303
  }
2161
- forEach(disabledElts, function (disabledElement) {
2304
+ forEach(disabledElts, function(disabledElement) {
2162
2305
  const internalData = getInternalData(disabledElement)
2163
2306
  internalData.requestCount = (internalData.requestCount || 0) + 1
2164
2307
  disabledElement.setAttribute('disabled', '')
@@ -2166,15 +2309,15 @@ const htmx = (function () {
2166
2309
  return disabledElts
2167
2310
  }
2168
2311
 
2169
- function removeRequestIndicators (indicators, disabled) {
2170
- forEach(indicators, function (ic) {
2312
+ function removeRequestIndicators(indicators, disabled) {
2313
+ forEach(indicators, function(ic) {
2171
2314
  const internalData = getInternalData(ic)
2172
2315
  internalData.requestCount = (internalData.requestCount || 0) - 1
2173
2316
  if (internalData.requestCount === 0) {
2174
2317
  ic.classList.remove.call(ic.classList, htmx.config.requestClass)
2175
2318
  }
2176
2319
  })
2177
- forEach(disabled, function (disabledElement) {
2320
+ forEach(disabled, function(disabledElement) {
2178
2321
  const internalData = getInternalData(disabledElement)
2179
2322
  internalData.requestCount = (internalData.requestCount || 0) - 1
2180
2323
  if (internalData.requestCount === 0) {
@@ -2187,7 +2330,12 @@ const htmx = (function () {
2187
2330
  // Input Value Processing
2188
2331
  //= ===================================================================
2189
2332
 
2190
- function haveSeenNode (processed, elt) {
2333
+ /**
2334
+ * @param {HTMLElement[]} processed
2335
+ * @param {HTMLElement} elt
2336
+ * @returns {boolean}
2337
+ */
2338
+ function haveSeenNode(processed, elt) {
2191
2339
  for (let i = 0; i < processed.length; i++) {
2192
2340
  const node = processed[i]
2193
2341
  if (node.isSameNode(elt)) {
@@ -2197,8 +2345,8 @@ const htmx = (function () {
2197
2345
  return false
2198
2346
  }
2199
2347
 
2200
- function shouldInclude (elt) {
2201
- if (elt.name === '' || elt.name == null || elt.disabled) {
2348
+ function shouldInclude(elt) {
2349
+ if (elt.name === '' || elt.name == null || elt.disabled || closest(elt, 'fieldset[disabled]')) {
2202
2350
  return false
2203
2351
  }
2204
2352
  // ignore "submitter" types (see jQuery src/serialize.js)
@@ -2211,30 +2359,43 @@ const htmx = (function () {
2211
2359
  return true
2212
2360
  }
2213
2361
 
2214
- function addValueToValues (name, value, values) {
2215
- // This is a little ugly because both the current value of the named value in the form
2216
- // and the new value could be arrays, so we have to handle all four cases :/
2362
+ /** @param {string} name
2363
+ * @param {string|Array} value
2364
+ * @param {FormData} formData */
2365
+ function addValueToFormData(name, value, formData) {
2217
2366
  if (name != null && value != null) {
2218
- const current = values[name]
2219
- if (current === undefined) {
2220
- values[name] = value
2221
- } else if (Array.isArray(current)) {
2222
- if (Array.isArray(value)) {
2223
- values[name] = current.concat(value)
2224
- } else {
2225
- current.push(value)
2226
- }
2367
+ if (Array.isArray(value)) {
2368
+ value.forEach(function(v) { formData.append(name, v) })
2227
2369
  } else {
2228
- if (Array.isArray(value)) {
2229
- values[name] = [current].concat(value)
2230
- } else {
2231
- values[name] = [current, value]
2232
- }
2370
+ formData.append(name, value)
2371
+ }
2372
+ }
2373
+ }
2374
+
2375
+ /** @param {string} name
2376
+ * @param {string|Array} value
2377
+ * @param {FormData} formData */
2378
+ function removeValueFromFormData(name, value, formData) {
2379
+ if (name != null && value != null) {
2380
+ let values = formData.getAll(name)
2381
+ if (Array.isArray(value)) {
2382
+ values = values.filter(v => value.indexOf(v) < 0)
2383
+ } else {
2384
+ values = values.filter(v => v !== value)
2233
2385
  }
2386
+ formData.delete(name)
2387
+ forEach(values, v => formData.append(name, v))
2234
2388
  }
2235
2389
  }
2236
2390
 
2237
- function processInputValue (processed, values, errors, elt, validate) {
2391
+ /**
2392
+ * @param {HTMLElement[]} processed
2393
+ * @param {FormData} formData
2394
+ * @param {HtmxElementValidationError[]} errors
2395
+ * @param {HTMLElement|HTMLInputElement|HTMLFormElement} elt
2396
+ * @param {boolean} validate
2397
+ */
2398
+ function processInputValue(processed, formData, errors, elt, validate) {
2238
2399
  if (elt == null || haveSeenNode(processed, elt)) {
2239
2400
  return
2240
2401
  } else {
@@ -2244,26 +2405,47 @@ const htmx = (function () {
2244
2405
  const name = getRawAttribute(elt, 'name')
2245
2406
  let value = elt.value
2246
2407
  if (elt.multiple && elt.tagName === 'SELECT') {
2247
- value = toArray(elt.querySelectorAll('option:checked')).map(function (e) { return e.value })
2408
+ value = toArray(elt.querySelectorAll('option:checked')).map(function(e) { return e.value })
2248
2409
  }
2249
2410
  // include file inputs
2250
2411
  if (elt.files) {
2251
2412
  value = toArray(elt.files)
2252
2413
  }
2253
- addValueToValues(name, value, values)
2414
+ addValueToFormData(name, value, formData)
2254
2415
  if (validate) {
2255
2416
  validateElement(elt, errors)
2256
2417
  }
2257
2418
  }
2258
2419
  if (matches(elt, 'form')) {
2259
- const inputs = elt.elements
2260
- forEach(inputs, function (input) {
2261
- processInputValue(processed, values, errors, input, validate)
2420
+ forEach(elt.elements, function(input) {
2421
+ if (processed.indexOf(input) >= 0) {
2422
+ // The input has already been processed and added to the values, but the FormData that will be
2423
+ // constructed right after on the form, will include it once again. So remove that input's value
2424
+ // now to avoid duplicates
2425
+ removeValueFromFormData(input.name, input.value, formData)
2426
+ } else {
2427
+ processed.push(input)
2428
+ }
2429
+ if (validate) {
2430
+ validateElement(input, errors)
2431
+ }
2432
+ })
2433
+ new FormData(elt).forEach(function(value, name) {
2434
+ addValueToFormData(name, value, formData)
2262
2435
  })
2263
2436
  }
2264
2437
  }
2265
2438
 
2266
- function validateElement (element, errors) {
2439
+ /**
2440
+ * @typedef {{elt: HTMLElement, message: string, validity: ValidityState}} HtmxElementValidationError
2441
+ */
2442
+
2443
+ /**
2444
+ *
2445
+ * @param {HTMLElement|HTMLObjectElement} element
2446
+ * @param {HtmxElementValidationError[]} errors
2447
+ */
2448
+ function validateElement(element, errors) {
2267
2449
  if (element.willValidate) {
2268
2450
  triggerEvent(element, 'htmx:validation:validate')
2269
2451
  if (!element.checkValidity()) {
@@ -2274,13 +2456,32 @@ const htmx = (function () {
2274
2456
  }
2275
2457
 
2276
2458
  /**
2277
- * @param {HTMLElement} elt
2459
+ * Override values in the one FormData with those from another.
2460
+ * @param {FormData} receiver the formdata that will be mutated
2461
+ * @param {FormData} donor the formdata that will provide the overriding values
2462
+ * @returns {FormData} the {@linkcode receiver}
2463
+ */
2464
+ function overrideFormData(receiver, donor) {
2465
+ for (const key of donor.keys()) {
2466
+ receiver.delete(key)
2467
+ donor.getAll(key).forEach(function(value) {
2468
+ receiver.append(key, value)
2469
+ })
2470
+ }
2471
+ return receiver
2472
+ }
2473
+
2474
+ /**
2475
+ * @param {HTMLElement|HTMLFormElement} elt
2278
2476
  * @param {string} verb
2477
+ * @returns {{errors: HtmxElementValidationError[], formData: FormData, values: Object}}
2279
2478
  */
2280
- function getInputValues (elt, verb) {
2479
+ function getInputValues(elt, verb) {
2480
+ /** @type HTMLElement[] */
2281
2481
  const processed = []
2282
- let values = {}
2283
- const formValues = {}
2482
+ const formData = new FormData()
2483
+ const priorityFormData = new FormData()
2484
+ /** @type HtmxElementValidationError[] */
2284
2485
  const errors = []
2285
2486
  const internalData = getInternalData(elt)
2286
2487
  if (internalData.lastButtonClicked && !bodyContains(internalData.lastButtonClicked)) {
@@ -2296,39 +2497,45 @@ const htmx = (function () {
2296
2497
 
2297
2498
  // for a non-GET include the closest form
2298
2499
  if (verb !== 'get') {
2299
- processInputValue(processed, formValues, errors, closest(elt, 'form'), validate)
2500
+ processInputValue(processed, priorityFormData, errors, closest(elt, 'form'), validate)
2300
2501
  }
2301
2502
 
2302
2503
  // include the element itself
2303
- processInputValue(processed, values, errors, elt, validate)
2504
+ processInputValue(processed, formData, errors, elt, validate)
2304
2505
 
2305
2506
  // if a button or submit was clicked last, include its value
2306
2507
  if (internalData.lastButtonClicked || elt.tagName === 'BUTTON' ||
2307
2508
  (elt.tagName === 'INPUT' && getRawAttribute(elt, 'type') === 'submit')) {
2308
2509
  const button = internalData.lastButtonClicked || elt
2309
2510
  const name = getRawAttribute(button, 'name')
2310
- addValueToValues(name, button.value, formValues)
2511
+ addValueToFormData(name, button.value, priorityFormData)
2311
2512
  }
2312
2513
 
2313
2514
  // include any explicit includes
2314
2515
  const includes = findAttributeTargets(elt, 'hx-include')
2315
- forEach(includes, function (node) {
2316
- processInputValue(processed, values, errors, node, validate)
2516
+ forEach(includes, function(node) {
2517
+ processInputValue(processed, formData, errors, node, validate)
2317
2518
  // if a non-form is included, include any input values within it
2318
2519
  if (!matches(node, 'form')) {
2319
- forEach(node.querySelectorAll(INPUT_SELECTOR), function (descendant) {
2320
- processInputValue(processed, values, errors, descendant, validate)
2520
+ forEach(node.querySelectorAll(INPUT_SELECTOR), function(descendant) {
2521
+ processInputValue(processed, formData, errors, descendant, validate)
2321
2522
  })
2322
2523
  }
2323
2524
  })
2324
2525
 
2325
- // form values take precedence, overriding the regular values
2326
- values = mergeObjects(values, formValues)
2526
+ // values from a <form> take precedence, overriding the regular values
2527
+ overrideFormData(formData, priorityFormData)
2327
2528
 
2328
- return { errors, values }
2529
+ return { errors, formData, values: formDataProxy(formData) }
2329
2530
  }
2330
2531
 
2331
- function appendParam (returnStr, name, realValue) {
2532
+ /**
2533
+ * @param {string} returnStr
2534
+ * @param {string} name
2535
+ * @param {any} realValue
2536
+ * @returns {string}
2537
+ */
2538
+ function appendParam(returnStr, name, realValue) {
2332
2539
  if (returnStr !== '') {
2333
2540
  returnStr += '&'
2334
2541
  }
@@ -2340,40 +2547,19 @@ const htmx = (function () {
2340
2547
  return returnStr
2341
2548
  }
2342
2549
 
2343
- function urlEncode (values) {
2550
+ /**
2551
+ * @param {FormData|Object} values
2552
+ * @returns string
2553
+ */
2554
+ function urlEncode(values) {
2555
+ values = formDataFromObject(values)
2344
2556
  let returnStr = ''
2345
- for (var name in values) {
2346
- if (values.hasOwnProperty(name)) {
2347
- const value = values[name]
2348
- if (Array.isArray(value)) {
2349
- forEach(value, function (v) {
2350
- returnStr = appendParam(returnStr, name, v)
2351
- })
2352
- } else {
2353
- returnStr = appendParam(returnStr, name, value)
2354
- }
2355
- }
2356
- }
2557
+ values.forEach(function(value, key) {
2558
+ returnStr = appendParam(returnStr, key, value)
2559
+ })
2357
2560
  return returnStr
2358
2561
  }
2359
2562
 
2360
- function makeFormData (values) {
2361
- const formData = new FormData()
2362
- for (var name in values) {
2363
- if (values.hasOwnProperty(name)) {
2364
- const value = values[name]
2365
- if (Array.isArray(value)) {
2366
- forEach(value, function (v) {
2367
- formData.append(name, v)
2368
- })
2369
- } else {
2370
- formData.append(name, value)
2371
- }
2372
- }
2373
- }
2374
- return formData
2375
- }
2376
-
2377
2563
  //= ===================================================================
2378
2564
  // Ajax
2379
2565
  //= ===================================================================
@@ -2384,7 +2570,7 @@ const htmx = (function () {
2384
2570
  * @param {string} prompt
2385
2571
  * @returns {Object} // TODO: Define/Improve HtmxHeaderSpecification
2386
2572
  */
2387
- function getHeaders (elt, target, prompt) {
2573
+ function getHeaders(elt, target, prompt) {
2388
2574
  const headers = {
2389
2575
  'HX-Request': 'true',
2390
2576
  'HX-Trigger': getRawAttribute(elt, 'id'),
@@ -2406,28 +2592,30 @@ const htmx = (function () {
2406
2592
  * filterValues takes an object containing form input values
2407
2593
  * and returns a new object that only contains keys that are
2408
2594
  * specified by the closest "hx-params" attribute
2409
- * @param {Object} inputValues
2595
+ * @param {FormData} inputValues
2410
2596
  * @param {HTMLElement} elt
2411
- * @returns {Object}
2597
+ * @returns {FormData}
2412
2598
  */
2413
- function filterValues (inputValues, elt) {
2599
+ function filterValues(inputValues, elt) {
2414
2600
  const paramsValue = getClosestAttributeValue(elt, 'hx-params')
2415
2601
  if (paramsValue) {
2416
2602
  if (paramsValue === 'none') {
2417
- return {}
2603
+ return new FormData()
2418
2604
  } else if (paramsValue === '*') {
2419
2605
  return inputValues
2420
2606
  } else if (paramsValue.indexOf('not ') === 0) {
2421
- forEach(paramsValue.substr(4).split(','), function (name) {
2607
+ forEach(paramsValue.substr(4).split(','), function(name) {
2422
2608
  name = name.trim()
2423
- delete inputValues[name]
2609
+ inputValues.delete(name)
2424
2610
  })
2425
2611
  return inputValues
2426
2612
  } else {
2427
- const newValues = {}
2428
- forEach(paramsValue.split(','), function (name) {
2613
+ const newValues = new FormData()
2614
+ forEach(paramsValue.split(','), function(name) {
2429
2615
  name = name.trim()
2430
- newValues[name] = inputValues[name]
2616
+ if (inputValues.has(name)) {
2617
+ inputValues.getAll(name).forEach(function(value) { newValues.append(name, value) })
2618
+ }
2431
2619
  })
2432
2620
  return newValues
2433
2621
  }
@@ -2436,18 +2624,19 @@ const htmx = (function () {
2436
2624
  }
2437
2625
  }
2438
2626
 
2439
- function isAnchorLink (elt) {
2627
+ function isAnchorLink(elt) {
2440
2628
  return getRawAttribute(elt, 'href') && getRawAttribute(elt, 'href').indexOf('#') >= 0
2441
2629
  }
2442
2630
 
2443
2631
  /**
2444
2632
  *
2445
2633
  * @param {HTMLElement} elt
2446
- * @param {string} swapInfoOverride
2634
+ * @param {import("./htmx").HtmxSwapStyle} swapInfoOverride
2447
2635
  * @returns {import("./htmx").HtmxSwapSpecification}
2448
2636
  */
2449
- function getSwapSpecification (elt, swapInfoOverride) {
2637
+ function getSwapSpecification(elt, swapInfoOverride) {
2450
2638
  const swapInfo = swapInfoOverride || getClosestAttributeValue(elt, 'hx-swap')
2639
+ /** @type import("./htmx").HtmxSwapSpecification */
2451
2640
  const swapSpec = {
2452
2641
  swapStyle: getInternalData(elt).boosted ? 'innerHTML' : htmx.config.defaultSwapStyle,
2453
2642
  swapDelay: htmx.config.defaultSwapDelay,
@@ -2497,14 +2686,20 @@ const htmx = (function () {
2497
2686
  return swapSpec
2498
2687
  }
2499
2688
 
2500
- function usesFormData (elt) {
2689
+ function usesFormData(elt) {
2501
2690
  return getClosestAttributeValue(elt, 'hx-encoding') === 'multipart/form-data' ||
2502
2691
  (matches(elt, 'form') && getRawAttribute(elt, 'enctype') === 'multipart/form-data')
2503
2692
  }
2504
2693
 
2505
- function encodeParamsForBody (xhr, elt, filteredParameters) {
2694
+ /**
2695
+ * @param {XMLHttpRequest} xhr
2696
+ * @param {HTMLElement} elt
2697
+ * @param {FormData} filteredParameters
2698
+ * @returns {*|string|null}
2699
+ */
2700
+ function encodeParamsForBody(xhr, elt, filteredParameters) {
2506
2701
  let encodedParameters = null
2507
- withExtensions(elt, function (extension) {
2702
+ withExtensions(elt, function(extension) {
2508
2703
  if (encodedParameters == null) {
2509
2704
  encodedParameters = extension.encodeParameters(xhr, filteredParameters, elt)
2510
2705
  }
@@ -2513,7 +2708,7 @@ const htmx = (function () {
2513
2708
  return encodedParameters
2514
2709
  } else {
2515
2710
  if (usesFormData(elt)) {
2516
- return makeFormData(filteredParameters)
2711
+ return formDataFromObject(filteredParameters)
2517
2712
  } else {
2518
2713
  return urlEncode(filteredParameters)
2519
2714
  }
@@ -2525,11 +2720,11 @@ const htmx = (function () {
2525
2720
  * @param {Element} target
2526
2721
  * @returns {import("./htmx").HtmxSettleInfo}
2527
2722
  */
2528
- function makeSettleInfo (target) {
2723
+ function makeSettleInfo(target) {
2529
2724
  return { tasks: [], elts: [target] }
2530
2725
  }
2531
2726
 
2532
- function updateScrollState (content, swapSpec) {
2727
+ function updateScrollState(content, swapSpec) {
2533
2728
  const first = content[0]
2534
2729
  const last = content[content.length - 1]
2535
2730
  if (swapSpec.scroll) {
@@ -2573,7 +2768,7 @@ const htmx = (function () {
2573
2768
  * @param {Object=} values
2574
2769
  * @returns {Object}
2575
2770
  */
2576
- function getValuesForElement (elt, attr, evalAsDefault, values) {
2771
+ function getValuesForElement(elt, attr, evalAsDefault, values) {
2577
2772
  if (values == null) {
2578
2773
  values = {}
2579
2774
  }
@@ -2599,7 +2794,7 @@ const htmx = (function () {
2599
2794
  }
2600
2795
  let varsValues
2601
2796
  if (evaluateValue) {
2602
- varsValues = maybeEval(elt, function () { return Function('return (' + str + ')')() }, {})
2797
+ varsValues = maybeEval(elt, function() { return Function('return (' + str + ')')() }, {})
2603
2798
  } else {
2604
2799
  varsValues = parseJSON(str)
2605
2800
  }
@@ -2614,7 +2809,7 @@ const htmx = (function () {
2614
2809
  return getValuesForElement(parentElt(elt), attr, evalAsDefault, values)
2615
2810
  }
2616
2811
 
2617
- function maybeEval (elt, toEval, defaultVal) {
2812
+ function maybeEval(elt, toEval, defaultVal) {
2618
2813
  if (htmx.config.allowEval) {
2619
2814
  return toEval()
2620
2815
  } else {
@@ -2625,31 +2820,31 @@ const htmx = (function () {
2625
2820
 
2626
2821
  /**
2627
2822
  * @param {HTMLElement} elt
2628
- * @param {*} expressionVars
2823
+ * @param {*?} expressionVars
2629
2824
  * @returns
2630
2825
  */
2631
- function getHXVarsForElement (elt, expressionVars) {
2826
+ function getHXVarsForElement(elt, expressionVars) {
2632
2827
  return getValuesForElement(elt, 'hx-vars', true, expressionVars)
2633
2828
  }
2634
2829
 
2635
2830
  /**
2636
2831
  * @param {HTMLElement} elt
2637
- * @param {*} expressionVars
2832
+ * @param {*?} expressionVars
2638
2833
  * @returns
2639
2834
  */
2640
- function getHXValsForElement (elt, expressionVars) {
2835
+ function getHXValsForElement(elt, expressionVars) {
2641
2836
  return getValuesForElement(elt, 'hx-vals', false, expressionVars)
2642
2837
  }
2643
2838
 
2644
2839
  /**
2645
2840
  * @param {HTMLElement} elt
2646
- * @returns {Object}
2841
+ * @returns {FormData}
2647
2842
  */
2648
- function getExpressionVars (elt) {
2649
- return mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt))
2843
+ function getExpressionVars(elt) {
2844
+ return formDataFromObject(mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt)))
2650
2845
  }
2651
2846
 
2652
- function safelySetHeaderValue (xhr, header, headerValue) {
2847
+ function safelySetHeaderValue(xhr, header, headerValue) {
2653
2848
  if (headerValue !== null) {
2654
2849
  try {
2655
2850
  xhr.setRequestHeader(header, headerValue)
@@ -2661,7 +2856,7 @@ const htmx = (function () {
2661
2856
  }
2662
2857
  }
2663
2858
 
2664
- function getPathFromResponse (xhr) {
2859
+ function getPathFromResponse(xhr) {
2665
2860
  // NB: IE11 does not support this stuff
2666
2861
  if (xhr.responseURL && typeof (URL) !== 'undefined') {
2667
2862
  try {
@@ -2673,11 +2868,11 @@ const htmx = (function () {
2673
2868
  }
2674
2869
  }
2675
2870
 
2676
- function hasHeader (xhr, regexp) {
2871
+ function hasHeader(xhr, regexp) {
2677
2872
  return regexp.test(xhr.getAllResponseHeaders())
2678
2873
  }
2679
2874
 
2680
- function ajaxHelper (verb, path, context) {
2875
+ function ajaxHelper(verb, path, context) {
2681
2876
  verb = verb.toLowerCase()
2682
2877
  if (context) {
2683
2878
  if (context instanceof Element || isType(context, 'String')) {
@@ -2704,7 +2899,7 @@ const htmx = (function () {
2704
2899
  }
2705
2900
  }
2706
2901
 
2707
- function hierarchyForElt (elt) {
2902
+ function hierarchyForElt(elt) {
2708
2903
  const arr = []
2709
2904
  while (elt) {
2710
2905
  arr.push(elt)
@@ -2713,7 +2908,7 @@ const htmx = (function () {
2713
2908
  return arr
2714
2909
  }
2715
2910
 
2716
- function verifyPath (elt, path, requestConfig) {
2911
+ function verifyPath(elt, path, requestConfig) {
2717
2912
  let sameHost
2718
2913
  let url
2719
2914
  if (typeof URL === 'function') {
@@ -2734,12 +2929,128 @@ const htmx = (function () {
2734
2929
  return triggerEvent(elt, 'htmx:validateUrl', mergeObjects({ url, sameHost }, requestConfig))
2735
2930
  }
2736
2931
 
2737
- function issueAjaxRequest (verb, path, elt, event, etc, confirmed) {
2932
+ function formDataFromObject(obj) {
2933
+ if (obj instanceof FormData) return obj
2934
+ const formData = new FormData()
2935
+ for (const key in obj) {
2936
+ if (obj.hasOwnProperty(key)) {
2937
+ if (typeof obj[key].forEach === 'function') {
2938
+ obj[key].forEach(function(v) { formData.append(key, v) })
2939
+ } else if (typeof obj[key] === 'object') {
2940
+ formData.append(key, JSON.stringify(obj[key]))
2941
+ } else {
2942
+ formData.append(key, obj[key])
2943
+ }
2944
+ }
2945
+ }
2946
+ return formData
2947
+ }
2948
+
2949
+ /**
2950
+ * @param {FormData} formData
2951
+ * @param {string|Symbol} name
2952
+ * @param {Array} array
2953
+ * @returns {Array}
2954
+ */
2955
+ function formDataArrayProxy(formData, name, array) {
2956
+ // mutating the array should mutate the underlying form data
2957
+ return new Proxy(array, {
2958
+ get: function(target, key) {
2959
+ if (typeof key === 'number') return target[key]
2960
+ if (key === 'length') return target.length
2961
+ if (key === 'push') {
2962
+ return function(value) {
2963
+ target.push(value)
2964
+ formData.append(name, value)
2965
+ }
2966
+ }
2967
+ if (typeof target[key] === 'function') {
2968
+ return function() {
2969
+ target[key].apply(target, arguments)
2970
+ formData.delete(name)
2971
+ target.forEach(function(v) { formData.append(name, v) })
2972
+ }
2973
+ }
2974
+
2975
+ if (target[key] && target[key].length === 1) {
2976
+ return target[key][0]
2977
+ } else {
2978
+ return target[key]
2979
+ }
2980
+ },
2981
+ set: function(target, index, value) {
2982
+ target[index] = value
2983
+ formData.delete(name)
2984
+ target.forEach(function(v) { formData.append(name, v) })
2985
+ return true
2986
+ }
2987
+ })
2988
+ }
2989
+
2990
+ /**
2991
+ * @param {FormData} formData
2992
+ * @returns {Object}
2993
+ */
2994
+ function formDataProxy(formData) {
2995
+ return new Proxy(formData, {
2996
+ get: function(target, name) {
2997
+ if (typeof name === 'symbol') {
2998
+ // Forward symbol calls to the FormData itself directly
2999
+ return Reflect.get(...arguments)
3000
+ }
3001
+ if (name === 'toJSON') {
3002
+ // Support JSON.stringify call on proxy
3003
+ return () => Object.fromEntries(formData)
3004
+ }
3005
+ if (name in target) {
3006
+ // Wrap in function with apply to correctly bind the FormData context, as a direct call would result in an illegal invocation error
3007
+ if (typeof target[name] === 'function') {
3008
+ return function() {
3009
+ return formData[name].apply(formData, arguments)
3010
+ }
3011
+ } else {
3012
+ return target[name]
3013
+ }
3014
+ }
3015
+ const array = formData.getAll(name)
3016
+ // Those 2 undefined & single value returns are for retro-compatibility as we weren't using FormData before
3017
+ if (array.length === 0) {
3018
+ return undefined
3019
+ } else if (array.length === 1) {
3020
+ return array[0]
3021
+ } else {
3022
+ return formDataArrayProxy(target, name, array)
3023
+ }
3024
+ },
3025
+ set: function(target, name, value) {
3026
+ target.delete(name)
3027
+ if (typeof value.forEach === 'function') {
3028
+ value.forEach(function(v) { target.append(name, v) })
3029
+ } else {
3030
+ target.append(name, value)
3031
+ }
3032
+ return true
3033
+ },
3034
+ deleteProperty: function(target, name) {
3035
+ target.delete(name)
3036
+ return true
3037
+ },
3038
+ // Support Object.assign call from proxy
3039
+ ownKeys: function(target) {
3040
+ return Reflect.ownKeys(Object.fromEntries(target))
3041
+ },
3042
+ getOwnPropertyDescriptor: function(target, prop) {
3043
+ return Reflect.getOwnPropertyDescriptor(Object.fromEntries(target), prop)
3044
+ }
3045
+ })
3046
+ }
3047
+
3048
+ function issueAjaxRequest(verb, path, elt, event, etc, confirmed) {
2738
3049
  let resolve = null
2739
3050
  let reject = null
2740
3051
  etc = etc != null ? etc : {}
2741
3052
  if (etc.returnPromise && typeof Promise !== 'undefined') {
2742
- var promise = new Promise(function (_resolve, _reject) {
3053
+ var promise = new Promise(function(_resolve, _reject) {
2743
3054
  resolve = _resolve
2744
3055
  reject = _reject
2745
3056
  })
@@ -2783,7 +3094,7 @@ const htmx = (function () {
2783
3094
  const confirmQuestion = getClosestAttributeValue(elt, 'hx-confirm')
2784
3095
  // allow event-based confirmation w/ a callback
2785
3096
  if (confirmed === undefined) {
2786
- const issueRequest = function (skipConfirmation) {
3097
+ const issueRequest = function(skipConfirmation) {
2787
3098
  return issueAjaxRequest(verb, path, elt, event, etc, !!skipConfirmation)
2788
3099
  }
2789
3100
  const confirmDetails = { target, elt, path, verb, triggeringEvent: event, etc, issueRequest, question: confirmQuestion }
@@ -2845,16 +3156,16 @@ const htmx = (function () {
2845
3156
  eltData.queuedRequests = []
2846
3157
  }
2847
3158
  if (queueStrategy === 'first' && eltData.queuedRequests.length === 0) {
2848
- eltData.queuedRequests.push(function () {
3159
+ eltData.queuedRequests.push(function() {
2849
3160
  issueAjaxRequest(verb, path, elt, event, etc)
2850
3161
  })
2851
3162
  } else if (queueStrategy === 'all') {
2852
- eltData.queuedRequests.push(function () {
3163
+ eltData.queuedRequests.push(function() {
2853
3164
  issueAjaxRequest(verb, path, elt, event, etc)
2854
3165
  })
2855
3166
  } else if (queueStrategy === 'last') {
2856
3167
  eltData.queuedRequests = [] // dump existing queue
2857
- eltData.queuedRequests.push(function () {
3168
+ eltData.queuedRequests.push(function() {
2858
3169
  issueAjaxRequest(verb, path, elt, event, etc)
2859
3170
  })
2860
3171
  }
@@ -2866,7 +3177,7 @@ const htmx = (function () {
2866
3177
  const xhr = new XMLHttpRequest()
2867
3178
  eltData.xhr = xhr
2868
3179
  eltData.abortable = abortable
2869
- const endRequestLock = function () {
3180
+ const endRequestLock = function() {
2870
3181
  eltData.xhr = null
2871
3182
  eltData.abortable = false
2872
3183
  if (eltData.queuedRequests != null &&
@@ -2906,16 +3217,16 @@ const htmx = (function () {
2906
3217
  }
2907
3218
  const results = getInputValues(elt, verb)
2908
3219
  let errors = results.errors
2909
- let rawParameters = results.values
3220
+ const rawFormData = results.formData
2910
3221
  if (etc.values) {
2911
- rawParameters = mergeObjects(rawParameters, etc.values)
3222
+ overrideFormData(rawFormData, formDataFromObject(etc.values))
2912
3223
  }
2913
3224
  const expressionVars = getExpressionVars(elt)
2914
- const allParameters = mergeObjects(rawParameters, expressionVars)
2915
- let filteredParameters = filterValues(allParameters, elt)
3225
+ const allFormData = overrideFormData(rawFormData, expressionVars)
3226
+ let filteredFormData = filterValues(allFormData, elt)
2916
3227
 
2917
3228
  if (htmx.config.getCacheBusterParam && verb === 'get') {
2918
- filteredParameters['org.htmx.cache-buster'] = getRawAttribute(target, 'id') || 'true'
3229
+ filteredFormData.set('org.htmx.cache-buster', getRawAttribute(target, 'id') || 'true')
2919
3230
  }
2920
3231
 
2921
3232
  // behavior of anchors w/ empty href is to use the current URL
@@ -2932,8 +3243,10 @@ const htmx = (function () {
2932
3243
  const requestConfig = {
2933
3244
  boosted: eltIsBoosted,
2934
3245
  useUrlParams,
2935
- parameters: filteredParameters,
2936
- unfilteredParameters: allParameters,
3246
+ formData: filteredFormData,
3247
+ parameters: formDataProxy(filteredFormData),
3248
+ unfilteredFormData: allFormData,
3249
+ unfilteredParameters: formDataProxy(allFormData),
2937
3250
  headers,
2938
3251
  target,
2939
3252
  verb,
@@ -2954,7 +3267,7 @@ const htmx = (function () {
2954
3267
  path = requestConfig.path
2955
3268
  verb = requestConfig.verb
2956
3269
  headers = requestConfig.headers
2957
- filteredParameters = requestConfig.parameters
3270
+ filteredFormData = formDataFromObject(requestConfig.parameters)
2958
3271
  errors = requestConfig.errors
2959
3272
  useUrlParams = requestConfig.useUrlParams
2960
3273
 
@@ -2972,14 +3285,14 @@ const htmx = (function () {
2972
3285
  let finalPath = path
2973
3286
  if (useUrlParams) {
2974
3287
  finalPath = pathNoAnchor
2975
- const values = Object.keys(filteredParameters).length !== 0
2976
- if (values) {
3288
+ const hasValues = !filteredFormData.keys().next().done
3289
+ if (hasValues) {
2977
3290
  if (finalPath.indexOf('?') < 0) {
2978
3291
  finalPath += '?'
2979
3292
  } else {
2980
3293
  finalPath += '&'
2981
3294
  }
2982
- finalPath += urlEncode(filteredParameters)
3295
+ finalPath += urlEncode(filteredFormData)
2983
3296
  if (anchor) {
2984
3297
  finalPath += '#' + anchor
2985
3298
  }
@@ -3023,7 +3336,7 @@ const htmx = (function () {
3023
3336
  }
3024
3337
  }
3025
3338
 
3026
- xhr.onload = function () {
3339
+ xhr.onload = function() {
3027
3340
  try {
3028
3341
  const hierarchy = hierarchyForElt(elt)
3029
3342
  responseInfo.pathInfo.responsePath = getPathFromResponse(xhr)
@@ -3053,21 +3366,21 @@ const htmx = (function () {
3053
3366
  throw e
3054
3367
  }
3055
3368
  }
3056
- xhr.onerror = function () {
3369
+ xhr.onerror = function() {
3057
3370
  removeRequestIndicators(indicators, disableElts)
3058
3371
  triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo)
3059
3372
  triggerErrorEvent(elt, 'htmx:sendError', responseInfo)
3060
3373
  maybeCall(reject)
3061
3374
  endRequestLock()
3062
3375
  }
3063
- xhr.onabort = function () {
3376
+ xhr.onabort = function() {
3064
3377
  removeRequestIndicators(indicators, disableElts)
3065
3378
  triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo)
3066
3379
  triggerErrorEvent(elt, 'htmx:sendAbort', responseInfo)
3067
3380
  maybeCall(reject)
3068
3381
  endRequestLock()
3069
3382
  }
3070
- xhr.ontimeout = function () {
3383
+ xhr.ontimeout = function() {
3071
3384
  removeRequestIndicators(indicators, disableElts)
3072
3385
  triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo)
3073
3386
  triggerErrorEvent(elt, 'htmx:timeout', responseInfo)
@@ -3082,9 +3395,9 @@ const htmx = (function () {
3082
3395
  var indicators = addRequestIndicatorClasses(elt)
3083
3396
  var disableElts = disableElements(elt)
3084
3397
 
3085
- forEach(['loadstart', 'loadend', 'progress', 'abort'], function (eventName) {
3086
- forEach([xhr, xhr.upload], function (target) {
3087
- target.addEventListener(eventName, function (event) {
3398
+ forEach(['loadstart', 'loadend', 'progress', 'abort'], function(eventName) {
3399
+ forEach([xhr, xhr.upload], function(target) {
3400
+ target.addEventListener(eventName, function(event) {
3088
3401
  triggerEvent(elt, 'htmx:xhr:' + eventName, {
3089
3402
  lengthComputable: event.lengthComputable,
3090
3403
  loaded: event.loaded,
@@ -3094,12 +3407,12 @@ const htmx = (function () {
3094
3407
  })
3095
3408
  })
3096
3409
  triggerEvent(elt, 'htmx:beforeSend', responseInfo)
3097
- const params = useUrlParams ? null : encodeParamsForBody(xhr, elt, filteredParameters)
3410
+ const params = useUrlParams ? null : encodeParamsForBody(xhr, elt, filteredFormData)
3098
3411
  xhr.send(params)
3099
3412
  return promise
3100
3413
  }
3101
3414
 
3102
- function determineHistoryUpdates (elt, responseInfo) {
3415
+ function determineHistoryUpdates(elt, responseInfo) {
3103
3416
  const xhr = responseInfo.xhr
3104
3417
 
3105
3418
  //= ==========================================
@@ -3166,8 +3479,7 @@ const htmx = (function () {
3166
3479
  }
3167
3480
 
3168
3481
  // restore any anchor associated with the request
3169
- if (responseInfo.pathInfo.anchor &&
3170
- path.indexOf('#') === -1) {
3482
+ if (responseInfo.pathInfo.anchor && path.indexOf('#') === -1) {
3171
3483
  path = path + '#' + responseInfo.pathInfo.anchor
3172
3484
  }
3173
3485
 
@@ -3180,17 +3492,45 @@ const htmx = (function () {
3180
3492
  }
3181
3493
  }
3182
3494
 
3183
- function handleAjaxResponse (elt, responseInfo) {
3495
+ function codeMatches(responseHandlingConfig, status) {
3496
+ var regExp = new RegExp(responseHandlingConfig.code)
3497
+ return regExp.test(status)
3498
+ }
3499
+
3500
+ function resolveResponseHandling(xhr) {
3501
+ for (var i = 0; i < htmx.config.responseHandling.length; i++) {
3502
+ var responseHandlingElement = htmx.config.responseHandling[i]
3503
+ if (codeMatches(responseHandlingElement, xhr.status)) {
3504
+ return responseHandlingElement
3505
+ }
3506
+ }
3507
+ // no matches, return no swap
3508
+ return {
3509
+ swap: false
3510
+ }
3511
+ }
3512
+
3513
+ function handleTitle(title) {
3514
+ if (title) {
3515
+ const titleElt = find('title')
3516
+ if (titleElt) {
3517
+ titleElt.innerHTML = title
3518
+ } else {
3519
+ window.document.title = title
3520
+ }
3521
+ }
3522
+ }
3523
+
3524
+ function handleAjaxResponse(elt, responseInfo) {
3184
3525
  const xhr = responseInfo.xhr
3185
3526
  let target = responseInfo.target
3186
3527
  const etc = responseInfo.etc
3187
- const requestConfig = responseInfo.requestConfig
3188
- const select = responseInfo.select
3528
+ const responseInfoSelect = responseInfo.select
3189
3529
 
3190
3530
  if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return
3191
3531
 
3192
3532
  if (hasHeader(xhr, /HX-Trigger:/i)) {
3193
- handleTrigger(xhr, 'HX-Trigger', elt)
3533
+ handleTriggerHeader(xhr, 'HX-Trigger', elt)
3194
3534
  }
3195
3535
 
3196
3536
  if (hasHeader(xhr, /HX-Location:/i)) {
@@ -3203,7 +3543,7 @@ const htmx = (function () {
3203
3543
  redirectPath = swapSpec.path
3204
3544
  delete swapSpec.path
3205
3545
  }
3206
- ajaxHelper('GET', redirectPath, swapSpec).then(function () {
3546
+ ajaxHelper('GET', redirectPath, swapSpec).then(function() {
3207
3547
  pushUrlIntoHistory(redirectPath)
3208
3548
  })
3209
3549
  return
@@ -3232,21 +3572,49 @@ const htmx = (function () {
3232
3572
 
3233
3573
  const historyUpdate = determineHistoryUpdates(elt, responseInfo)
3234
3574
 
3235
- // by default htmx only swaps on 200 return codes and does not swap
3236
- // on 204 'No Content'
3237
- // this can be ovverriden by responding to the htmx:beforeSwap event and
3238
- // overriding the detail.shouldSwap property
3239
- const shouldSwap = xhr.status >= 200 && xhr.status < 400 && xhr.status !== 204
3240
- let serverResponse = xhr.response
3241
- let isError = xhr.status >= 400
3242
- let ignoreTitle = htmx.config.ignoreTitle
3243
- const beforeSwapDetails = mergeObjects({ shouldSwap, serverResponse, isError, ignoreTitle }, responseInfo)
3575
+ const responseHandling = resolveResponseHandling(xhr)
3576
+ const shouldSwap = responseHandling.swap
3577
+ let isError = !!responseHandling.error
3578
+ let ignoreTitle = htmx.config.ignoreTitle || responseHandling.ignoreTitle
3579
+ let selectOverride = responseHandling.select
3580
+ if (responseHandling.target) {
3581
+ responseInfo.target = querySelectorExt(elt, responseHandling.target)
3582
+ }
3583
+ var swapOverride = etc.swapOverride
3584
+ if (swapOverride == null && responseHandling.swapOverride) {
3585
+ swapOverride = responseHandling.swapOverride
3586
+ }
3587
+
3588
+ // response headers override response handling config
3589
+ if (hasHeader(xhr, /HX-Retarget:/i)) {
3590
+ if (xhr.getResponseHeader('HX-Retarget') === 'this') {
3591
+ responseInfo.target = elt
3592
+ } else {
3593
+ responseInfo.target = querySelectorExt(elt, xhr.getResponseHeader('HX-Retarget'))
3594
+ }
3595
+ }
3596
+ if (hasHeader(xhr, /HX-Reswap:/i)) {
3597
+ swapOverride = xhr.getResponseHeader('HX-Reswap')
3598
+ }
3599
+
3600
+ var serverResponse = xhr.response
3601
+ var beforeSwapDetails = mergeObjects({
3602
+ shouldSwap,
3603
+ serverResponse,
3604
+ isError,
3605
+ ignoreTitle,
3606
+ selectOverride
3607
+ }, responseInfo)
3608
+
3609
+ if (responseHandling.event && !triggerEvent(target, responseHandling.event, beforeSwapDetails)) return
3610
+
3244
3611
  if (!triggerEvent(target, 'htmx:beforeSwap', beforeSwapDetails)) return
3245
3612
 
3246
3613
  target = beforeSwapDetails.target // allow re-targeting
3247
3614
  serverResponse = beforeSwapDetails.serverResponse // allow updating content
3248
3615
  isError = beforeSwapDetails.isError // allow updating error
3249
3616
  ignoreTitle = beforeSwapDetails.ignoreTitle // allow updating ignoring title
3617
+ selectOverride = beforeSwapDetails.selectOverride // allow updating select override
3250
3618
 
3251
3619
  responseInfo.target = target // Make updated target available to response events
3252
3620
  responseInfo.failed = isError // Make failed property available to response events
@@ -3257,7 +3625,7 @@ const htmx = (function () {
3257
3625
  cancelPolling(elt)
3258
3626
  }
3259
3627
 
3260
- withExtensions(elt, function (extension) {
3628
+ withExtensions(elt, function(extension) {
3261
3629
  serverResponse = extension.transformResponse(serverResponse, xhr, elt)
3262
3630
  })
3263
3631
 
@@ -3266,14 +3634,13 @@ const htmx = (function () {
3266
3634
  saveCurrentPageToHistory()
3267
3635
  }
3268
3636
 
3269
- let swapOverride = etc.swapOverride
3270
3637
  if (hasHeader(xhr, /HX-Reswap:/i)) {
3271
3638
  swapOverride = xhr.getResponseHeader('HX-Reswap')
3272
3639
  }
3273
3640
  var swapSpec = getSwapSpecification(elt, swapOverride)
3274
3641
 
3275
- if (swapSpec.hasOwnProperty('ignoreTitle')) {
3276
- ignoreTitle = swapSpec.ignoreTitle
3642
+ if (!swapSpec.hasOwnProperty('ignoreTitle')) {
3643
+ swapSpec.ignoreTitle = ignoreTitle
3277
3644
  }
3278
3645
 
3279
3646
  target.classList.add(htmx.config.swappingClass)
@@ -3282,31 +3649,19 @@ const htmx = (function () {
3282
3649
  let settleResolve = null
3283
3650
  let settleReject = null
3284
3651
 
3285
- let doSwap = function () {
3286
- try {
3287
- const activeElt = document.activeElement
3288
- let selectionInfo = {}
3289
- try {
3290
- selectionInfo = {
3291
- elt: activeElt,
3292
- // @ts-ignore
3293
- start: activeElt ? activeElt.selectionStart : null,
3294
- // @ts-ignore
3295
- end: activeElt ? activeElt.selectionEnd : null
3296
- }
3297
- } catch (e) {
3298
- // safari issue - see https://github.com/microsoft/playwright/issues/5894
3299
- }
3652
+ if (responseInfoSelect) {
3653
+ selectOverride = responseInfoSelect
3654
+ }
3300
3655
 
3301
- let selectOverride
3302
- if (select) {
3303
- selectOverride = select
3304
- }
3656
+ if (hasHeader(xhr, /HX-Reselect:/i)) {
3657
+ selectOverride = xhr.getResponseHeader('HX-Reselect')
3658
+ }
3305
3659
 
3306
- if (hasHeader(xhr, /HX-Reselect:/i)) {
3307
- selectOverride = xhr.getResponseHeader('HX-Reselect')
3308
- }
3660
+ const selectOOB = getClosestAttributeValue(elt, 'hx-select-oob')
3661
+ const select = getClosestAttributeValue(elt, 'hx-select')
3309
3662
 
3663
+ let doSwap = function() {
3664
+ try {
3310
3665
  // if we need to save history, do so, before swapping so that relative resources have the correct base URL
3311
3666
  if (historyUpdate.type) {
3312
3667
  triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo))
@@ -3319,88 +3674,32 @@ const htmx = (function () {
3319
3674
  }
3320
3675
  }
3321
3676
 
3322
- const settleInfo = makeSettleInfo(target)
3323
- selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, selectOverride)
3324
-
3325
- if (selectionInfo.elt &&
3326
- !bodyContains(selectionInfo.elt) &&
3327
- getRawAttribute(selectionInfo.elt, 'id')) {
3328
- const newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, 'id'))
3329
- const focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }
3330
- if (newActiveElt) {
3331
- // @ts-ignore
3332
- if (selectionInfo.start && newActiveElt.setSelectionRange) {
3333
- // @ts-ignore
3334
- try {
3335
- newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end)
3336
- } catch (e) {
3337
- // the setSelectionRange method is present on fields that don't support it, so just let this fail
3677
+ swap(target, serverResponse, swapSpec, {
3678
+ select: selectOverride || select,
3679
+ selectOOB,
3680
+ eventInfo: responseInfo,
3681
+ anchor: responseInfo.pathInfo.anchor,
3682
+ contextElement: elt,
3683
+ afterSwapCallback: function() {
3684
+ if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
3685
+ let finalElt = elt
3686
+ if (!bodyContains(elt)) {
3687
+ finalElt = getDocument().body
3338
3688
  }
3689
+ handleTriggerHeader(xhr, 'HX-Trigger-After-Swap', finalElt)
3339
3690
  }
3340
- newActiveElt.focus(focusOptions)
3341
- }
3342
- }
3343
-
3344
- target.classList.remove(htmx.config.swappingClass)
3345
- forEach(settleInfo.elts, function (elt) {
3346
- if (elt.classList) {
3347
- elt.classList.add(htmx.config.settlingClass)
3348
- }
3349
- triggerEvent(elt, 'htmx:afterSwap', responseInfo)
3350
- })
3351
-
3352
- if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
3353
- let finalElt = elt
3354
- if (!bodyContains(elt)) {
3355
- finalElt = getDocument().body
3356
- }
3357
- handleTrigger(xhr, 'HX-Trigger-After-Swap', finalElt)
3358
- }
3359
-
3360
- const doSettle = function () {
3361
- forEach(settleInfo.tasks, function (task) {
3362
- task.call()
3363
- })
3364
- forEach(settleInfo.elts, function (elt) {
3365
- if (elt.classList) {
3366
- elt.classList.remove(htmx.config.settlingClass)
3367
- }
3368
- triggerEvent(elt, 'htmx:afterSettle', responseInfo)
3369
- })
3370
-
3371
- if (responseInfo.pathInfo.anchor) {
3372
- const anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor)
3373
- if (anchorTarget) {
3374
- anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
3375
- }
3376
- }
3377
-
3378
- if (settleInfo.title && !ignoreTitle) {
3379
- const titleElt = find('title')
3380
- if (titleElt) {
3381
- titleElt.innerHTML = settleInfo.title
3382
- } else {
3383
- window.document.title = settleInfo.title
3384
- }
3385
- }
3386
-
3387
- updateScrollState(settleInfo.elts, swapSpec)
3388
-
3389
- if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
3390
- let finalElt = elt
3391
- if (!bodyContains(elt)) {
3392
- finalElt = getDocument().body
3691
+ },
3692
+ afterSettleCallback: function() {
3693
+ if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
3694
+ let finalElt = elt
3695
+ if (!bodyContains(elt)) {
3696
+ finalElt = getDocument().body
3697
+ }
3698
+ handleTriggerHeader(xhr, 'HX-Trigger-After-Settle', finalElt)
3393
3699
  }
3394
- handleTrigger(xhr, 'HX-Trigger-After-Settle', finalElt)
3700
+ maybeCall(settleResolve)
3395
3701
  }
3396
- maybeCall(settleResolve)
3397
- }
3398
-
3399
- if (swapSpec.settleDelay > 0) {
3400
- setTimeout(doSettle, swapSpec.settleDelay)
3401
- } else {
3402
- doSettle()
3403
- }
3702
+ })
3404
3703
  } catch (e) {
3405
3704
  triggerErrorEvent(elt, 'htmx:swapError', responseInfo)
3406
3705
  maybeCall(settleReject)
@@ -3414,16 +3713,16 @@ const htmx = (function () {
3414
3713
  }
3415
3714
 
3416
3715
  if (shouldTransition &&
3417
- triggerEvent(elt, 'htmx:beforeTransition', responseInfo) &&
3418
- typeof Promise !== 'undefined' && document.startViewTransition) {
3419
- const settlePromise = new Promise(function (_resolve, _reject) {
3716
+ triggerEvent(elt, 'htmx:beforeTransition', responseInfo) &&
3717
+ typeof Promise !== 'undefined' && document.startViewTransition) {
3718
+ const settlePromise = new Promise(function(_resolve, _reject) {
3420
3719
  settleResolve = _resolve
3421
3720
  settleReject = _reject
3422
3721
  })
3423
3722
  // wrap the original doSwap() in a call to startViewTransition()
3424
3723
  const innerDoSwap = doSwap
3425
- doSwap = function () {
3426
- document.startViewTransition(function () {
3724
+ doSwap = function() {
3725
+ document.startViewTransition(function() {
3427
3726
  innerDoSwap()
3428
3727
  return settlePromise
3429
3728
  })
@@ -3452,14 +3751,14 @@ const htmx = (function () {
3452
3751
  * extensionBase defines the default functions for all extensions.
3453
3752
  * @returns {import("./htmx").HtmxExtension}
3454
3753
  */
3455
- function extensionBase () {
3754
+ function extensionBase() {
3456
3755
  return {
3457
- init: function (api) { return null },
3458
- onEvent: function (name, evt) { return true },
3459
- transformResponse: function (text, xhr, elt) { return text },
3460
- isInlineSwap: function (swapStyle) { return false },
3461
- handleSwap: function (swapStyle, target, fragment, settleInfo) { return false },
3462
- encodeParameters: function (xhr, parameters, elt) { return null }
3756
+ init: function(api) { return null },
3757
+ onEvent: function(name, evt) { return true },
3758
+ transformResponse: function(text, xhr, elt) { return text },
3759
+ isInlineSwap: function(swapStyle) { return false },
3760
+ handleSwap: function(swapStyle, target, fragment, settleInfo) { return false },
3761
+ encodeParameters: function(xhr, parameters, elt) { return null }
3463
3762
  }
3464
3763
  }
3465
3764
 
@@ -3469,7 +3768,7 @@ const htmx = (function () {
3469
3768
  * @param {string} name
3470
3769
  * @param {import("./htmx").HtmxExtension} extension
3471
3770
  */
3472
- function defineExtension (name, extension) {
3771
+ function defineExtension(name, extension) {
3473
3772
  if (extension.init) {
3474
3773
  extension.init(internalAPI)
3475
3774
  }
@@ -3481,7 +3780,7 @@ const htmx = (function () {
3481
3780
  *
3482
3781
  * @param {string} name
3483
3782
  */
3484
- function removeExtension (name) {
3783
+ function removeExtension(name) {
3485
3784
  delete extensions[name]
3486
3785
  }
3487
3786
 
@@ -3492,19 +3791,19 @@ const htmx = (function () {
3492
3791
  * @param {import("./htmx").HtmxExtension[]=} extensionsToReturn
3493
3792
  * @param {import("./htmx").HtmxExtension[]=} extensionsToIgnore
3494
3793
  */
3495
- function getExtensions (elt, extensionsToReturn, extensionsToIgnore) {
3496
- if (elt == undefined) {
3497
- return extensionsToReturn
3498
- }
3794
+ function getExtensions(elt, extensionsToReturn, extensionsToIgnore) {
3499
3795
  if (extensionsToReturn == undefined) {
3500
3796
  extensionsToReturn = []
3501
3797
  }
3798
+ if (elt == undefined) {
3799
+ return extensionsToReturn
3800
+ }
3502
3801
  if (extensionsToIgnore == undefined) {
3503
3802
  extensionsToIgnore = []
3504
3803
  }
3505
3804
  const extensionsForElement = getAttributeValue(elt, 'hx-ext')
3506
3805
  if (extensionsForElement) {
3507
- forEach(extensionsForElement.split(','), function (extensionName) {
3806
+ forEach(extensionsForElement.split(','), function(extensionName) {
3508
3807
  extensionName = extensionName.replace(/ /g, '')
3509
3808
  if (extensionName.slice(0, 7) == 'ignore:') {
3510
3809
  extensionsToIgnore.push(extensionName.slice(7))
@@ -3524,37 +3823,29 @@ const htmx = (function () {
3524
3823
  //= ===================================================================
3525
3824
  // Initialization
3526
3825
  //= ===================================================================
3527
- /**
3528
- * We want to initialize the page elements after DOMContentLoaded
3529
- * fires, but there isn't always a good way to tell whether
3530
- * it has already fired when we get here or not.
3531
- */
3532
- function ready (functionToCall) {
3533
- // call the function exactly once no matter how many times this is called
3534
- const callReadyFunction = function () {
3535
- if (!functionToCall) return
3536
- functionToCall()
3537
- functionToCall = null
3538
- }
3826
+ var isReady = false
3827
+ getDocument().addEventListener('DOMContentLoaded', function() {
3828
+ isReady = true
3829
+ })
3539
3830
 
3540
- if (getDocument().readyState === 'complete') {
3541
- // DOMContentLoaded definitely fired, we can initialize the page
3542
- callReadyFunction()
3831
+ /**
3832
+ * Execute a function now if DOMContentLoaded has fired, otherwise listen for it.
3833
+ *
3834
+ * This function uses isReady because there is no realiable way to ask the browswer whether
3835
+ * the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded
3836
+ * firing and readystate=complete.
3837
+ */
3838
+ function ready(fn) {
3839
+ // Checking readyState here is a failsafe in case the htmx script tag entered the DOM by
3840
+ // some means other than the initial page load.
3841
+ if (isReady || getDocument().readyState === 'complete') {
3842
+ fn()
3543
3843
  } else {
3544
- /* DOMContentLoaded *maybe* already fired, wait for
3545
- * the next DOMContentLoaded or readystatechange event
3546
- */
3547
- getDocument().addEventListener('DOMContentLoaded', function () {
3548
- callReadyFunction()
3549
- })
3550
- getDocument().addEventListener('readystatechange', function () {
3551
- if (getDocument().readyState !== 'complete') return
3552
- callReadyFunction()
3553
- })
3844
+ getDocument().addEventListener('DOMContentLoaded', fn)
3554
3845
  }
3555
3846
  }
3556
3847
 
3557
- function insertIndicatorStyles () {
3848
+ function insertIndicatorStyles() {
3558
3849
  if (htmx.config.includeIndicatorStyles !== false) {
3559
3850
  getDocument().head.insertAdjacentHTML('beforeend',
3560
3851
  '<style>\
@@ -3565,7 +3856,7 @@ const htmx = (function () {
3565
3856
  }
3566
3857
  }
3567
3858
 
3568
- function getMetaConfig () {
3859
+ function getMetaConfig() {
3569
3860
  const element = getDocument().querySelector('meta[name="htmx-config"]')
3570
3861
  if (element) {
3571
3862
  // @ts-ignore
@@ -3575,7 +3866,7 @@ const htmx = (function () {
3575
3866
  }
3576
3867
  }
3577
3868
 
3578
- function mergeMetaConfig () {
3869
+ function mergeMetaConfig() {
3579
3870
  const metaConfig = getMetaConfig()
3580
3871
  if (metaConfig) {
3581
3872
  htmx.config = mergeObjects(htmx.config, metaConfig)
@@ -3583,7 +3874,7 @@ const htmx = (function () {
3583
3874
  }
3584
3875
 
3585
3876
  // initialize the document
3586
- ready(function () {
3877
+ ready(function() {
3587
3878
  mergeMetaConfig()
3588
3879
  insertIndicatorStyles()
3589
3880
  let body = getDocument().body
@@ -3591,7 +3882,7 @@ const htmx = (function () {
3591
3882
  const restoredElts = getDocument().querySelectorAll(
3592
3883
  "[hx-trigger='restored'],[data-hx-trigger='restored']"
3593
3884
  )
3594
- body.addEventListener('htmx:abort', function (evt) {
3885
+ body.addEventListener('htmx:abort', function(evt) {
3595
3886
  const target = evt.target
3596
3887
  const internalData = getInternalData(target)
3597
3888
  if (internalData && internalData.xhr) {
@@ -3601,10 +3892,10 @@ const htmx = (function () {
3601
3892
  /** @type {(ev: PopStateEvent) => any} */
3602
3893
  const originalPopstate = window.onpopstate ? window.onpopstate.bind(window) : null
3603
3894
  /** @type {(ev: PopStateEvent) => any} */
3604
- window.onpopstate = function (event) {
3895
+ window.onpopstate = function(event) {
3605
3896
  if (event.state && event.state.htmx) {
3606
3897
  restoreHistory()
3607
- forEach(restoredElts, function (elt) {
3898
+ forEach(restoredElts, function(elt) {
3608
3899
  triggerEvent(elt, 'htmx:restored', {
3609
3900
  document: getDocument(),
3610
3901
  triggerEvent
@@ -3616,14 +3907,13 @@ const htmx = (function () {
3616
3907
  }
3617
3908
  }
3618
3909
  }
3619
- setTimeout(function () {
3910
+ setTimeout(function() {
3620
3911
  triggerEvent(body, 'htmx:load', {}) // give ready handlers a chance to load up before firing this event
3621
3912
  body = null // kill reference for gc
3622
3913
  }, 0)
3623
3914
  })
3624
3915
 
3625
3916
  return htmx
3626
- }
3627
- )()
3917
+ })()
3628
3918
  return htmx
3629
3919
  })