htmx.org 2.0.0-alpha1 → 2.0.0-beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/htmx.js CHANGED
@@ -1,78 +1,308 @@
1
- const htmx = (function () {
1
+ var htmx = (function() {
2
2
  'use strict'
3
3
 
4
4
  // Public API
5
- //* * @type {import("./htmx").HtmxApi} */
6
- // TODO: list all methods in public API
7
5
  const htmx = {
8
- onLoad: onLoadHelper,
9
- process: processNode,
10
- on: addEventListenerImpl,
11
- off: removeEventListenerImpl,
12
- trigger: triggerEvent,
13
- ajax: ajaxHelper,
14
- find,
15
- findAll,
16
- closest,
17
- values: function (elt, type) {
6
+ // Tsc madness here, assigning the functions directly results in an invalid TypeScript output, but reassigning is fine
7
+ /* Event processing */
8
+ /** @type {typeof onLoadHelper} */
9
+ onLoad: null,
10
+ /** @type {typeof processNode} */
11
+ process: null,
12
+ /** @type {typeof addEventListenerImpl} */
13
+ on: null,
14
+ /** @type {typeof removeEventListenerImpl} */
15
+ off: null,
16
+ /** @type {typeof triggerEvent} */
17
+ trigger: null,
18
+ /** @type {typeof ajaxHelper} */
19
+ ajax: null,
20
+ /* DOM querying helpers */
21
+ /** @type {typeof find} */
22
+ find: null,
23
+ /** @type {typeof findAll} */
24
+ findAll: null,
25
+ /** @type {typeof closest} */
26
+ closest: null,
27
+ /**
28
+ * Returns the input values that would resolve for a given element via the htmx value resolution mechanism
29
+ *
30
+ * @see https://htmx.org/api/#values
31
+ *
32
+ * @param {Element} elt the element to resolve values on
33
+ * @param {HttpVerb} type the request type (e.g. **get** or **post**) non-GET's will include the enclosing form of the element. Defaults to **post**
34
+ * @returns {Object}
35
+ */
36
+ values: function(elt, type) {
18
37
  const inputValues = getInputValues(elt, type || 'post')
19
38
  return inputValues.values
20
39
  },
21
- remove: removeElement,
22
- addClass: addClassToElement,
23
- removeClass: removeClassFromElement,
24
- toggleClass: toggleClassOnElement,
25
- takeClass: takeClassForElement,
26
- defineExtension,
27
- removeExtension,
28
- logAll,
29
- logNone,
40
+ /* DOM manipulation helpers */
41
+ /** @type {typeof removeElement} */
42
+ remove: null,
43
+ /** @type {typeof addClassToElement} */
44
+ addClass: null,
45
+ /** @type {typeof removeClassFromElement} */
46
+ removeClass: null,
47
+ /** @type {typeof toggleClassOnElement} */
48
+ toggleClass: null,
49
+ /** @type {typeof takeClassForElement} */
50
+ takeClass: null,
51
+ /** @type {typeof swap} */
52
+ swap: null,
53
+ /* Extension entrypoints */
54
+ /** @type {typeof defineExtension} */
55
+ defineExtension: null,
56
+ /** @type {typeof removeExtension} */
57
+ removeExtension: null,
58
+ /* Debugging */
59
+ /** @type {typeof logAll} */
60
+ logAll: null,
61
+ /** @type {typeof logNone} */
62
+ logNone: null,
63
+ /* Debugging */
64
+ /**
65
+ * The logger htmx uses to log with
66
+ *
67
+ * @see https://htmx.org/api/#logger
68
+ */
30
69
  logger: null,
70
+ /**
71
+ * A property holding the configuration htmx uses at runtime.
72
+ *
73
+ * Note that using a [meta tag](https://htmx.org/docs/#config) is the preferred mechanism for setting these properties.
74
+ *
75
+ * @see https://htmx.org/api/#config
76
+ */
31
77
  config: {
78
+ /**
79
+ * Whether to use history.
80
+ * @type boolean
81
+ * @default true
82
+ */
32
83
  historyEnabled: true,
84
+ /**
85
+ * The number of pages to keep in **localStorage** for history support.
86
+ * @type number
87
+ * @default 10
88
+ */
33
89
  historyCacheSize: 10,
90
+ /**
91
+ * @type boolean
92
+ * @default false
93
+ */
34
94
  refreshOnHistoryMiss: false,
95
+ /**
96
+ * The default swap style to use if **[hx-swap](https://htmx.org/attributes/hx-swap)** is omitted.
97
+ * @type HtmxSwapStyle
98
+ * @default 'innerHTML'
99
+ */
35
100
  defaultSwapStyle: 'innerHTML',
101
+ /**
102
+ * The default delay between receiving a response from the server and doing the swap.
103
+ * @type number
104
+ * @default 0
105
+ */
36
106
  defaultSwapDelay: 0,
107
+ /**
108
+ * The default delay between completing the content swap and settling attributes.
109
+ * @type number
110
+ * @default 20
111
+ */
37
112
  defaultSettleDelay: 20,
113
+ /**
114
+ * If true, htmx will inject a small amount of CSS into the page to make indicators invisible unless the **htmx-indicator** class is present.
115
+ * @type boolean
116
+ * @default true
117
+ */
38
118
  includeIndicatorStyles: true,
119
+ /**
120
+ * The class to place on indicators when a request is in flight.
121
+ * @type string
122
+ * @default 'htmx-indicator'
123
+ */
39
124
  indicatorClass: 'htmx-indicator',
125
+ /**
126
+ * The class to place on triggering elements when a request is in flight.
127
+ * @type string
128
+ * @default 'htmx-request'
129
+ */
40
130
  requestClass: 'htmx-request',
131
+ /**
132
+ * The class to temporarily place on elements that htmx has added to the DOM.
133
+ * @type string
134
+ * @default 'htmx-added'
135
+ */
41
136
  addedClass: 'htmx-added',
137
+ /**
138
+ * The class to place on target elements when htmx is in the settling phase.
139
+ * @type string
140
+ * @default 'htmx-settling'
141
+ */
42
142
  settlingClass: 'htmx-settling',
143
+ /**
144
+ * The class to place on target elements when htmx is in the swapping phase.
145
+ * @type string
146
+ * @default 'htmx-swapping'
147
+ */
43
148
  swappingClass: 'htmx-swapping',
149
+ /**
150
+ * Allows the use of eval-like functionality in htmx, to enable **hx-vars**, trigger conditions & script tag evaluation. Can be set to **false** for CSP compatibility.
151
+ * @type boolean
152
+ * @default true
153
+ */
44
154
  allowEval: true,
155
+ /**
156
+ * If set to false, disables the interpretation of script tags.
157
+ * @type boolean
158
+ * @default true
159
+ */
45
160
  allowScriptTags: true,
161
+ /**
162
+ * If set, the nonce will be added to inline scripts.
163
+ * @type string
164
+ * @default ''
165
+ */
46
166
  inlineScriptNonce: '',
167
+ /**
168
+ * The attributes to settle during the settling phase.
169
+ * @type string[]
170
+ * @default ['class', 'style', 'width', 'height']
171
+ */
47
172
  attributesToSettle: ['class', 'style', 'width', 'height'],
173
+ /**
174
+ * Allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates.
175
+ * @type boolean
176
+ * @default false
177
+ */
48
178
  withCredentials: false,
179
+ /**
180
+ * @type number
181
+ * @default 0
182
+ */
49
183
  timeout: 0,
184
+ /**
185
+ * The default implementation of **getWebSocketReconnectDelay** for reconnecting after unexpected connection loss by the event code **Abnormal Closure**, **Service Restart** or **Try Again Later**.
186
+ * @type {'full-jitter' | ((retryCount:number) => number)}
187
+ * @default "full-jitter"
188
+ */
50
189
  wsReconnectDelay: 'full-jitter',
190
+ /**
191
+ * The type of binary data being received over the WebSocket connection
192
+ * @type BinaryType
193
+ * @default 'blob'
194
+ */
51
195
  wsBinaryType: 'blob',
196
+ /**
197
+ * @type string
198
+ * @default '[hx-disable], [data-hx-disable]'
199
+ */
52
200
  disableSelector: '[hx-disable], [data-hx-disable]',
53
- useTemplateFragments: false,
201
+ /**
202
+ * @type {'auto' | 'instant' | 'smooth'}
203
+ * @default 'smooth'
204
+ */
54
205
  scrollBehavior: 'instant',
206
+ /**
207
+ * If the focused element should be scrolled into view.
208
+ * @type boolean
209
+ * @default false
210
+ */
55
211
  defaultFocusScroll: false,
212
+ /**
213
+ * If set to true htmx will include a cache-busting parameter in GET requests to avoid caching partial responses by the browser
214
+ * @type boolean
215
+ * @default false
216
+ */
56
217
  getCacheBusterParam: false,
218
+ /**
219
+ * If set to true, htmx will use the View Transition API when swapping in new content.
220
+ * @type boolean
221
+ * @default false
222
+ */
57
223
  globalViewTransitions: false,
224
+ /**
225
+ * htmx will format requests with these methods by encoding their parameters in the URL, not the request body
226
+ * @type {(HttpVerb)[]}
227
+ * @default ['get', 'delete']
228
+ */
58
229
  methodsThatUseUrlParams: ['get', 'delete'],
230
+ /**
231
+ * If set to true, disables htmx-based requests to non-origin hosts.
232
+ * @type boolean
233
+ * @default false
234
+ */
59
235
  selfRequestsOnly: true,
236
+ /**
237
+ * If set to true htmx will not update the title of the document when a title tag is found in new content
238
+ * @type boolean
239
+ * @default false
240
+ */
60
241
  ignoreTitle: false,
242
+ /**
243
+ * Whether the target of a boosted element is scrolled into the viewport.
244
+ * @type boolean
245
+ * @default true
246
+ */
61
247
  scrollIntoViewOnBoost: true,
62
- triggerSpecsCache: null
248
+ /**
249
+ * The cache to store evaluated trigger specifications into.
250
+ * You may define a simple object to use a never-clearing cache, or implement your own system using a [proxy object](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
251
+ * @type {Object|null}
252
+ * @default null
253
+ */
254
+ triggerSpecsCache: null,
255
+ /** @type boolean */
256
+ disableInheritance: false,
257
+ /** @type HtmxResponseHandlingConfig[] */
258
+ responseHandling: [
259
+ { code: '204', swap: false },
260
+ { code: '[23]..', swap: true },
261
+ { code: '[45]..', swap: false, error: true }
262
+ ],
263
+ /**
264
+ * Whether to process OOB swaps on elements that are nested within the main response element.
265
+ * @type boolean
266
+ * @default true
267
+ */
268
+ allowNestedOobSwaps: true
63
269
  },
64
- parseInterval,
65
- _: internalEval,
66
- version: '1.9.10'
67
- }
270
+ /** @type {typeof parseInterval} */
271
+ parseInterval: null,
272
+ /** @type {typeof internalEval} */
273
+ _: null,
274
+ version: '2.0a'
275
+ }
276
+ // Tsc madness part 2
277
+ htmx.onLoad = onLoadHelper
278
+ htmx.process = processNode
279
+ htmx.on = addEventListenerImpl
280
+ htmx.off = removeEventListenerImpl
281
+ htmx.trigger = triggerEvent
282
+ htmx.ajax = ajaxHelper
283
+ htmx.find = find
284
+ htmx.findAll = findAll
285
+ htmx.closest = closest
286
+ htmx.remove = removeElement
287
+ htmx.addClass = addClassToElement
288
+ htmx.removeClass = removeClassFromElement
289
+ htmx.toggleClass = toggleClassOnElement
290
+ htmx.takeClass = takeClassForElement
291
+ htmx.swap = swap
292
+ htmx.defineExtension = defineExtension
293
+ htmx.removeExtension = removeExtension
294
+ htmx.logAll = logAll
295
+ htmx.logNone = logNone
296
+ htmx.parseInterval = parseInterval
297
+ htmx._ = internalEval
68
298
 
69
- /** @type {import("./htmx").HtmxInternalApi} */
70
299
  const internalAPI = {
71
300
  addTriggerHandler,
72
301
  bodyContains,
73
302
  canAccessLocalStorage,
74
303
  findThisElement,
75
304
  filterValues,
305
+ swap,
76
306
  hasAttribute,
77
307
  getAttributeValue,
78
308
  getClosestAttributeValue,
@@ -89,7 +319,6 @@ const htmx = (function () {
89
319
  makeSettleInfo,
90
320
  oobSwap,
91
321
  querySelectorExt,
92
- selectAndSwap,
93
322
  settleImmediately,
94
323
  shouldCancel,
95
324
  triggerEvent,
@@ -98,13 +327,11 @@ const htmx = (function () {
98
327
  }
99
328
 
100
329
  const VERBS = ['get', 'post', 'put', 'delete', 'patch']
101
- const VERB_SELECTOR = VERBS.map(function (verb) {
330
+ const VERB_SELECTOR = VERBS.map(function(verb) {
102
331
  return '[hx-' + verb + '], [data-hx-' + verb + ']'
103
332
  }).join(', ')
104
333
 
105
334
  const HEAD_TAG_REGEX = makeTagRegEx('head')
106
- const TITLE_TAG_REGEX = makeTagRegEx('title')
107
- const SVG_TAGS_REGEX = makeTagRegEx('svg', true)
108
335
 
109
336
  //= ===================================================================
110
337
  // Utilities
@@ -115,12 +342,22 @@ const htmx = (function () {
115
342
  * @param {boolean} global
116
343
  * @returns {RegExp}
117
344
  */
118
- function makeTagRegEx (tag, global = false) {
345
+ function makeTagRegEx(tag, global = false) {
119
346
  return new RegExp(`<${tag}(\\s[^>]*>|>)([\\s\\S]*?)<\\/${tag}>`,
120
347
  global ? 'gim' : 'im')
121
348
  }
122
349
 
123
- function parseInterval (str) {
350
+ /**
351
+ * Parses an interval string consistent with the way htmx does. Useful for plugins that have timing-related attributes.
352
+ *
353
+ * Caution: Accepts an int followed by either **s** or **ms**. All other values use **parseFloat**
354
+ *
355
+ * @see https://htmx.org/api/#parseInterval
356
+ *
357
+ * @param {string} str timing string
358
+ * @returns {number|undefined}
359
+ */
360
+ function parseInterval(str) {
124
361
  if (str == undefined) {
125
362
  return undefined
126
363
  }
@@ -139,35 +376,40 @@ const htmx = (function () {
139
376
  }
140
377
 
141
378
  /**
142
- * @param {HTMLElement} elt
379
+ * @param {Node} elt
143
380
  * @param {string} name
144
381
  * @returns {(string | null)}
145
382
  */
146
- function getRawAttribute (elt, name) {
147
- return elt.getAttribute && elt.getAttribute(name)
383
+ function getRawAttribute(elt, name) {
384
+ return elt instanceof Element && elt.getAttribute(name)
148
385
  }
149
386
 
387
+ /**
388
+ * @param {Element} elt
389
+ * @param {string} qualifiedName
390
+ * @returns {boolean}
391
+ */
150
392
  // resolve with both hx and data-hx prefixes
151
- function hasAttribute (elt, qualifiedName) {
152
- return elt.hasAttribute && (elt.hasAttribute(qualifiedName) ||
393
+ function hasAttribute(elt, qualifiedName) {
394
+ return !!elt.hasAttribute && (elt.hasAttribute(qualifiedName) ||
153
395
  elt.hasAttribute('data-' + qualifiedName))
154
396
  }
155
397
 
156
398
  /**
157
399
  *
158
- * @param {HTMLElement} elt
400
+ * @param {Node} elt
159
401
  * @param {string} qualifiedName
160
402
  * @returns {(string | null)}
161
403
  */
162
- function getAttributeValue (elt, qualifiedName) {
404
+ function getAttributeValue(elt, qualifiedName) {
163
405
  return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, 'data-' + qualifiedName)
164
406
  }
165
407
 
166
408
  /**
167
- * @param {HTMLElement} elt
168
- * @returns {HTMLElement | ShadowRoot | null}
409
+ * @param {Node} elt
410
+ * @returns {Node | null}
169
411
  */
170
- function parentElt (elt) {
412
+ function parentElt(elt) {
171
413
  const parent = elt.parentElement
172
414
  if (!parent && elt.parentNode instanceof ShadowRoot) return elt.parentNode
173
415
  return parent
@@ -176,23 +418,25 @@ const htmx = (function () {
176
418
  /**
177
419
  * @returns {Document}
178
420
  */
179
- function getDocument () {
421
+ function getDocument() {
180
422
  return document
181
423
  }
182
424
 
183
425
  /**
184
- * @returns {Document | ShadowRoot}
426
+ * @param {Node} elt
427
+ * @param {boolean} global
428
+ * @returns {Node|Document}
185
429
  */
186
- function getRootNode (elt, global) {
430
+ function getRootNode(elt, global) {
187
431
  return elt.getRootNode ? elt.getRootNode({ composed: global }) : getDocument()
188
432
  }
189
433
 
190
434
  /**
191
- * @param {HTMLElement} elt
192
- * @param {(e:HTMLElement) => boolean} condition
193
- * @returns {HTMLElement | null}
435
+ * @param {Node} elt
436
+ * @param {(e:Node) => boolean} condition
437
+ * @returns {Node | null}
194
438
  */
195
- function getClosestMatch (elt, condition) {
439
+ function getClosestMatch(elt, condition) {
196
440
  while (elt && !condition(elt)) {
197
441
  elt = parentElt(elt)
198
442
  }
@@ -200,25 +444,40 @@ const htmx = (function () {
200
444
  return elt || null
201
445
  }
202
446
 
203
- function getAttributeValueWithDisinheritance (initialElement, ancestor, attributeName) {
447
+ /**
448
+ * @param {Element} initialElement
449
+ * @param {Element} ancestor
450
+ * @param {string} attributeName
451
+ * @returns {string|null}
452
+ */
453
+ function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName) {
204
454
  const attributeValue = getAttributeValue(ancestor, attributeName)
205
455
  const disinherit = getAttributeValue(ancestor, 'hx-disinherit')
206
- if (initialElement !== ancestor && disinherit && (disinherit === '*' || disinherit.split(' ').indexOf(attributeName) >= 0)) {
207
- return 'unset'
208
- } else {
209
- return attributeValue
456
+ var inherit = getAttributeValue(ancestor, 'hx-inherit')
457
+ if (initialElement !== ancestor) {
458
+ if (htmx.config.disableInheritance) {
459
+ if (inherit && (inherit === '*' || inherit.split(' ').indexOf(attributeName) >= 0)) {
460
+ return attributeValue
461
+ } else {
462
+ return null
463
+ }
464
+ }
465
+ if (disinherit && (disinherit === '*' || disinherit.split(' ').indexOf(attributeName) >= 0)) {
466
+ return 'unset'
467
+ }
210
468
  }
469
+ return attributeValue
211
470
  }
212
471
 
213
472
  /**
214
- * @param {HTMLElement} elt
473
+ * @param {Element} elt
215
474
  * @param {string} attributeName
216
475
  * @returns {string | null}
217
476
  */
218
- function getClosestAttributeValue (elt, attributeName) {
477
+ function getClosestAttributeValue(elt, attributeName) {
219
478
  let closestAttr = null
220
- getClosestMatch(elt, function (e) {
221
- return closestAttr = getAttributeValueWithDisinheritance(elt, e, attributeName)
479
+ getClosestMatch(elt, function(e) {
480
+ return !!(closestAttr = getAttributeValueWithDisinheritance(elt, asElement(e), attributeName))
222
481
  })
223
482
  if (closestAttr !== 'unset') {
224
483
  return closestAttr
@@ -226,22 +485,22 @@ const htmx = (function () {
226
485
  }
227
486
 
228
487
  /**
229
- * @param {HTMLElement} elt
488
+ * @param {Node} elt
230
489
  * @param {string} selector
231
490
  * @returns {boolean}
232
491
  */
233
- function matches (elt, selector) {
492
+ function matches(elt, selector) {
234
493
  // @ts-ignore: non-standard properties for browser compatibility
235
494
  // noinspection JSUnresolvedVariable
236
- const matchesFunction = elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector
237
- return matchesFunction && matchesFunction.call(elt, selector)
495
+ const matchesFunction = elt instanceof Element && (elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector)
496
+ return !!matchesFunction && matchesFunction.call(elt, selector)
238
497
  }
239
498
 
240
499
  /**
241
500
  * @param {string} str
242
501
  * @returns {string}
243
502
  */
244
- function getStartTag (str) {
503
+ function getStartTag(str) {
245
504
  const tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i
246
505
  const match = tagMatcher.exec(str)
247
506
  if (match) {
@@ -252,77 +511,129 @@ const htmx = (function () {
252
511
  }
253
512
 
254
513
  /**
255
- *
256
514
  * @param {string} resp
257
- * @param {number} depth
258
- * @returns {Element}
515
+ * @returns {Document}
259
516
  */
260
- function parseHTML (resp, depth) {
517
+ function parseHTML(resp) {
261
518
  const parser = new DOMParser()
262
- const responseDoc = parser.parseFromString(resp, 'text/html')
519
+ return parser.parseFromString(resp, 'text/html')
520
+ }
263
521
 
264
- /** @type {Element} */
265
- let responseNode = responseDoc.body
266
- while (depth > 0) {
267
- depth--
268
- // @ts-ignore
269
- responseNode = responseNode.firstChild
522
+ /**
523
+ * @param {DocumentFragment} fragment
524
+ * @param {Node} elt
525
+ */
526
+ function takeChildrenFor(fragment, elt) {
527
+ while (elt.childNodes.length > 0) {
528
+ fragment.append(elt.childNodes[0])
270
529
  }
271
- if (responseNode == null) {
272
- // @ts-ignore
273
- responseNode = getDocument().createDocumentFragment()
530
+ }
531
+
532
+ /**
533
+ * @param {HTMLScriptElement} script
534
+ * @returns {HTMLScriptElement}
535
+ */
536
+ function duplicateScript(script) {
537
+ const newScript = getDocument().createElement('script')
538
+ forEach(script.attributes, function(attr) {
539
+ newScript.setAttribute(attr.name, attr.value)
540
+ })
541
+ newScript.textContent = script.textContent
542
+ newScript.async = false
543
+ if (htmx.config.inlineScriptNonce) {
544
+ newScript.nonce = htmx.config.inlineScriptNonce
274
545
  }
275
- return responseNode
546
+ return newScript
276
547
  }
277
548
 
278
- function aFullPageResponse (resp) {
279
- return /<body/.test(resp)
549
+ /**
550
+ * @param {HTMLScriptElement} script
551
+ * @returns {boolean}
552
+ */
553
+ function isJavaScriptScriptNode(script) {
554
+ return script.matches('script') && (script.type === 'text/javascript' || script.type === 'module' || script.type === '')
280
555
  }
281
556
 
282
557
  /**
283
- *
284
- * @param {string} response
285
- * @returns {Element}
558
+ * we have to make new copies of script tags that we are going to insert because
559
+ * SOME browsers (not saying who, but it involves an element and an animal) don't
560
+ * execute scripts created in <template> tags when they are inserted into the DOM
561
+ * and all the others do lmao
562
+ * @param {DocumentFragment} fragment
286
563
  */
287
- function makeFragment (response) {
288
- const partialResponse = !aFullPageResponse(response)
289
- const startTag = getStartTag(response)
290
- let content = response
291
- if (startTag === 'head') {
292
- content = content.replace(HEAD_TAG_REGEX, '')
293
- }
294
- if (htmx.config.useTemplateFragments && partialResponse) {
295
- const documentFragment = parseHTML('<body><template>' + content + '</template></body>', 0)
296
- // @ts-ignore type mismatch between DocumentFragment and Element.
297
- // TODO: Are these close enough for htmx to use interchangeably?
298
- return documentFragment.querySelector('template').content
299
- }
300
- switch (startTag) {
301
- case 'thead':
302
- case 'tbody':
303
- case 'tfoot':
304
- case 'colgroup':
305
- case 'caption':
306
- return parseHTML('<table>' + content + '</table>', 1)
307
- case 'col':
308
- return parseHTML('<table><colgroup>' + content + '</colgroup></table>', 2)
309
- case 'tr':
310
- return parseHTML('<table><tbody>' + content + '</tbody></table>', 2)
311
- case 'td':
312
- case 'th':
313
- return parseHTML('<table><tbody><tr>' + content + '</tr></tbody></table>', 3)
314
- case 'script':
315
- case 'style':
316
- return parseHTML('<div>' + content + '</div>', 1)
317
- default:
318
- return parseHTML(content, 0)
564
+ function normalizeScriptTags(fragment) {
565
+ Array.from(fragment.querySelectorAll('script')).forEach(/** @param {HTMLScriptElement} script */ (script) => {
566
+ if (isJavaScriptScriptNode(script)) {
567
+ const newScript = duplicateScript(script)
568
+ const parent = script.parentNode
569
+ try {
570
+ parent.insertBefore(newScript, script)
571
+ } catch (e) {
572
+ logError(e)
573
+ } finally {
574
+ script.remove()
575
+ }
576
+ }
577
+ })
578
+ }
579
+
580
+ /**
581
+ * @typedef {DocumentFragment & {title?: string}} DocumentFragmentWithTitle
582
+ * @description a document fragment representing the response HTML, including
583
+ * a `title` property for any title information found
584
+ */
585
+
586
+ /**
587
+ * @param {string} response HTML
588
+ * @returns {DocumentFragmentWithTitle}
589
+ */
590
+ function makeFragment(response) {
591
+ // strip head tag to determine shape of response we are dealing with
592
+ const responseWithNoHead = response.replace(HEAD_TAG_REGEX, '')
593
+ const startTag = getStartTag(responseWithNoHead)
594
+ /** @type DocumentFragmentWithTitle */
595
+ let fragment
596
+ if (startTag === 'html') {
597
+ // if it is a full document, parse it and return the body
598
+ fragment = /** @type DocumentFragmentWithTitle */ (new DocumentFragment())
599
+ const doc = parseHTML(response)
600
+ takeChildrenFor(fragment, doc.body)
601
+ fragment.title = doc.title
602
+ } else if (startTag === 'body') {
603
+ // parse body w/o wrapping in template
604
+ fragment = /** @type DocumentFragmentWithTitle */ (new DocumentFragment())
605
+ const doc = parseHTML(responseWithNoHead)
606
+ takeChildrenFor(fragment, doc.body)
607
+ fragment.title = doc.title
608
+ } else {
609
+ // otherwise we have non-body partial HTML content, so wrap it in a template to maximize parsing flexibility
610
+ const doc = parseHTML('<body><template class="internal-htmx-wrapper">' + responseWithNoHead + '</template></body>')
611
+ fragment = /** @type DocumentFragmentWithTitle */ (doc.querySelector('template').content)
612
+ // extract title into fragment for later processing
613
+ fragment.title = doc.title
614
+
615
+ // for legacy reasons we support a title tag at the root level of non-body responses, so we need to handle it
616
+ var titleElement = fragment.querySelector('title')
617
+ if (titleElement && titleElement.parentNode === fragment) {
618
+ titleElement.remove()
619
+ fragment.title = titleElement.innerText
620
+ }
621
+ }
622
+ if (fragment) {
623
+ if (htmx.config.allowScriptTags) {
624
+ normalizeScriptTags(fragment)
625
+ } else {
626
+ // remove all script tags if scripts are disabled
627
+ fragment.querySelectorAll('script').forEach((script) => script.remove())
628
+ }
319
629
  }
630
+ return fragment
320
631
  }
321
632
 
322
633
  /**
323
634
  * @param {Function} func
324
635
  */
325
- function maybeCall (func) {
636
+ function maybeCall(func) {
326
637
  if (func) {
327
638
  func()
328
639
  }
@@ -333,7 +644,7 @@ const htmx = (function () {
333
644
  * @param {string} type
334
645
  * @returns
335
646
  */
336
- function isType (o, type) {
647
+ function isType(o, type) {
337
648
  return Object.prototype.toString.call(o) === '[object ' + type + ']'
338
649
  }
339
650
 
@@ -341,24 +652,65 @@ const htmx = (function () {
341
652
  * @param {*} o
342
653
  * @returns {o is Function}
343
654
  */
344
- function isFunction (o) {
345
- return isType(o, 'Function')
655
+ function isFunction(o) {
656
+ return typeof o === 'function'
346
657
  }
347
658
 
348
659
  /**
349
660
  * @param {*} o
350
661
  * @returns {o is Object}
351
662
  */
352
- function isRawObject (o) {
663
+ function isRawObject(o) {
353
664
  return isType(o, 'Object')
354
665
  }
355
666
 
667
+ /**
668
+ * @typedef {Object} OnHandler
669
+ * @property {(keyof HTMLElementEventMap)|string} event
670
+ * @property {EventListener} listener
671
+ */
672
+
673
+ /**
674
+ * @typedef {Object} ListenerInfo
675
+ * @property {string} trigger
676
+ * @property {EventListener} listener
677
+ * @property {EventTarget} on
678
+ */
679
+
680
+ /**
681
+ * @typedef {Object} HtmxNodeInternalData
682
+ * Element data
683
+ * @property {number} [initHash]
684
+ * @property {boolean} [boosted]
685
+ * @property {OnHandler[]} [onHandlers]
686
+ * @property {number} [timeout]
687
+ * @property {ListenerInfo[]} [listenerInfos]
688
+ * @property {boolean} [cancelled]
689
+ * @property {boolean} [triggeredOnce]
690
+ * @property {number} [delayed]
691
+ * @property {number|null} [throttle]
692
+ * @property {string} [lastValue]
693
+ * @property {boolean} [loaded]
694
+ * @property {string} [path]
695
+ * @property {string} [verb]
696
+ * @property {boolean} [polling]
697
+ * @property {HTMLButtonElement|HTMLInputElement|null} [lastButtonClicked]
698
+ * @property {number} [requestCount]
699
+ * @property {XMLHttpRequest} [xhr]
700
+ * @property {(() => void)[]} [queuedRequests]
701
+ * @property {boolean} [abortable]
702
+ *
703
+ * Event data
704
+ * @property {HtmxTriggerSpecification} [triggerSpec]
705
+ * @property {EventTarget[]} [handledFor]
706
+ */
707
+
356
708
  /**
357
709
  * getInternalData retrieves "private" data stored by htmx within an element
358
- * @param {HTMLElement} elt
359
- * @returns {*}
710
+ * @param {EventTarget|Event} elt
711
+ * @returns {HtmxNodeInternalData}
360
712
  */
361
- function getInternalData (elt) {
713
+ function getInternalData(elt) {
362
714
  const dataProp = 'htmx-internal-data'
363
715
  let data = elt[dataProp]
364
716
  if (!data) {
@@ -369,10 +721,11 @@ const htmx = (function () {
369
721
 
370
722
  /**
371
723
  * toArray converts an ArrayLike object into a real array.
372
- * @param {ArrayLike} arr
373
- * @returns {any[]}
724
+ * @template T
725
+ * @param {ArrayLike<T>} arr
726
+ * @returns {T[]}
374
727
  */
375
- function toArray (arr) {
728
+ function toArray(arr) {
376
729
  const returnArr = []
377
730
  if (arr) {
378
731
  for (let i = 0; i < arr.length; i++) {
@@ -382,7 +735,12 @@ const htmx = (function () {
382
735
  return returnArr
383
736
  }
384
737
 
385
- function forEach (arr, func) {
738
+ /**
739
+ * @template T
740
+ * @param {T[]|NamedNodeMap|HTMLCollection|HTMLFormControlsCollection|ArrayLike<T>} arr
741
+ * @param {(T) => void} func
742
+ */
743
+ function forEach(arr, func) {
386
744
  if (arr) {
387
745
  for (let i = 0; i < arr.length; i++) {
388
746
  func(arr[i])
@@ -390,43 +748,64 @@ const htmx = (function () {
390
748
  }
391
749
  }
392
750
 
393
- function isScrolledIntoView (el) {
751
+ /**
752
+ * @param {Element} el
753
+ * @returns {boolean}
754
+ */
755
+ function isScrolledIntoView(el) {
394
756
  const rect = el.getBoundingClientRect()
395
757
  const elemTop = rect.top
396
758
  const elemBottom = rect.bottom
397
759
  return elemTop < window.innerHeight && elemBottom >= 0
398
760
  }
399
761
 
400
- function bodyContains (elt) {
762
+ /**
763
+ * @param {Node} elt
764
+ * @returns {boolean}
765
+ */
766
+ function bodyContains(elt) {
401
767
  // IE Fix
402
- if (elt.getRootNode && elt.getRootNode() instanceof window.ShadowRoot) {
403
- return getDocument().body.contains(elt.getRootNode().host)
768
+ const rootNode = elt.getRootNode && elt.getRootNode()
769
+ if (rootNode && rootNode instanceof window.ShadowRoot) {
770
+ return getDocument().body.contains(rootNode.host)
404
771
  } else {
405
772
  return getDocument().body.contains(elt)
406
773
  }
407
774
  }
408
775
 
409
- function splitOnWhitespace (trigger) {
776
+ /**
777
+ * @param {string} trigger
778
+ * @returns {string[]}
779
+ */
780
+ function splitOnWhitespace(trigger) {
410
781
  return trigger.trim().split(/\s+/)
411
782
  }
412
783
 
413
784
  /**
414
- * mergeObjects takes all of the keys from
785
+ * mergeObjects takes all the keys from
415
786
  * obj2 and duplicates them into obj1
416
- * @param {Object} obj1
417
- * @param {Object} obj2
418
- * @returns {Object}
787
+ * @template T1
788
+ * @template T2
789
+ * @param {T1} obj1
790
+ * @param {T2} obj2
791
+ * @returns {T1 & T2}
419
792
  */
420
- function mergeObjects (obj1, obj2) {
793
+ function mergeObjects(obj1, obj2) {
421
794
  for (const key in obj2) {
422
795
  if (obj2.hasOwnProperty(key)) {
796
+ // @ts-ignore tsc doesn't seem to properly handle types merging
423
797
  obj1[key] = obj2[key]
424
798
  }
425
799
  }
800
+ // @ts-ignore tsc doesn't seem to properly handle types merging
426
801
  return obj1
427
802
  }
428
803
 
429
- function parseJSON (jString) {
804
+ /**
805
+ * @param {string} jString
806
+ * @returns {any|null}
807
+ */
808
+ function parseJSON(jString) {
430
809
  try {
431
810
  return JSON.parse(jString)
432
811
  } catch (error) {
@@ -435,7 +814,10 @@ const htmx = (function () {
435
814
  }
436
815
  }
437
816
 
438
- function canAccessLocalStorage () {
817
+ /**
818
+ * @returns {boolean}
819
+ */
820
+ function canAccessLocalStorage() {
439
821
  const test = 'htmx:localStorageTest'
440
822
  try {
441
823
  localStorage.setItem(test, test)
@@ -446,7 +828,11 @@ const htmx = (function () {
446
828
  }
447
829
  }
448
830
 
449
- function normalizePath (path) {
831
+ /**
832
+ * @param {string} path
833
+ * @returns {string}
834
+ */
835
+ function normalizePath(path) {
450
836
  try {
451
837
  const url = new URL(path)
452
838
  if (url) {
@@ -467,51 +853,101 @@ const htmx = (function () {
467
853
  // public API
468
854
  //= =========================================================================================
469
855
 
470
- function internalEval (str) {
471
- return maybeEval(getDocument().body, function () {
856
+ /**
857
+ * @param {string} str
858
+ * @returns {any}
859
+ */
860
+ function internalEval(str) {
861
+ return maybeEval(getDocument().body, function() {
472
862
  return eval(str)
473
863
  })
474
864
  }
475
865
 
476
- function onLoadHelper (callback) {
477
- const value = htmx.on('htmx:load', function (evt) {
866
+ /**
867
+ * Adds a callback for the **htmx:load** event. This can be used to process new content, for example initializing the content with a javascript library
868
+ *
869
+ * @see https://htmx.org/api/#onLoad
870
+ *
871
+ * @param {(elt: Node) => void} callback the callback to call on newly loaded content
872
+ * @returns {EventListener}
873
+ */
874
+ function onLoadHelper(callback) {
875
+ const value = htmx.on('htmx:load', /** @param {CustomEvent} evt */ function(evt) {
478
876
  callback(evt.detail.elt)
479
877
  })
480
878
  return value
481
879
  }
482
880
 
483
- function logAll () {
484
- htmx.logger = function (elt, event, data) {
881
+ /**
882
+ * Log all htmx events, useful for debugging.
883
+ *
884
+ * @see https://htmx.org/api/#logAll
885
+ */
886
+ function logAll() {
887
+ htmx.logger = function(elt, event, data) {
485
888
  if (console) {
486
889
  console.log(event, elt, data)
487
890
  }
488
891
  }
489
892
  }
490
893
 
491
- function logNone () {
894
+ function logNone() {
492
895
  htmx.logger = null
493
896
  }
494
897
 
495
- function find (eltOrSelector, selector) {
496
- if (selector) {
898
+ /**
899
+ * Finds an element matching the selector
900
+ *
901
+ * @see https://htmx.org/api/#find
902
+ *
903
+ * @param {ParentNode|string} eltOrSelector the root element to find the matching element in, inclusive | the selector to match
904
+ * @param {string} [selector] the selector to match
905
+ * @returns {Element|null}
906
+ */
907
+ function find(eltOrSelector, selector) {
908
+ if (typeof eltOrSelector !== 'string') {
497
909
  return eltOrSelector.querySelector(selector)
498
910
  } else {
499
911
  return find(getDocument(), eltOrSelector)
500
912
  }
501
913
  }
502
914
 
503
- function findAll (eltOrSelector, selector) {
504
- if (selector) {
915
+ /**
916
+ * Finds all elements matching the selector
917
+ *
918
+ * @see https://htmx.org/api/#findAll
919
+ *
920
+ * @param {ParentNode|string} eltOrSelector the root element to find the matching elements in, inclusive | the selector to match
921
+ * @param {string} [selector] the selector to match
922
+ * @returns {NodeListOf<Element>}
923
+ */
924
+ function findAll(eltOrSelector, selector) {
925
+ if (typeof eltOrSelector !== 'string') {
505
926
  return eltOrSelector.querySelectorAll(selector)
506
927
  } else {
507
928
  return findAll(getDocument(), eltOrSelector)
508
929
  }
509
930
  }
510
931
 
511
- function removeElement (elt, delay) {
932
+ /**
933
+ * @returns Window
934
+ */
935
+ function getWindow() {
936
+ return window
937
+ }
938
+
939
+ /**
940
+ * Removes an element from the DOM
941
+ *
942
+ * @see https://htmx.org/api/#remove
943
+ *
944
+ * @param {Node} elt
945
+ * @param {number} [delay]
946
+ */
947
+ function removeElement(elt, delay) {
512
948
  elt = resolveTarget(elt)
513
949
  if (delay) {
514
- setTimeout(function () {
950
+ getWindow().setTimeout(function() {
515
951
  removeElement(elt)
516
952
  elt = null
517
953
  }, delay)
@@ -520,10 +956,54 @@ const htmx = (function () {
520
956
  }
521
957
  }
522
958
 
523
- function addClassToElement (elt, clazz, delay) {
524
- elt = resolveTarget(elt)
959
+ /**
960
+ * @param {any} elt
961
+ * @return {Element|null}
962
+ */
963
+ function asElement(elt) {
964
+ return elt instanceof Element ? elt : null
965
+ }
966
+
967
+ /**
968
+ * @param {any} elt
969
+ * @return {HTMLElement|null}
970
+ */
971
+ function asHtmlElement(elt) {
972
+ return elt instanceof HTMLElement ? elt : null
973
+ }
974
+
975
+ /**
976
+ * @param {any} value
977
+ * @return {string|null}
978
+ */
979
+ function asString(value) {
980
+ return typeof value === 'string' ? value : null
981
+ }
982
+
983
+ /**
984
+ * @param {EventTarget} elt
985
+ * @return {ParentNode|null}
986
+ */
987
+ function asParentNode(elt) {
988
+ return elt instanceof Element || elt instanceof Document || elt instanceof DocumentFragment ? elt : null
989
+ }
990
+
991
+ /**
992
+ * This method adds a class to the given element.
993
+ *
994
+ * @see https://htmx.org/api/#addClass
995
+ *
996
+ * @param {Element|string} elt the element to add the class to
997
+ * @param {string} clazz the class to add
998
+ * @param {number} [delay] the delay (in milliseconds) before class is added
999
+ */
1000
+ function addClassToElement(elt, clazz, delay) {
1001
+ elt = asElement(resolveTarget(elt))
1002
+ if (!elt) {
1003
+ return
1004
+ }
525
1005
  if (delay) {
526
- setTimeout(function () {
1006
+ getWindow().setTimeout(function() {
527
1007
  addClassToElement(elt, clazz)
528
1008
  elt = null
529
1009
  }, delay)
@@ -532,10 +1012,22 @@ const htmx = (function () {
532
1012
  }
533
1013
  }
534
1014
 
535
- function removeClassFromElement (elt, clazz, delay) {
536
- elt = resolveTarget(elt)
1015
+ /**
1016
+ * Removes a class from the given element
1017
+ *
1018
+ * @see https://htmx.org/api/#removeClass
1019
+ *
1020
+ * @param {Node|string} node element to remove the class from
1021
+ * @param {string} clazz the class to remove
1022
+ * @param {number} [delay] the delay (in milliseconds before class is removed)
1023
+ */
1024
+ function removeClassFromElement(node, clazz, delay) {
1025
+ let elt = asElement(resolveTarget(node))
1026
+ if (!elt) {
1027
+ return
1028
+ }
537
1029
  if (delay) {
538
- setTimeout(function () {
1030
+ getWindow().setTimeout(function() {
539
1031
  removeClassFromElement(elt, clazz)
540
1032
  elt = null
541
1033
  }, delay)
@@ -550,22 +1042,47 @@ const htmx = (function () {
550
1042
  }
551
1043
  }
552
1044
 
553
- function toggleClassOnElement (elt, clazz) {
1045
+ /**
1046
+ * Toggles the given class on an element
1047
+ *
1048
+ * @see https://htmx.org/api/#toggleClass
1049
+ *
1050
+ * @param {Element|string} elt the element to toggle the class on
1051
+ * @param {string} clazz the class to toggle
1052
+ */
1053
+ function toggleClassOnElement(elt, clazz) {
554
1054
  elt = resolveTarget(elt)
555
1055
  elt.classList.toggle(clazz)
556
1056
  }
557
1057
 
558
- function takeClassForElement (elt, clazz) {
1058
+ /**
1059
+ * Takes the given class from its siblings, so that among its siblings, only the given element will have the class.
1060
+ *
1061
+ * @see https://htmx.org/api/#takeClass
1062
+ *
1063
+ * @param {Node|string} elt the element that will take the class
1064
+ * @param {string} clazz the class to take
1065
+ */
1066
+ function takeClassForElement(elt, clazz) {
559
1067
  elt = resolveTarget(elt)
560
- forEach(elt.parentElement.children, function (child) {
1068
+ forEach(elt.parentElement.children, function(child) {
561
1069
  removeClassFromElement(child, clazz)
562
1070
  })
563
- addClassToElement(elt, clazz)
1071
+ addClassToElement(asElement(elt), clazz)
564
1072
  }
565
1073
 
566
- function closest (elt, selector) {
567
- elt = resolveTarget(elt)
568
- if (elt.closest) {
1074
+ /**
1075
+ * Finds the closest matching element in the given elements parentage, inclusive of the element
1076
+ *
1077
+ * @see https://htmx.org/api/#closest
1078
+ *
1079
+ * @param {Element|string} elt the element to find the selector from
1080
+ * @param {string} selector the selector to find
1081
+ * @returns {Element|null}
1082
+ */
1083
+ function closest(elt, selector) {
1084
+ elt = asElement(resolveTarget(elt))
1085
+ if (elt && elt.closest) {
569
1086
  return elt.closest(selector)
570
1087
  } else {
571
1088
  // TODO remove when IE goes away
@@ -574,20 +1091,34 @@ const htmx = (function () {
574
1091
  return elt
575
1092
  }
576
1093
  }
577
- while (elt = elt && parentElt(elt))
1094
+ while (elt = elt && asElement(parentElt(elt)))
578
1095
  return null
579
1096
  }
580
1097
  }
581
1098
 
582
- function startsWith (str, prefix) {
1099
+ /**
1100
+ * @param {string} str
1101
+ * @param {string} prefix
1102
+ * @returns {boolean}
1103
+ */
1104
+ function startsWith(str, prefix) {
583
1105
  return str.substring(0, prefix.length) === prefix
584
1106
  }
585
1107
 
586
- function endsWith (str, suffix) {
1108
+ /**
1109
+ * @param {string} str
1110
+ * @param {string} suffix
1111
+ * @returns {boolean}
1112
+ */
1113
+ function endsWith(str, suffix) {
587
1114
  return str.substring(str.length - suffix.length) === suffix
588
1115
  }
589
1116
 
590
- function normalizeSelector (selector) {
1117
+ /**
1118
+ * @param {string} selector
1119
+ * @returns {string}
1120
+ */
1121
+ function normalizeSelector(selector) {
591
1122
  const trimmedSelector = selector.trim()
592
1123
  if (startsWith(trimmedSelector, '<') && endsWith(trimmedSelector, '/>')) {
593
1124
  return trimmedSelector.substring(1, trimmedSelector.length - 2)
@@ -596,17 +1127,24 @@ const htmx = (function () {
596
1127
  }
597
1128
  }
598
1129
 
599
- function querySelectorAllExt (elt, selector, global) {
1130
+ /**
1131
+ * @param {Node|Element|Document|string} elt
1132
+ * @param {string} selector
1133
+ * @param {boolean=} global
1134
+ * @returns {(Node|Window)[]}
1135
+ */
1136
+ function querySelectorAllExt(elt, selector, global) {
1137
+ elt = resolveTarget(elt)
600
1138
  if (selector.indexOf('closest ') === 0) {
601
- return [closest(elt, normalizeSelector(selector.substr(8)))]
1139
+ return [closest(asElement(elt), normalizeSelector(selector.substr(8)))]
602
1140
  } else if (selector.indexOf('find ') === 0) {
603
- return [find(elt, normalizeSelector(selector.substr(5)))]
1141
+ return [find(asParentNode(elt), normalizeSelector(selector.substr(5)))]
604
1142
  } else if (selector === 'next') {
605
- return [elt.nextElementSibling]
1143
+ return [asElement(elt).nextElementSibling]
606
1144
  } else if (selector.indexOf('next ') === 0) {
607
1145
  return [scanForwardQuery(elt, normalizeSelector(selector.substr(5)), !!global)]
608
1146
  } else if (selector === 'previous') {
609
- return [elt.previousElementSibling]
1147
+ return [asElement(elt).previousElementSibling]
610
1148
  } else if (selector.indexOf('previous ') === 0) {
611
1149
  return [scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)), !!global)]
612
1150
  } else if (selector === 'document') {
@@ -620,12 +1158,18 @@ const htmx = (function () {
620
1158
  } else if (selector.indexOf('global ') === 0) {
621
1159
  return querySelectorAllExt(elt, selector.slice(7), true)
622
1160
  } else {
623
- return getRootNode(elt, !!global).querySelectorAll(normalizeSelector(selector))
1161
+ return toArray(asParentNode(getRootNode(elt, !!global)).querySelectorAll(normalizeSelector(selector)))
624
1162
  }
625
1163
  }
626
1164
 
627
- var scanForwardQuery = function (start, match, global) {
628
- const results = getRootNode(start, global).querySelectorAll(match)
1165
+ /**
1166
+ * @param {Node} start
1167
+ * @param {string} match
1168
+ * @param {boolean} global
1169
+ * @returns {Element}
1170
+ */
1171
+ var scanForwardQuery = function(start, match, global) {
1172
+ const results = asParentNode(getRootNode(start, global)).querySelectorAll(match)
629
1173
  for (let i = 0; i < results.length; i++) {
630
1174
  const elt = results[i]
631
1175
  if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_PRECEDING) {
@@ -634,8 +1178,14 @@ const htmx = (function () {
634
1178
  }
635
1179
  }
636
1180
 
637
- var scanBackwardsQuery = function (start, match, global) {
638
- const results = getRootNode(start, global).querySelectorAll(match)
1181
+ /**
1182
+ * @param {Node} start
1183
+ * @param {string} match
1184
+ * @param {boolean} global
1185
+ * @returns {Element}
1186
+ */
1187
+ var scanBackwardsQuery = function(start, match, global) {
1188
+ const results = asParentNode(getRootNode(start, global)).querySelectorAll(match)
639
1189
  for (let i = results.length - 1; i >= 0; i--) {
640
1190
  const elt = results[i]
641
1191
  if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_FOLLOWING) {
@@ -644,40 +1194,78 @@ const htmx = (function () {
644
1194
  }
645
1195
  }
646
1196
 
647
- function querySelectorExt (eltOrSelector, selector) {
648
- if (selector) {
1197
+ /**
1198
+ * @param {Node|string} eltOrSelector
1199
+ * @param {string=} selector
1200
+ * @returns {Node|Window}
1201
+ */
1202
+ function querySelectorExt(eltOrSelector, selector) {
1203
+ if (typeof eltOrSelector !== 'string') {
649
1204
  return querySelectorAllExt(eltOrSelector, selector)[0]
650
1205
  } else {
651
1206
  return querySelectorAllExt(getDocument().body, eltOrSelector)[0]
652
1207
  }
653
1208
  }
654
1209
 
655
- function resolveTarget (arg2, context) {
656
- if (isType(arg2, 'String')) {
657
- return find(context || document, arg2)
1210
+ /**
1211
+ * @template {EventTarget} T
1212
+ * @param {T|string} eltOrSelector
1213
+ * @param {T} [context]
1214
+ * @returns {Element|T|null}
1215
+ */
1216
+ function resolveTarget(eltOrSelector, context) {
1217
+ if (typeof eltOrSelector === 'string') {
1218
+ return find(asParentNode(context) || document, eltOrSelector)
658
1219
  } else {
659
- return arg2
1220
+ return eltOrSelector
660
1221
  }
661
1222
  }
662
1223
 
663
- function processEventArgs (arg1, arg2, arg3) {
1224
+ /**
1225
+ * @typedef {keyof HTMLElementEventMap|string} AnyEventName
1226
+ */
1227
+
1228
+ /**
1229
+ * @typedef {Object} EventArgs
1230
+ * @property {EventTarget} target
1231
+ * @property {AnyEventName} event
1232
+ * @property {EventListener} listener
1233
+ */
1234
+
1235
+ /**
1236
+ * @param {EventTarget|AnyEventName} arg1
1237
+ * @param {AnyEventName|EventListener} arg2
1238
+ * @param {EventListener} [arg3]
1239
+ * @returns {EventArgs}
1240
+ */
1241
+ function processEventArgs(arg1, arg2, arg3) {
664
1242
  if (isFunction(arg2)) {
665
1243
  return {
666
1244
  target: getDocument().body,
667
- event: arg1,
1245
+ event: asString(arg1),
668
1246
  listener: arg2
669
1247
  }
670
1248
  } else {
671
1249
  return {
672
1250
  target: resolveTarget(arg1),
673
- event: arg2,
1251
+ event: asString(arg2),
674
1252
  listener: arg3
675
1253
  }
676
1254
  }
677
1255
  }
678
1256
 
679
- function addEventListenerImpl (arg1, arg2, arg3) {
680
- ready(function () {
1257
+ /**
1258
+ * Adds an event listener to an element
1259
+ *
1260
+ * @see https://htmx.org/api/#on
1261
+ *
1262
+ * @param {EventTarget|string} arg1 the element to add the listener to | the event name to add the listener for
1263
+ * @param {string|EventListener} arg2 the event name to add the listener for | the listener to add
1264
+ * @param {EventListener} [arg3] the listener to add
1265
+ * @returns {EventListener}
1266
+ */
1267
+ function addEventListenerImpl(arg1, arg2, arg3) {
1268
+ ready(function() {
681
1269
  const eventArgs = processEventArgs(arg1, arg2, arg3)
682
1270
  eventArgs.target.addEventListener(eventArgs.event, eventArgs.listener)
683
1271
  })
@@ -685,8 +1273,18 @@ const htmx = (function () {
685
1273
  return b ? arg2 : arg3
686
1274
  }
687
1275
 
688
- function removeEventListenerImpl (arg1, arg2, arg3) {
689
- ready(function () {
1276
+ /**
1277
+ * Removes an event listener from an element
1278
+ *
1279
+ * @see https://htmx.org/api/#off
1280
+ *
1281
+ * @param {EventTarget|string} arg1 the element to remove the listener from | the event name to remove the listener from
1282
+ * @param {string|EventListener} arg2 the event name to remove the listener from | the listener to remove
1283
+ * @param {EventListener} [arg3] the listener to remove
1284
+ * @returns {EventListener}
1285
+ */
1286
+ function removeEventListenerImpl(arg1, arg2, arg3) {
1287
+ ready(function() {
690
1288
  const eventArgs = processEventArgs(arg1, arg2, arg3)
691
1289
  eventArgs.target.removeEventListener(eventArgs.event, eventArgs.listener)
692
1290
  })
@@ -698,7 +1296,12 @@ const htmx = (function () {
698
1296
  //= ===================================================================
699
1297
 
700
1298
  const DUMMY_ELT = getDocument().createElement('output') // dummy element for bad selectors
701
- function findAttributeTargets (elt, attrName) {
1299
+ /**
1300
+ * @param {Element} elt
1301
+ * @param {string} attrName
1302
+ * @returns {(Node|Window)[]}
1303
+ */
1304
+ function findAttributeTargets(elt, attrName) {
702
1305
  const attrTarget = getClosestAttributeValue(elt, attrName)
703
1306
  if (attrTarget) {
704
1307
  if (attrTarget === 'this') {
@@ -715,13 +1318,22 @@ const htmx = (function () {
715
1318
  }
716
1319
  }
717
1320
 
718
- function findThisElement (elt, attribute) {
719
- return getClosestMatch(elt, function (elt) {
720
- return getAttributeValue(elt, attribute) != null
721
- })
1321
+ /**
1322
+ * @param {Element} elt
1323
+ * @param {string} attribute
1324
+ * @returns {Element|null}
1325
+ */
1326
+ function findThisElement(elt, attribute) {
1327
+ return asElement(getClosestMatch(elt, function(elt) {
1328
+ return getAttributeValue(asElement(elt), attribute) != null
1329
+ }))
722
1330
  }
723
1331
 
724
- function getTarget (elt) {
1332
+ /**
1333
+ * @param {Element} elt
1334
+ * @returns {Node|Window|null}
1335
+ */
1336
+ function getTarget(elt) {
725
1337
  const targetStr = getClosestAttributeValue(elt, 'hx-target')
726
1338
  if (targetStr) {
727
1339
  if (targetStr === 'this') {
@@ -739,7 +1351,11 @@ const htmx = (function () {
739
1351
  }
740
1352
  }
741
1353
 
742
- function shouldSettleAttribute (name) {
1354
+ /**
1355
+ * @param {string} name
1356
+ * @returns {boolean}
1357
+ */
1358
+ function shouldSettleAttribute(name) {
743
1359
  const attributesToSettle = htmx.config.attributesToSettle
744
1360
  for (let i = 0; i < attributesToSettle.length; i++) {
745
1361
  if (name === attributesToSettle[i]) {
@@ -749,20 +1365,29 @@ const htmx = (function () {
749
1365
  return false
750
1366
  }
751
1367
 
752
- function cloneAttributes (mergeTo, mergeFrom) {
753
- forEach(mergeTo.attributes, function (attr) {
1368
+ /**
1369
+ * @param {Element} mergeTo
1370
+ * @param {Element} mergeFrom
1371
+ */
1372
+ function cloneAttributes(mergeTo, mergeFrom) {
1373
+ forEach(mergeTo.attributes, function(attr) {
754
1374
  if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) {
755
1375
  mergeTo.removeAttribute(attr.name)
756
1376
  }
757
1377
  })
758
- forEach(mergeFrom.attributes, function (attr) {
1378
+ forEach(mergeFrom.attributes, function(attr) {
759
1379
  if (shouldSettleAttribute(attr.name)) {
760
1380
  mergeTo.setAttribute(attr.name, attr.value)
761
1381
  }
762
1382
  })
763
1383
  }
764
1384
 
765
- function isInlineSwap (swapStyle, target) {
1385
+ /**
1386
+ * @param {HtmxSwapStyle} swapStyle
1387
+ * @param {Element} target
1388
+ * @returns {boolean}
1389
+ */
1390
+ function isInlineSwap(swapStyle, target) {
766
1391
  const extensions = getExtensions(target)
767
1392
  for (let i = 0; i < extensions.length; i++) {
768
1393
  const extension = extensions[i]
@@ -778,14 +1403,14 @@ const htmx = (function () {
778
1403
  }
779
1404
 
780
1405
  /**
781
- *
782
1406
  * @param {string} oobValue
783
- * @param {HTMLElement} oobElement
784
- * @param {*} settleInfo
1407
+ * @param {Element} oobElement
1408
+ * @param {HtmxSettleInfo} settleInfo
785
1409
  * @returns
786
1410
  */
787
- function oobSwap (oobValue, oobElement, settleInfo) {
1411
+ function oobSwap(oobValue, oobElement, settleInfo) {
788
1412
  let selector = '#' + getRawAttribute(oobElement, 'id')
1413
+ /** @type HtmxSwapStyle */
789
1414
  let swapStyle = 'outerHTML'
790
1415
  if (oobValue === 'true') {
791
1416
  // do nothing
@@ -800,13 +1425,13 @@ const htmx = (function () {
800
1425
  if (targets) {
801
1426
  forEach(
802
1427
  targets,
803
- function (target) {
1428
+ function(target) {
804
1429
  let fragment
805
1430
  const oobElementClone = oobElement.cloneNode(true)
806
1431
  fragment = getDocument().createDocumentFragment()
807
1432
  fragment.appendChild(oobElementClone)
808
1433
  if (!isInlineSwap(swapStyle, target)) {
809
- fragment = oobElementClone // if this is not an inline swap, we use the content of the node, not the node itself
1434
+ fragment = asParentNode(oobElementClone) // if this is not an inline swap, we use the content of the node, not the node itself
810
1435
  }
811
1436
 
812
1437
  const beforeSwapDetails = { shouldSwap: true, target, fragment }
@@ -814,9 +1439,9 @@ const htmx = (function () {
814
1439
 
815
1440
  target = beforeSwapDetails.target // allow re-targeting
816
1441
  if (beforeSwapDetails.shouldSwap) {
817
- swap(swapStyle, target, target, fragment, settleInfo)
1442
+ swapWithStyle(swapStyle, target, target, fragment, settleInfo)
818
1443
  }
819
- forEach(settleInfo.elts, function (elt) {
1444
+ forEach(settleInfo.elts, function(elt) {
820
1445
  triggerEvent(elt, 'htmx:oobAfterSwap', beforeSwapDetails)
821
1446
  })
822
1447
  }
@@ -829,33 +1454,11 @@ const htmx = (function () {
829
1454
  return oobValue
830
1455
  }
831
1456
 
832
- function handleOutOfBandSwaps (elt, fragment, settleInfo) {
833
- const oobSelects = getClosestAttributeValue(elt, 'hx-select-oob')
834
- if (oobSelects) {
835
- const oobSelectValues = oobSelects.split(',')
836
- for (let i = 0; i < oobSelectValues.length; i++) {
837
- const oobSelectValue = oobSelectValues[i].split(':', 2)
838
- let id = oobSelectValue[0].trim()
839
- if (id.indexOf('#') === 0) {
840
- id = id.substring(1)
841
- }
842
- const oobValue = oobSelectValue[1] || 'true'
843
- const oobElement = fragment.querySelector('#' + id)
844
- if (oobElement) {
845
- oobSwap(oobValue, oobElement, settleInfo)
846
- }
847
- }
848
- }
849
- forEach(findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]'), function (oobElement) {
850
- const oobValue = getAttributeValue(oobElement, 'hx-swap-oob')
851
- if (oobValue != null) {
852
- oobSwap(oobValue, oobElement, settleInfo)
853
- }
854
- })
855
- }
856
-
857
- function handlePreservedElements (fragment) {
858
- forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function (preservedElt) {
1457
+ /**
1458
+ * @param {DocumentFragment} fragment
1459
+ */
1460
+ function handlePreservedElements(fragment) {
1461
+ forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function(preservedElt) {
859
1462
  const id = getAttributeValue(preservedElt, 'id')
860
1463
  const oldElt = getDocument().getElementById(id)
861
1464
  if (oldElt != null) {
@@ -864,17 +1467,23 @@ const htmx = (function () {
864
1467
  })
865
1468
  }
866
1469
 
867
- function handleAttributes (parentNode, fragment, settleInfo) {
868
- forEach(fragment.querySelectorAll('[id]'), function (newNode) {
1470
+ /**
1471
+ * @param {Node} parentNode
1472
+ * @param {ParentNode} fragment
1473
+ * @param {HtmxSettleInfo} settleInfo
1474
+ */
1475
+ function handleAttributes(parentNode, fragment, settleInfo) {
1476
+ forEach(fragment.querySelectorAll('[id]'), function(newNode) {
869
1477
  const id = getRawAttribute(newNode, 'id')
870
1478
  if (id && id.length > 0) {
871
1479
  const normalizedId = id.replace("'", "\\'")
872
1480
  const normalizedTag = newNode.tagName.replace(':', '\\:')
873
- const oldNode = parentNode.querySelector(normalizedTag + "[id='" + normalizedId + "']")
874
- if (oldNode && oldNode !== parentNode) {
1481
+ const parentElt = asParentNode(parentNode)
1482
+ const oldNode = parentElt && parentElt.querySelector(normalizedTag + "[id='" + normalizedId + "']")
1483
+ if (oldNode && oldNode !== parentElt) {
875
1484
  const newAttributes = newNode.cloneNode()
876
1485
  cloneAttributes(newNode, oldNode)
877
- settleInfo.tasks.push(function () {
1486
+ settleInfo.tasks.push(function() {
878
1487
  cloneAttributes(newNode, newAttributes)
879
1488
  })
880
1489
  }
@@ -882,29 +1491,41 @@ const htmx = (function () {
882
1491
  })
883
1492
  }
884
1493
 
885
- function makeAjaxLoadTask (child) {
886
- return function () {
1494
+ /**
1495
+ * @param {Node} child
1496
+ * @returns {HtmxSettleTask}
1497
+ */
1498
+ function makeAjaxLoadTask(child) {
1499
+ return function() {
887
1500
  removeClassFromElement(child, htmx.config.addedClass)
888
- processNode(child)
889
- processScripts(child)
890
- processFocus(child)
1501
+ processNode(asElement(child))
1502
+ processFocus(asParentNode(child))
891
1503
  triggerEvent(child, 'htmx:load')
892
1504
  }
893
1505
  }
894
1506
 
895
- function processFocus (child) {
1507
+ /**
1508
+ * @param {ParentNode} child
1509
+ */
1510
+ function processFocus(child) {
896
1511
  const autofocus = '[autofocus]'
897
- const autoFocusedElt = matches(child, autofocus) ? child : child.querySelector(autofocus)
1512
+ const autoFocusedElt = asHtmlElement(matches(child, autofocus) ? child : child.querySelector(autofocus))
898
1513
  if (autoFocusedElt != null) {
899
1514
  autoFocusedElt.focus()
900
1515
  }
901
1516
  }
902
1517
 
903
- function insertNodesBefore (parentNode, insertBefore, fragment, settleInfo) {
1518
+ /**
1519
+ * @param {Node} parentNode
1520
+ * @param {Node} insertBefore
1521
+ * @param {ParentNode} fragment
1522
+ * @param {HtmxSettleInfo} settleInfo
1523
+ */
1524
+ function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
904
1525
  handleAttributes(parentNode, fragment, settleInfo)
905
1526
  while (fragment.childNodes.length > 0) {
906
1527
  const child = fragment.firstChild
907
- addClassToElement(child, htmx.config.addedClass)
1528
+ addClassToElement(asElement(child), htmx.config.addedClass)
908
1529
  parentNode.insertBefore(child, insertBefore)
909
1530
  if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) {
910
1531
  settleInfo.tasks.push(makeAjaxLoadTask(child))
@@ -912,9 +1533,14 @@ const htmx = (function () {
912
1533
  }
913
1534
  }
914
1535
 
915
- // based on https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0,
916
- // derived from Java's string hashcode implementation
917
- function stringHash (string, hash) {
1536
+ /**
1537
+ * based on https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0,
1538
+ * derived from Java's string hashcode implementation
1539
+ * @param {string} string
1540
+ * @param {number} hash
1541
+ * @returns {number}
1542
+ */
1543
+ function stringHash(string, hash) {
918
1544
  let char = 0
919
1545
  while (char < string.length) {
920
1546
  hash = (hash << 5) - hash + string.charCodeAt(char++) | 0 // bitwise or ensures we have a 32-bit int
@@ -922,7 +1548,11 @@ const htmx = (function () {
922
1548
  return hash
923
1549
  }
924
1550
 
925
- function attributeHash (elt) {
1551
+ /**
1552
+ * @param {Element} elt
1553
+ * @returns {number}
1554
+ */
1555
+ function attributeHash(elt) {
926
1556
  let hash = 0
927
1557
  // IE fix
928
1558
  if (elt.attributes) {
@@ -937,87 +1567,135 @@ const htmx = (function () {
937
1567
  return hash
938
1568
  }
939
1569
 
940
- function deInitOnHandlers (elt) {
1570
+ /**
1571
+ * @param {EventTarget} elt
1572
+ */
1573
+ function deInitOnHandlers(elt) {
941
1574
  const internalData = getInternalData(elt)
942
1575
  if (internalData.onHandlers) {
943
1576
  for (let i = 0; i < internalData.onHandlers.length; i++) {
944
1577
  const handlerInfo = internalData.onHandlers[i]
945
- elt.removeEventListener(handlerInfo.event, handlerInfo.listener)
1578
+ removeEventListenerImpl(elt, handlerInfo.event, handlerInfo.listener)
946
1579
  }
947
1580
  delete internalData.onHandlers
948
1581
  }
949
1582
  }
950
1583
 
951
- function deInitNode (element) {
1584
+ /**
1585
+ * @param {Node} element
1586
+ */
1587
+ function deInitNode(element) {
952
1588
  const internalData = getInternalData(element)
953
1589
  if (internalData.timeout) {
954
1590
  clearTimeout(internalData.timeout)
955
1591
  }
956
1592
  if (internalData.listenerInfos) {
957
- forEach(internalData.listenerInfos, function (info) {
1593
+ forEach(internalData.listenerInfos, function(info) {
958
1594
  if (info.on) {
959
- info.on.removeEventListener(info.trigger, info.listener)
1595
+ removeEventListenerImpl(info.on, info.trigger, info.listener)
960
1596
  }
961
1597
  })
962
1598
  }
963
1599
  deInitOnHandlers(element)
964
- forEach(Object.keys(internalData), function (key) { delete internalData[key] })
1600
+ forEach(Object.keys(internalData), function(key) { delete internalData[key] })
965
1601
  }
966
1602
 
967
- function cleanUpElement (element) {
1603
+ /**
1604
+ * @param {Node} element
1605
+ */
1606
+ function cleanUpElement(element) {
968
1607
  triggerEvent(element, 'htmx:beforeCleanupElement')
969
1608
  deInitNode(element)
1609
+ // @ts-ignore IE11 code
1610
+ // noinspection JSUnresolvedReference
970
1611
  if (element.children) { // IE
971
- forEach(element.children, function (child) { cleanUpElement(child) })
1612
+ // @ts-ignore
1613
+ forEach(element.children, function(child) { cleanUpElement(child) })
972
1614
  }
973
1615
  }
974
1616
 
975
- function swapOuterHTML (target, fragment, settleInfo) {
976
- if (target.tagName === 'BODY') {
977
- return swapInnerHTML(target, fragment, settleInfo)
1617
+ /**
1618
+ * @param {Node} target
1619
+ * @param {ParentNode} fragment
1620
+ * @param {HtmxSettleInfo} settleInfo
1621
+ */
1622
+ function swapOuterHTML(target, fragment, settleInfo) {
1623
+ /** @type {Node} */
1624
+ let newElt
1625
+ const eltBeforeNewContent = target.previousSibling
1626
+ insertNodesBefore(parentElt(target), target, fragment, settleInfo)
1627
+ if (eltBeforeNewContent == null) {
1628
+ newElt = parentElt(target).firstChild
978
1629
  } else {
979
- // @type {HTMLElement}
980
- let newElt
981
- const eltBeforeNewContent = target.previousSibling
982
- insertNodesBefore(parentElt(target), target, fragment, settleInfo)
983
- if (eltBeforeNewContent == null) {
984
- newElt = parentElt(target).firstChild
985
- } else {
986
- newElt = eltBeforeNewContent.nextSibling
987
- }
988
- settleInfo.elts = settleInfo.elts.filter(function (e) { return e != target })
989
- while (newElt && newElt !== target) {
990
- if (newElt.nodeType === Node.ELEMENT_NODE) {
991
- settleInfo.elts.push(newElt)
992
- }
1630
+ newElt = eltBeforeNewContent.nextSibling
1631
+ }
1632
+ settleInfo.elts = settleInfo.elts.filter(function(e) { return e !== target })
1633
+ while (newElt && newElt !== target) {
1634
+ if (newElt instanceof Element) {
1635
+ settleInfo.elts.push(newElt)
993
1636
  newElt = newElt.nextElementSibling
1637
+ } else {
1638
+ newElt = null
994
1639
  }
995
- cleanUpElement(target)
996
- parentElt(target).removeChild(target)
1640
+ }
1641
+ cleanUpElement(target)
1642
+ if (target instanceof Element) {
1643
+ target.remove()
1644
+ } else {
1645
+ target.parentNode.removeChild(target)
997
1646
  }
998
1647
  }
999
1648
 
1000
- function swapAfterBegin (target, fragment, settleInfo) {
1649
+ /**
1650
+ * @param {Node} target
1651
+ * @param {ParentNode} fragment
1652
+ * @param {HtmxSettleInfo} settleInfo
1653
+ */
1654
+ function swapAfterBegin(target, fragment, settleInfo) {
1001
1655
  return insertNodesBefore(target, target.firstChild, fragment, settleInfo)
1002
1656
  }
1003
1657
 
1004
- function swapBeforeBegin (target, fragment, settleInfo) {
1658
+ /**
1659
+ * @param {Node} target
1660
+ * @param {ParentNode} fragment
1661
+ * @param {HtmxSettleInfo} settleInfo
1662
+ */
1663
+ function swapBeforeBegin(target, fragment, settleInfo) {
1005
1664
  return insertNodesBefore(parentElt(target), target, fragment, settleInfo)
1006
1665
  }
1007
1666
 
1008
- function swapBeforeEnd (target, fragment, settleInfo) {
1667
+ /**
1668
+ * @param {Node} target
1669
+ * @param {ParentNode} fragment
1670
+ * @param {HtmxSettleInfo} settleInfo
1671
+ */
1672
+ function swapBeforeEnd(target, fragment, settleInfo) {
1009
1673
  return insertNodesBefore(target, null, fragment, settleInfo)
1010
1674
  }
1011
1675
 
1012
- function swapAfterEnd (target, fragment, settleInfo) {
1676
+ /**
1677
+ * @param {Node} target
1678
+ * @param {ParentNode} fragment
1679
+ * @param {HtmxSettleInfo} settleInfo
1680
+ */
1681
+ function swapAfterEnd(target, fragment, settleInfo) {
1013
1682
  return insertNodesBefore(parentElt(target), target.nextSibling, fragment, settleInfo)
1014
1683
  }
1015
- function swapDelete (target, fragment, settleInfo) {
1684
+
1685
+ /**
1686
+ * @param {Node} target
1687
+ */
1688
+ function swapDelete(target) {
1016
1689
  cleanUpElement(target)
1017
1690
  return parentElt(target).removeChild(target)
1018
1691
  }
1019
1692
 
1020
- function swapInnerHTML (target, fragment, settleInfo) {
1693
+ /**
1694
+ * @param {Node} target
1695
+ * @param {ParentNode} fragment
1696
+ * @param {HtmxSettleInfo} settleInfo
1697
+ */
1698
+ function swapInnerHTML(target, fragment, settleInfo) {
1021
1699
  const firstChild = target.firstChild
1022
1700
  insertNodesBefore(target, firstChild, fragment, settleInfo)
1023
1701
  if (firstChild) {
@@ -1030,19 +1708,14 @@ const htmx = (function () {
1030
1708
  }
1031
1709
  }
1032
1710
 
1033
- function maybeSelectFromResponse (elt, fragment, selectOverride) {
1034
- const selector = selectOverride || getClosestAttributeValue(elt, 'hx-select')
1035
- if (selector) {
1036
- const newFragment = getDocument().createDocumentFragment()
1037
- forEach(fragment.querySelectorAll(selector), function (node) {
1038
- newFragment.appendChild(node)
1039
- })
1040
- fragment = newFragment
1041
- }
1042
- return fragment
1043
- }
1044
-
1045
- function swap (swapStyle, elt, target, fragment, settleInfo) {
1711
+ /**
1712
+ * @param {HtmxSwapStyle} swapStyle
1713
+ * @param {Element} elt
1714
+ * @param {Node} target
1715
+ * @param {ParentNode} fragment
1716
+ * @param {HtmxSettleInfo} settleInfo
1717
+ */
1718
+ function swapWithStyle(swapStyle, elt, target, fragment, settleInfo) {
1046
1719
  switch (swapStyle) {
1047
1720
  case 'none':
1048
1721
  return
@@ -1062,7 +1735,7 @@ const htmx = (function () {
1062
1735
  swapAfterEnd(target, fragment, settleInfo)
1063
1736
  return
1064
1737
  case 'delete':
1065
- swapDelete(target, fragment, settleInfo)
1738
+ swapDelete(target)
1066
1739
  return
1067
1740
  default:
1068
1741
  var extensions = getExtensions(elt)
@@ -1089,33 +1762,181 @@ const htmx = (function () {
1089
1762
  if (swapStyle === 'innerHTML') {
1090
1763
  swapInnerHTML(target, fragment, settleInfo)
1091
1764
  } else {
1092
- swap(htmx.config.defaultSwapStyle, elt, target, fragment, settleInfo)
1765
+ swapWithStyle(htmx.config.defaultSwapStyle, elt, target, fragment, settleInfo)
1093
1766
  }
1094
1767
  }
1095
1768
  }
1096
1769
 
1097
- function findTitle (content) {
1098
- if (content.indexOf('<title') > -1) {
1099
- const contentWithSvgsRemoved = content.replace(SVG_TAGS_REGEX, '')
1100
- const result = contentWithSvgsRemoved.match(TITLE_TAG_REGEX)
1101
- if (result) {
1102
- return result[2]
1770
+ /**
1771
+ * @param {DocumentFragment} fragment
1772
+ * @param {HtmxSettleInfo} settleInfo
1773
+ */
1774
+ function findAndSwapOobElements(fragment, settleInfo) {
1775
+ forEach(findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]'), function(oobElement) {
1776
+ if (htmx.config.allowNestedOobSwaps || oobElement.parentElement === null) {
1777
+ const oobValue = getAttributeValue(oobElement, 'hx-swap-oob')
1778
+ if (oobValue != null) {
1779
+ oobSwap(oobValue, oobElement, settleInfo)
1780
+ }
1781
+ } else {
1782
+ oobElement.removeAttribute('hx-swap-oob')
1783
+ oobElement.removeAttribute('data-hx-swap-oob')
1784
+ }
1785
+ })
1786
+ }
1787
+
1788
+ /**
1789
+ * Implements complete swapping pipeline, including: focus and selection preservation,
1790
+ * title updates, scroll, OOB swapping, normal swapping and settling
1791
+ * @param {string|Element} target
1792
+ * @param {string} content
1793
+ * @param {HtmxSwapSpecification} swapSpec
1794
+ * @param {SwapOptions} [swapOptions]
1795
+ */
1796
+ function swap(target, content, swapSpec, swapOptions) {
1797
+ if (!swapOptions) {
1798
+ swapOptions = {}
1799
+ }
1800
+
1801
+ target = resolveTarget(target)
1802
+
1803
+ // preserve focus and selection
1804
+ const activeElt = document.activeElement
1805
+ let selectionInfo = {}
1806
+ try {
1807
+ selectionInfo = {
1808
+ elt: activeElt,
1809
+ // @ts-ignore
1810
+ start: activeElt ? activeElt.selectionStart : null,
1811
+ // @ts-ignore
1812
+ end: activeElt ? activeElt.selectionEnd : null
1103
1813
  }
1814
+ } catch (e) {
1815
+ // safari issue - see https://github.com/microsoft/playwright/issues/5894
1104
1816
  }
1105
- }
1817
+ const settleInfo = makeSettleInfo(target)
1106
1818
 
1107
- function selectAndSwap (swapStyle, target, elt, responseText, settleInfo, selectOverride) {
1108
- settleInfo.title = findTitle(responseText)
1109
- let fragment = makeFragment(responseText)
1110
- if (fragment) {
1111
- handleOutOfBandSwaps(elt, fragment, settleInfo)
1112
- fragment = maybeSelectFromResponse(elt, fragment, selectOverride)
1819
+ // For text content swaps, don't parse the response as HTML, just insert it
1820
+ if (swapSpec.swapStyle === 'textContent') {
1821
+ target.textContent = content
1822
+ // Otherwise, make the fragment and process it
1823
+ } else {
1824
+ let fragment = makeFragment(content)
1825
+
1826
+ settleInfo.title = fragment.title
1827
+
1828
+ // select-oob swaps
1829
+ if (swapOptions.selectOOB) {
1830
+ const oobSelectValues = swapOptions.selectOOB.split(',')
1831
+ for (let i = 0; i < oobSelectValues.length; i++) {
1832
+ const oobSelectValue = oobSelectValues[i].split(':', 2)
1833
+ let id = oobSelectValue[0].trim()
1834
+ if (id.indexOf('#') === 0) {
1835
+ id = id.substring(1)
1836
+ }
1837
+ const oobValue = oobSelectValue[1] || 'true'
1838
+ const oobElement = fragment.querySelector('#' + id)
1839
+ if (oobElement) {
1840
+ oobSwap(oobValue, oobElement, settleInfo)
1841
+ }
1842
+ }
1843
+ }
1844
+ // oob swaps
1845
+ findAndSwapOobElements(fragment, settleInfo)
1846
+ forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) {
1847
+ findAndSwapOobElements(template.content, settleInfo)
1848
+ if (template.content.childElementCount === 0) {
1849
+ // Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
1850
+ template.remove()
1851
+ }
1852
+ })
1853
+
1854
+ // normal swap
1855
+ if (swapOptions.select) {
1856
+ const newFragment = getDocument().createDocumentFragment()
1857
+ forEach(fragment.querySelectorAll(swapOptions.select), function(node) {
1858
+ newFragment.appendChild(node)
1859
+ })
1860
+ fragment = newFragment
1861
+ }
1113
1862
  handlePreservedElements(fragment)
1114
- return swap(swapStyle, elt, target, fragment, settleInfo)
1863
+ swapWithStyle(swapSpec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo)
1864
+ }
1865
+
1866
+ // apply saved focus and selection information to swapped content
1867
+ if (selectionInfo.elt &&
1868
+ !bodyContains(selectionInfo.elt) &&
1869
+ getRawAttribute(selectionInfo.elt, 'id')) {
1870
+ const newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, 'id'))
1871
+ const focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }
1872
+ if (newActiveElt) {
1873
+ // @ts-ignore
1874
+ if (selectionInfo.start && newActiveElt.setSelectionRange) {
1875
+ try {
1876
+ // @ts-ignore
1877
+ newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end)
1878
+ } catch (e) {
1879
+ // the setSelectionRange method is present on fields that don't support it, so just let this fail
1880
+ }
1881
+ }
1882
+ newActiveElt.focus(focusOptions)
1883
+ }
1884
+ }
1885
+
1886
+ target.classList.remove(htmx.config.swappingClass)
1887
+ forEach(settleInfo.elts, function(elt) {
1888
+ if (elt.classList) {
1889
+ elt.classList.add(htmx.config.settlingClass)
1890
+ }
1891
+ triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo)
1892
+ })
1893
+ if (swapOptions.afterSwapCallback) {
1894
+ swapOptions.afterSwapCallback()
1895
+ }
1896
+
1897
+ // merge in new title after swap but before settle
1898
+ if (!swapSpec.ignoreTitle) {
1899
+ handleTitle(settleInfo.title)
1900
+ }
1901
+
1902
+ // settle
1903
+ const doSettle = function() {
1904
+ forEach(settleInfo.tasks, function(task) {
1905
+ task.call()
1906
+ })
1907
+ forEach(settleInfo.elts, function(elt) {
1908
+ if (elt.classList) {
1909
+ elt.classList.remove(htmx.config.settlingClass)
1910
+ }
1911
+ triggerEvent(elt, 'htmx:afterSettle', swapOptions.eventInfo)
1912
+ })
1913
+
1914
+ if (swapOptions.anchor) {
1915
+ const anchorTarget = asElement(resolveTarget('#' + swapOptions.anchor))
1916
+ if (anchorTarget) {
1917
+ anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
1918
+ }
1919
+ }
1920
+
1921
+ updateScrollState(settleInfo.elts, swapSpec)
1922
+ if (swapOptions.afterSettleCallback) {
1923
+ swapOptions.afterSettleCallback()
1924
+ }
1925
+ }
1926
+
1927
+ if (swapSpec.settleDelay > 0) {
1928
+ getWindow().setTimeout(doSettle, swapSpec.settleDelay)
1929
+ } else {
1930
+ doSettle()
1115
1931
  }
1116
1932
  }
1117
1933
 
1118
- function handleTrigger (xhr, header, elt) {
1934
+ /**
1935
+ * @param {XMLHttpRequest} xhr
1936
+ * @param {string} header
1937
+ * @param {EventTarget} elt
1938
+ */
1939
+ function handleTriggerHeader(xhr, header, elt) {
1119
1940
  const triggerBody = xhr.getResponseHeader(header)
1120
1941
  if (triggerBody.indexOf('{') === 0) {
1121
1942
  const triggers = parseJSON(triggerBody)
@@ -1144,7 +1965,13 @@ const htmx = (function () {
1144
1965
  const NOT_WHITESPACE = /[^\s]/
1145
1966
  const COMBINED_SELECTOR_START = /[{(]/
1146
1967
  const COMBINED_SELECTOR_END = /[})]/
1147
- function tokenizeString (str) {
1968
+
1969
+ /**
1970
+ * @param {string} str
1971
+ * @returns {string[]}
1972
+ */
1973
+ function tokenizeString(str) {
1974
+ /** @type string[] */
1148
1975
  const tokens = []
1149
1976
  let position = 0
1150
1977
  while (position < str.length) {
@@ -1174,7 +2001,13 @@ const htmx = (function () {
1174
2001
  return tokens
1175
2002
  }
1176
2003
 
1177
- function isPossibleRelativeReference (token, last, paramName) {
2004
+ /**
2005
+ * @param {string} token
2006
+ * @param {string|null} last
2007
+ * @param {string} paramName
2008
+ * @returns {boolean}
2009
+ */
2010
+ function isPossibleRelativeReference(token, last, paramName) {
1178
2011
  return SYMBOL_START.exec(token.charAt(0)) &&
1179
2012
  token !== 'true' &&
1180
2013
  token !== 'false' &&
@@ -1183,7 +2016,13 @@ const htmx = (function () {
1183
2016
  last !== '.'
1184
2017
  }
1185
2018
 
1186
- function maybeGenerateConditional (elt, tokens, paramName) {
2019
+ /**
2020
+ * @param {EventTarget|string} elt
2021
+ * @param {string[]} tokens
2022
+ * @param {string} paramName
2023
+ * @returns {ConditionalFunction|null}
2024
+ */
2025
+ function maybeGenerateConditional(elt, tokens, paramName) {
1187
2026
  if (tokens[0] === '[') {
1188
2027
  tokens.shift()
1189
2028
  let bracketCount = 1
@@ -1191,6 +2030,7 @@ const htmx = (function () {
1191
2030
  let last = null
1192
2031
  while (tokens.length > 0) {
1193
2032
  const token = tokens[0]
2033
+ // @ts-ignore For some reason tsc doesn't understand the shift call, and thinks we're comparing the same value here, i.e. '[' vs ']'
1194
2034
  if (token === ']') {
1195
2035
  bracketCount--
1196
2036
  if (bracketCount === 0) {
@@ -1200,10 +2040,10 @@ const htmx = (function () {
1200
2040
  tokens.shift()
1201
2041
  conditionalSource += ')})'
1202
2042
  try {
1203
- const conditionFunction = maybeEval(elt, function () {
2043
+ const conditionFunction = maybeEval(elt, function() {
1204
2044
  return Function(conditionalSource)()
1205
2045
  },
1206
- function () { return true })
2046
+ function() { return true })
1207
2047
  conditionFunction.source = conditionalSource
1208
2048
  return conditionFunction
1209
2049
  } catch (e) {
@@ -1224,7 +2064,12 @@ const htmx = (function () {
1224
2064
  }
1225
2065
  }
1226
2066
 
1227
- function consumeUntil (tokens, match) {
2067
+ /**
2068
+ * @param {string[]} tokens
2069
+ * @param {RegExp} match
2070
+ * @returns {string}
2071
+ */
2072
+ function consumeUntil(tokens, match) {
1228
2073
  let result = ''
1229
2074
  while (tokens.length > 0 && !match.test(tokens[0])) {
1230
2075
  result += tokens.shift()
@@ -1232,7 +2077,11 @@ const htmx = (function () {
1232
2077
  return result
1233
2078
  }
1234
2079
 
1235
- function consumeCSSSelector (tokens) {
2080
+ /**
2081
+ * @param {string[]} tokens
2082
+ * @returns {string}
2083
+ */
2084
+ function consumeCSSSelector(tokens) {
1236
2085
  let result
1237
2086
  if (tokens.length > 0 && COMBINED_SELECTOR_START.test(tokens[0])) {
1238
2087
  tokens.shift()
@@ -1247,12 +2096,13 @@ const htmx = (function () {
1247
2096
  const INPUT_SELECTOR = 'input, textarea, select'
1248
2097
 
1249
2098
  /**
1250
- * @param {HTMLElement} elt
2099
+ * @param {Element} elt
1251
2100
  * @param {string} explicitTrigger
1252
- * @param {cache} cache for trigger specs
1253
- * @returns {import("./htmx").HtmxTriggerSpecification[]}
2101
+ * @param {Object} cache for trigger specs
2102
+ * @returns {HtmxTriggerSpecification[]}
1254
2103
  */
1255
- function parseAndCacheTrigger (elt, explicitTrigger, cache) {
2104
+ function parseAndCacheTrigger(elt, explicitTrigger, cache) {
2105
+ /** @type HtmxTriggerSpecification[] */
1256
2106
  const triggerSpecs = []
1257
2107
  const tokens = tokenizeString(explicitTrigger)
1258
2108
  do {
@@ -1261,6 +2111,7 @@ const htmx = (function () {
1261
2111
  const trigger = consumeUntil(tokens, /[,\[\s]/)
1262
2112
  if (trigger !== '') {
1263
2113
  if (trigger === 'every') {
2114
+ /** @type HtmxTriggerSpecification */
1264
2115
  const every = { trigger: 'every' }
1265
2116
  consumeUntil(tokens, NOT_WHITESPACE)
1266
2117
  every.pollInterval = parseInterval(consumeUntil(tokens, /[,\[\s]/))
@@ -1271,6 +2122,7 @@ const htmx = (function () {
1271
2122
  }
1272
2123
  triggerSpecs.push(every)
1273
2124
  } else {
2125
+ /** @type HtmxTriggerSpecification */
1274
2126
  const triggerSpec = { trigger }
1275
2127
  var eventFilter = maybeGenerateConditional(elt, tokens, 'event')
1276
2128
  if (eventFilter) {
@@ -1338,10 +2190,10 @@ const htmx = (function () {
1338
2190
  }
1339
2191
 
1340
2192
  /**
1341
- * @param {HTMLElement} elt
1342
- * @returns {import("./htmx").HtmxTriggerSpecification[]}
2193
+ * @param {Element} elt
2194
+ * @returns {HtmxTriggerSpecification[]}
1343
2195
  */
1344
- function getTriggerSpecs (elt) {
2196
+ function getTriggerSpecs(elt) {
1345
2197
  const explicitTrigger = getAttributeValue(elt, 'hx-trigger')
1346
2198
  let triggerSpecs = []
1347
2199
  if (explicitTrigger) {
@@ -1362,13 +2214,21 @@ const htmx = (function () {
1362
2214
  }
1363
2215
  }
1364
2216
 
1365
- function cancelPolling (elt) {
2217
+ /**
2218
+ * @param {Element} elt
2219
+ */
2220
+ function cancelPolling(elt) {
1366
2221
  getInternalData(elt).cancelled = true
1367
2222
  }
1368
2223
 
1369
- function processPolling (elt, handler, spec) {
2224
+ /**
2225
+ * @param {Element} elt
2226
+ * @param {TriggerHandler} handler
2227
+ * @param {HtmxTriggerSpecification} spec
2228
+ */
2229
+ function processPolling(elt, handler, spec) {
1370
2230
  const nodeData = getInternalData(elt)
1371
- nodeData.timeout = setTimeout(function () {
2231
+ nodeData.timeout = getWindow().setTimeout(function() {
1372
2232
  if (bodyContains(elt) && nodeData.cancelled !== true) {
1373
2233
  if (!maybeFilterEvent(spec, elt, makeEvent('hx:poll:trigger', {
1374
2234
  triggerSpec: spec,
@@ -1381,14 +2241,23 @@ const htmx = (function () {
1381
2241
  }, spec.pollInterval)
1382
2242
  }
1383
2243
 
1384
- function isLocalLink (elt) {
2244
+ /**
2245
+ * @param {HTMLAnchorElement} elt
2246
+ * @returns {boolean}
2247
+ */
2248
+ function isLocalLink(elt) {
1385
2249
  return location.hostname === elt.hostname &&
1386
2250
  getRawAttribute(elt, 'href') &&
1387
2251
  getRawAttribute(elt, 'href').indexOf('#') !== 0
1388
2252
  }
1389
2253
 
1390
- function boostElement (elt, nodeData, triggerSpecs) {
1391
- if ((elt.tagName === 'A' && isLocalLink(elt) && (elt.target === '' || elt.target === '_self')) || elt.tagName === 'FORM') {
2254
+ /**
2255
+ * @param {Element} elt
2256
+ * @param {HtmxNodeInternalData} nodeData
2257
+ * @param {HtmxTriggerSpecification[]} triggerSpecs
2258
+ */
2259
+ function boostElement(elt, nodeData, triggerSpecs) {
2260
+ if ((elt instanceof HTMLAnchorElement && isLocalLink(elt) && (elt.target === '' || elt.target === '_self')) || elt.tagName === 'FORM') {
1392
2261
  nodeData.boosted = true
1393
2262
  let verb, path
1394
2263
  if (elt.tagName === 'A') {
@@ -1401,8 +2270,9 @@ const htmx = (function () {
1401
2270
  }
1402
2271
  path = getRawAttribute(elt, 'action')
1403
2272
  }
1404
- triggerSpecs.forEach(function (triggerSpec) {
1405
- addEventListener(elt, function (elt, evt) {
2273
+ triggerSpecs.forEach(function(triggerSpec) {
2274
+ addEventListener(elt, function(node, evt) {
2275
+ const elt = asElement(node)
1406
2276
  if (closest(elt, htmx.config.disableSelector)) {
1407
2277
  cleanUpElement(elt)
1408
2278
  return
@@ -1414,12 +2284,15 @@ const htmx = (function () {
1414
2284
  }
1415
2285
 
1416
2286
  /**
1417
- *
1418
2287
  * @param {Event} evt
1419
- * @param {HTMLElement} elt
1420
- * @returns
2288
+ * @param {Node} node
2289
+ * @returns {boolean}
1421
2290
  */
1422
- function shouldCancel (evt, elt) {
2291
+ function shouldCancel(evt, node) {
2292
+ const elt = asElement(node)
2293
+ if (!elt) {
2294
+ return false
2295
+ }
1423
2296
  if (evt.type === 'submit' || evt.type === 'click') {
1424
2297
  if (elt.tagName === 'FORM') {
1425
2298
  return true
@@ -1427,7 +2300,7 @@ const htmx = (function () {
1427
2300
  if (matches(elt, 'input[type="submit"], button') && closest(elt, 'form') !== null) {
1428
2301
  return true
1429
2302
  }
1430
- if (elt.tagName === 'A' && elt.href &&
2303
+ if (elt instanceof HTMLAnchorElement && elt.href &&
1431
2304
  (elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf('#') !== 0)) {
1432
2305
  return true
1433
2306
  }
@@ -1435,25 +2308,47 @@ const htmx = (function () {
1435
2308
  return false
1436
2309
  }
1437
2310
 
1438
- function ignoreBoostedAnchorCtrlClick (elt, evt) {
1439
- return getInternalData(elt).boosted && elt.tagName === 'A' && evt.type === 'click' && (evt.ctrlKey || evt.metaKey)
2311
+ /**
2312
+ * @param {Node} elt
2313
+ * @param {Event|MouseEvent|KeyboardEvent|TouchEvent} evt
2314
+ * @returns {boolean}
2315
+ */
2316
+ function ignoreBoostedAnchorCtrlClick(elt, evt) {
2317
+ return getInternalData(elt).boosted && elt instanceof HTMLAnchorElement && evt.type === 'click' &&
2318
+ // @ts-ignore this will resolve to undefined for events that don't define those properties, which is fine
2319
+ (evt.ctrlKey || evt.metaKey)
1440
2320
  }
1441
2321
 
1442
- function maybeFilterEvent (triggerSpec, elt, evt) {
2322
+ /**
2323
+ * @param {HtmxTriggerSpecification} triggerSpec
2324
+ * @param {Node} elt
2325
+ * @param {Event} evt
2326
+ * @returns {boolean}
2327
+ */
2328
+ function maybeFilterEvent(triggerSpec, elt, evt) {
1443
2329
  const eventFilter = triggerSpec.eventFilter
1444
2330
  if (eventFilter) {
1445
2331
  try {
1446
2332
  return eventFilter.call(elt, evt) !== true
1447
2333
  } catch (e) {
1448
- triggerErrorEvent(getDocument().body, 'htmx:eventFilter:error', { error: e, source: eventFilter.source })
2334
+ const source = eventFilter.source
2335
+ triggerErrorEvent(getDocument().body, 'htmx:eventFilter:error', { error: e, source })
1449
2336
  return true
1450
2337
  }
1451
2338
  }
1452
2339
  return false
1453
2340
  }
1454
2341
 
1455
- function addEventListener (elt, handler, nodeData, triggerSpec, explicitCancel) {
2342
+ /**
2343
+ * @param {Node} elt
2344
+ * @param {TriggerHandler} handler
2345
+ * @param {HtmxNodeInternalData} nodeData
2346
+ * @param {HtmxTriggerSpecification} triggerSpec
2347
+ * @param {boolean} [explicitCancel]
2348
+ */
2349
+ function addEventListener(elt, handler, nodeData, triggerSpec, explicitCancel) {
1456
2350
  const elementData = getInternalData(elt)
2351
+ /** @type {(Node|Window)[]} */
1457
2352
  let eltsToListenOn
1458
2353
  if (triggerSpec.from) {
1459
2354
  eltsToListenOn = querySelectorAllExt(elt, triggerSpec.from)
@@ -1462,13 +2357,15 @@ const htmx = (function () {
1462
2357
  }
1463
2358
  // store the initial values of the elements, so we can tell if they change
1464
2359
  if (triggerSpec.changed) {
1465
- eltsToListenOn.forEach(function (eltToListenOn) {
2360
+ eltsToListenOn.forEach(function(eltToListenOn) {
1466
2361
  const eltToListenOnData = getInternalData(eltToListenOn)
2362
+ // @ts-ignore value will be undefined for non-input elements, which is fine
1467
2363
  eltToListenOnData.lastValue = eltToListenOn.value
1468
2364
  })
1469
2365
  }
1470
- forEach(eltsToListenOn, function (eltToListenOn) {
1471
- const eventListener = function (evt) {
2366
+ forEach(eltsToListenOn, function(eltToListenOn) {
2367
+ /** @type EventListener */
2368
+ const eventListener = function(evt) {
1472
2369
  if (!bodyContains(elt)) {
1473
2370
  eltToListenOn.removeEventListener(triggerSpec.trigger, eventListener)
1474
2371
  return
@@ -1493,7 +2390,7 @@ const htmx = (function () {
1493
2390
  evt.stopPropagation()
1494
2391
  }
1495
2392
  if (triggerSpec.target && evt.target) {
1496
- if (!matches(evt.target, triggerSpec.target)) {
2393
+ if (!matches(asElement(evt.target), triggerSpec.target)) {
1497
2394
  return
1498
2395
  }
1499
2396
  }
@@ -1506,10 +2403,12 @@ const htmx = (function () {
1506
2403
  }
1507
2404
  if (triggerSpec.changed) {
1508
2405
  const eltToListenOnData = getInternalData(eltToListenOn)
1509
- if (eltToListenOnData.lastValue === eltToListenOn.value) {
2406
+ // @ts-ignore value will be undefined for non-input elements, which is fine
2407
+ const value = eltToListenOn.value
2408
+ if (eltToListenOnData.lastValue === value) {
1510
2409
  return
1511
2410
  }
1512
- eltToListenOnData.lastValue = eltToListenOn.value
2411
+ eltToListenOnData.lastValue = value
1513
2412
  }
1514
2413
  if (elementData.delayed) {
1515
2414
  clearTimeout(elementData.delayed)
@@ -1521,12 +2420,12 @@ const htmx = (function () {
1521
2420
  if (triggerSpec.throttle > 0) {
1522
2421
  if (!elementData.throttle) {
1523
2422
  handler(elt, evt)
1524
- elementData.throttle = setTimeout(function () {
2423
+ elementData.throttle = getWindow().setTimeout(function() {
1525
2424
  elementData.throttle = null
1526
2425
  }, triggerSpec.throttle)
1527
2426
  }
1528
2427
  } else if (triggerSpec.delay > 0) {
1529
- elementData.delayed = setTimeout(function () { handler(elt, evt) }, triggerSpec.delay)
2428
+ elementData.delayed = getWindow().setTimeout(function() { handler(elt, evt) }, triggerSpec.delay)
1530
2429
  } else {
1531
2430
  triggerEvent(elt, 'htmx:trigger')
1532
2431
  handler(elt, evt)
@@ -1547,16 +2446,16 @@ const htmx = (function () {
1547
2446
 
1548
2447
  let windowIsScrolling = false // used by initScrollHandler
1549
2448
  let scrollHandler = null
1550
- function initScrollHandler () {
2449
+ function initScrollHandler() {
1551
2450
  if (!scrollHandler) {
1552
- scrollHandler = function () {
2451
+ scrollHandler = function() {
1553
2452
  windowIsScrolling = true
1554
2453
  }
1555
2454
  window.addEventListener('scroll', scrollHandler)
1556
- setInterval(function () {
2455
+ setInterval(function() {
1557
2456
  if (windowIsScrolling) {
1558
2457
  windowIsScrolling = false
1559
- forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
2458
+ forEach(getDocument().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"), function(elt) {
1560
2459
  maybeReveal(elt)
1561
2460
  })
1562
2461
  }
@@ -1564,7 +2463,10 @@ const htmx = (function () {
1564
2463
  }
1565
2464
  }
1566
2465
 
1567
- function maybeReveal (elt) {
2466
+ /**
2467
+ * @param {Element} elt
2468
+ */
2469
+ function maybeReveal(elt) {
1568
2470
  if (!hasAttribute(elt, 'data-hx-revealed') && isScrolledIntoView(elt)) {
1569
2471
  elt.setAttribute('data-hx-revealed', 'true')
1570
2472
  const nodeData = getInternalData(elt)
@@ -1572,37 +2474,50 @@ const htmx = (function () {
1572
2474
  triggerEvent(elt, 'revealed')
1573
2475
  } else {
1574
2476
  // if the node isn't initialized, wait for it before triggering the request
1575
- elt.addEventListener('htmx:afterProcessNode', function (evt) { triggerEvent(elt, 'revealed') }, { once: true })
2477
+ elt.addEventListener('htmx:afterProcessNode', function() { triggerEvent(elt, 'revealed') }, { once: true })
1576
2478
  }
1577
2479
  }
1578
2480
  }
1579
2481
 
1580
2482
  //= ===================================================================
1581
2483
 
1582
- function loadImmediately (elt, handler, nodeData, delay) {
1583
- const load = function () {
2484
+ /**
2485
+ * @param {Element} elt
2486
+ * @param {TriggerHandler} handler
2487
+ * @param {HtmxNodeInternalData} nodeData
2488
+ * @param {number} delay
2489
+ */
2490
+ function loadImmediately(elt, handler, nodeData, delay) {
2491
+ const load = function() {
1584
2492
  if (!nodeData.loaded) {
1585
2493
  nodeData.loaded = true
1586
2494
  handler(elt)
1587
2495
  }
1588
2496
  }
1589
2497
  if (delay > 0) {
1590
- setTimeout(load, delay)
2498
+ getWindow().setTimeout(load, delay)
1591
2499
  } else {
1592
2500
  load()
1593
2501
  }
1594
2502
  }
1595
2503
 
1596
- function processVerbs (elt, nodeData, triggerSpecs) {
2504
+ /**
2505
+ * @param {Element} elt
2506
+ * @param {HtmxNodeInternalData} nodeData
2507
+ * @param {HtmxTriggerSpecification[]} triggerSpecs
2508
+ * @returns {boolean}
2509
+ */
2510
+ function processVerbs(elt, nodeData, triggerSpecs) {
1597
2511
  let explicitAction = false
1598
- forEach(VERBS, function (verb) {
2512
+ forEach(VERBS, function(verb) {
1599
2513
  if (hasAttribute(elt, 'hx-' + verb)) {
1600
2514
  const path = getAttributeValue(elt, 'hx-' + verb)
1601
2515
  explicitAction = true
1602
2516
  nodeData.path = path
1603
2517
  nodeData.verb = verb
1604
- triggerSpecs.forEach(function (triggerSpec) {
1605
- addTriggerHandler(elt, triggerSpec, nodeData, function (elt, evt) {
2518
+ triggerSpecs.forEach(function(triggerSpec) {
2519
+ addTriggerHandler(elt, triggerSpec, nodeData, function(node, evt) {
2520
+ const elt = asElement(node)
1606
2521
  if (closest(elt, htmx.config.disableSelector)) {
1607
2522
  cleanUpElement(elt)
1608
2523
  return
@@ -1615,11 +2530,23 @@ const htmx = (function () {
1615
2530
  return explicitAction
1616
2531
  }
1617
2532
 
1618
- function addTriggerHandler (elt, triggerSpec, nodeData, handler) {
2533
+ /**
2534
+ * @callback TriggerHandler
2535
+ * @param {Node} elt
2536
+ * @param {Event} [evt]
2537
+ */
2538
+
2539
+ /**
2540
+ * @param {Node} elt
2541
+ * @param {HtmxTriggerSpecification} triggerSpec
2542
+ * @param {HtmxNodeInternalData} nodeData
2543
+ * @param {TriggerHandler} handler
2544
+ */
2545
+ function addTriggerHandler(elt, triggerSpec, nodeData, handler) {
1619
2546
  if (triggerSpec.trigger === 'revealed') {
1620
2547
  initScrollHandler()
1621
2548
  addEventListener(elt, handler, nodeData, triggerSpec)
1622
- maybeReveal(elt)
2549
+ maybeReveal(asElement(elt))
1623
2550
  } else if (triggerSpec.trigger === 'intersect') {
1624
2551
  const observerOptions = {}
1625
2552
  if (triggerSpec.root) {
@@ -1628,7 +2555,7 @@ const htmx = (function () {
1628
2555
  if (triggerSpec.threshold) {
1629
2556
  observerOptions.threshold = parseFloat(triggerSpec.threshold)
1630
2557
  }
1631
- const observer = new IntersectionObserver(function (entries) {
2558
+ const observer = new IntersectionObserver(function(entries) {
1632
2559
  for (let i = 0; i < entries.length; i++) {
1633
2560
  const entry = entries[i]
1634
2561
  if (entry.isIntersecting) {
@@ -1637,56 +2564,29 @@ const htmx = (function () {
1637
2564
  }
1638
2565
  }
1639
2566
  }, observerOptions)
1640
- observer.observe(elt)
1641
- addEventListener(elt, handler, nodeData, triggerSpec)
2567
+ observer.observe(asElement(elt))
2568
+ addEventListener(asElement(elt), handler, nodeData, triggerSpec)
1642
2569
  } else if (triggerSpec.trigger === 'load') {
1643
2570
  if (!maybeFilterEvent(triggerSpec, elt, makeEvent('load', { elt }))) {
1644
- loadImmediately(elt, handler, nodeData, triggerSpec.delay)
2571
+ loadImmediately(asElement(elt), handler, nodeData, triggerSpec.delay)
1645
2572
  }
1646
2573
  } else if (triggerSpec.pollInterval > 0) {
1647
2574
  nodeData.polling = true
1648
- processPolling(elt, handler, triggerSpec)
2575
+ processPolling(asElement(elt), handler, triggerSpec)
1649
2576
  } else {
1650
2577
  addEventListener(elt, handler, nodeData, triggerSpec)
1651
2578
  }
1652
2579
  }
1653
2580
 
1654
- function evalScript (script) {
1655
- if (htmx.config.allowScriptTags && (script.type === 'text/javascript' || script.type === 'module' || script.type === '')) {
1656
- const newScript = getDocument().createElement('script')
1657
- forEach(script.attributes, function (attr) {
1658
- newScript.setAttribute(attr.name, attr.value)
1659
- })
1660
- newScript.textContent = script.textContent
1661
- newScript.async = false
1662
- if (htmx.config.inlineScriptNonce) {
1663
- newScript.nonce = htmx.config.inlineScriptNonce
1664
- }
1665
- const parent = script.parentElement
1666
-
1667
- try {
1668
- parent.insertBefore(newScript, script)
1669
- } catch (e) {
1670
- logError(e)
1671
- } finally {
1672
- // remove old script element, but only if it is still in DOM
1673
- if (script.parentElement) {
1674
- script.parentElement.removeChild(script)
1675
- }
1676
- }
1677
- }
1678
- }
1679
-
1680
- function processScripts (elt) {
1681
- if (matches(elt, 'script')) {
1682
- evalScript(elt)
2581
+ /**
2582
+ * @param {Node} node
2583
+ * @returns {boolean}
2584
+ */
2585
+ function shouldProcessHxOn(node) {
2586
+ const elt = asElement(node)
2587
+ if (!elt) {
2588
+ return false
1683
2589
  }
1684
- forEach(findAll(elt, 'script'), function (script) {
1685
- evalScript(script)
1686
- })
1687
- }
1688
-
1689
- function shouldProcessHxOn (elt) {
1690
2590
  const attributes = elt.attributes
1691
2591
  for (let j = 0; j < attributes.length; j++) {
1692
2592
  const attrName = attributes[j].name
@@ -1698,23 +2598,32 @@ const htmx = (function () {
1698
2598
  return false
1699
2599
  }
1700
2600
 
1701
- function findHxOnWildcardElements (elt) {
2601
+ /**
2602
+ * @param {Node} elt
2603
+ * @returns {Element[]}
2604
+ */
2605
+ function findHxOnWildcardElements(elt) {
1702
2606
  let node = null
2607
+ /** @type {Element[]} */
1703
2608
  const elements = []
1704
2609
 
1705
2610
  if (!(elt instanceof ShadowRoot)) {
1706
2611
  if (shouldProcessHxOn(elt)) {
1707
- elements.push(elt)
2612
+ elements.push(asElement(elt))
1708
2613
  }
1709
2614
 
1710
2615
  const iter = document.evaluate('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
1711
2616
  ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', elt)
1712
- while (node = iter.iterateNext()) elements.push(node)
2617
+ while (node = iter.iterateNext()) elements.push(asElement(node))
1713
2618
  }
1714
2619
  return elements
1715
2620
  }
1716
2621
 
1717
- function findElementsToProcess (elt) {
2622
+ /**
2623
+ * @param {Element} elt
2624
+ * @returns {NodeListOf<Element>|[]}
2625
+ */
2626
+ function findElementsToProcess(elt) {
1718
2627
  if (elt.querySelectorAll) {
1719
2628
  const boostedSelector = ', [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]'
1720
2629
  const results = elt.querySelectorAll(VERB_SELECTOR + boostedSelector + ", form, [type='submit']," +
@@ -1725,23 +2634,35 @@ const htmx = (function () {
1725
2634
  }
1726
2635
  }
1727
2636
 
1728
- // Handle submit buttons/inputs that have the form attribute set
1729
- // see https://developer.mozilla.org/docs/Web/HTML/Element/button
1730
- function maybeSetLastButtonClicked (evt) {
1731
- const elt = closest(evt.target, "button, input[type='submit']")
2637
+ /**
2638
+ * Handle submit buttons/inputs that have the form attribute set
2639
+ * see https://developer.mozilla.org/docs/Web/HTML/Element/button
2640
+ * @param {Event} evt
2641
+ */
2642
+ function maybeSetLastButtonClicked(evt) {
2643
+ const elt = /** @type {HTMLButtonElement|HTMLInputElement} */ (closest(asElement(evt.target), "button, input[type='submit']"))
1732
2644
  const internalData = getRelatedFormData(evt)
1733
2645
  if (internalData) {
1734
2646
  internalData.lastButtonClicked = elt
1735
2647
  }
1736
- };
1737
- function maybeUnsetLastButtonClicked (evt) {
2648
+ }
2649
+
2650
+ /**
2651
+ * @param {Event} evt
2652
+ */
2653
+ function maybeUnsetLastButtonClicked(evt) {
1738
2654
  const internalData = getRelatedFormData(evt)
1739
2655
  if (internalData) {
1740
2656
  internalData.lastButtonClicked = null
1741
2657
  }
1742
2658
  }
1743
- function getRelatedFormData (evt) {
1744
- const elt = closest(evt.target, "button, input[type='submit']")
2659
+
2660
+ /**
2661
+ * @param {Event} evt
2662
+ * @returns {HtmxNodeInternalData|undefined}
2663
+ */
2664
+ function getRelatedFormData(evt) {
2665
+ const elt = closest(asElement(evt.target), "button, input[type='submit']")
1745
2666
  if (!elt) {
1746
2667
  return
1747
2668
  }
@@ -1751,7 +2672,11 @@ const htmx = (function () {
1751
2672
  }
1752
2673
  return getInternalData(form)
1753
2674
  }
1754
- function initButtonTracking (elt) {
2675
+
2676
+ /**
2677
+ * @param {EventTarget} elt
2678
+ */
2679
+ function initButtonTracking(elt) {
1755
2680
  // need to handle both click and focus in:
1756
2681
  // focusin - in case someone tabs in to a button and hits the space bar
1757
2682
  // click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724
@@ -1760,28 +2685,20 @@ const htmx = (function () {
1760
2685
  elt.addEventListener('focusout', maybeUnsetLastButtonClicked)
1761
2686
  }
1762
2687
 
1763
- function countCurlies (line) {
1764
- const tokens = tokenizeString(line)
1765
- let netCurlies = 0
1766
- for (let i = 0; i < tokens.length; i++) {
1767
- const token = tokens[i]
1768
- if (token === '{') {
1769
- netCurlies++
1770
- } else if (token === '}') {
1771
- netCurlies--
1772
- }
1773
- }
1774
- return netCurlies
1775
- }
1776
-
1777
- function addHxOnEventHandler (elt, eventName, code) {
2688
+ /**
2689
+ * @param {EventTarget} elt
2690
+ * @param {string} eventName
2691
+ * @param {string} code
2692
+ */
2693
+ function addHxOnEventHandler(elt, eventName, code) {
1778
2694
  const nodeData = getInternalData(elt)
1779
2695
  if (!Array.isArray(nodeData.onHandlers)) {
1780
2696
  nodeData.onHandlers = []
1781
2697
  }
1782
2698
  let func
1783
- const listener = function (e) {
1784
- return maybeEval(elt, function () {
2699
+ /** @type EventListener */
2700
+ const listener = function(e) {
2701
+ maybeEval(elt, function() {
1785
2702
  if (!func) {
1786
2703
  func = new Function('event', code)
1787
2704
  }
@@ -1792,7 +2709,10 @@ const htmx = (function () {
1792
2709
  nodeData.onHandlers.push({ event: eventName, listener })
1793
2710
  }
1794
2711
 
1795
- function processHxOnWildcard (elt) {
2712
+ /**
2713
+ * @param {Element} elt
2714
+ */
2715
+ function processHxOnWildcard(elt) {
1796
2716
  // wipe any previous on handlers so that this function takes precedence
1797
2717
  deInitOnHandlers(elt)
1798
2718
 
@@ -1819,7 +2739,10 @@ const htmx = (function () {
1819
2739
  }
1820
2740
  }
1821
2741
 
1822
- function initNode (elt) {
2742
+ /**
2743
+ * @param {Element|HTMLInputElement} elt
2744
+ */
2745
+ function initNode(elt) {
1823
2746
  if (closest(elt, htmx.config.disableSelector)) {
1824
2747
  cleanUpElement(elt)
1825
2748
  return
@@ -1833,7 +2756,9 @@ const htmx = (function () {
1833
2756
 
1834
2757
  triggerEvent(elt, 'htmx:beforeProcessNode')
1835
2758
 
2759
+ // @ts-ignore value will be undefined for non-input elements, which is fine
1836
2760
  if (elt.value) {
2761
+ // @ts-ignore
1837
2762
  nodeData.lastValue = elt.value
1838
2763
  }
1839
2764
 
@@ -1844,9 +2769,9 @@ const htmx = (function () {
1844
2769
  if (getClosestAttributeValue(elt, 'hx-boost') === 'true') {
1845
2770
  boostElement(elt, nodeData, triggerSpecs)
1846
2771
  } else if (hasAttribute(elt, 'hx-trigger')) {
1847
- triggerSpecs.forEach(function (triggerSpec) {
2772
+ triggerSpecs.forEach(function(triggerSpec) {
1848
2773
  // For "naked" triggers, don't do anything at all
1849
- addTriggerHandler(elt, triggerSpec, nodeData, function () {
2774
+ addTriggerHandler(elt, triggerSpec, nodeData, function() {
1850
2775
  })
1851
2776
  })
1852
2777
  }
@@ -1862,14 +2787,21 @@ const htmx = (function () {
1862
2787
  }
1863
2788
  }
1864
2789
 
1865
- function processNode (elt) {
2790
+ /**
2791
+ * Processes new content, enabling htmx behavior. This can be useful if you have content that is added to the DOM outside of the normal htmx request cycle but still want htmx attributes to work.
2792
+ *
2793
+ * @see https://htmx.org/api/#process
2794
+ *
2795
+ * @param {Element|string} elt element to process
2796
+ */
2797
+ function processNode(elt) {
1866
2798
  elt = resolveTarget(elt)
1867
2799
  if (closest(elt, htmx.config.disableSelector)) {
1868
2800
  cleanUpElement(elt)
1869
2801
  return
1870
2802
  }
1871
2803
  initNode(elt)
1872
- forEach(findElementsToProcess(elt), function (child) { initNode(child) })
2804
+ forEach(findElementsToProcess(elt), function(child) { initNode(child) })
1873
2805
  forEach(findHxOnWildcardElements(elt), processHxOnWildcard)
1874
2806
  }
1875
2807
 
@@ -1877,11 +2809,20 @@ const htmx = (function () {
1877
2809
  // Event/Log Support
1878
2810
  //= ===================================================================
1879
2811
 
1880
- function kebabEventName (str) {
2812
+ /**
2813
+ * @param {string} str
2814
+ * @returns {string}
2815
+ */
2816
+ function kebabEventName(str) {
1881
2817
  return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
1882
2818
  }
1883
2819
 
1884
- function makeEvent (eventName, detail) {
2820
+ /**
2821
+ * @param {string} eventName
2822
+ * @param {any} detail
2823
+ * @returns {CustomEvent}
2824
+ */
2825
+ function makeEvent(eventName, detail) {
1885
2826
  let evt
1886
2827
  if (window.CustomEvent && typeof window.CustomEvent === 'function') {
1887
2828
  // TODO: `composed: true` here is a hack to make global event handlers work with events in shadow DOM
@@ -1894,11 +2835,20 @@ const htmx = (function () {
1894
2835
  return evt
1895
2836
  }
1896
2837
 
1897
- function triggerErrorEvent (elt, eventName, detail) {
2838
+ /**
2839
+ * @param {EventTarget|string} elt
2840
+ * @param {string} eventName
2841
+ * @param {any=} detail
2842
+ */
2843
+ function triggerErrorEvent(elt, eventName, detail) {
1898
2844
  triggerEvent(elt, eventName, mergeObjects({ error: eventName }, detail))
1899
2845
  }
1900
2846
 
1901
- function ignoreEventForLogging (eventName) {
2847
+ /**
2848
+ * @param {string} eventName
2849
+ * @returns {boolean}
2850
+ */
2851
+ function ignoreEventForLogging(eventName) {
1902
2852
  return eventName === 'htmx:afterProcessNode'
1903
2853
  }
1904
2854
 
@@ -1907,12 +2857,12 @@ const htmx = (function () {
1907
2857
  * executes the provided function using each of the active extensions. It should
1908
2858
  * be called internally at every extendable execution point in htmx.
1909
2859
  *
1910
- * @param {HTMLElement} elt
1911
- * @param {(extension:import("./htmx").HtmxExtension) => void} toDo
2860
+ * @param {Element} elt
2861
+ * @param {(extension:HtmxExtension) => void} toDo
1912
2862
  * @returns void
1913
2863
  */
1914
- function withExtensions (elt, toDo) {
1915
- forEach(getExtensions(elt), function (extension) {
2864
+ function withExtensions(elt, toDo) {
2865
+ forEach(getExtensions(elt), function(extension) {
1916
2866
  try {
1917
2867
  toDo(extension)
1918
2868
  } catch (e) {
@@ -1921,7 +2871,7 @@ const htmx = (function () {
1921
2871
  })
1922
2872
  }
1923
2873
 
1924
- function logError (msg) {
2874
+ function logError(msg) {
1925
2875
  if (console.error) {
1926
2876
  console.error(msg)
1927
2877
  } else if (console.log) {
@@ -1929,7 +2879,17 @@ const htmx = (function () {
1929
2879
  }
1930
2880
  }
1931
2881
 
1932
- function triggerEvent (elt, eventName, detail) {
2882
+ /**
2883
+ * Triggers a given event on an element
2884
+ *
2885
+ * @see https://htmx.org/api/#trigger
2886
+ *
2887
+ * @param {EventTarget|string} elt the element to trigger the event on
2888
+ * @param {string} eventName the name of the event to trigger
2889
+ * @param {any=} detail details for the event
2890
+ * @returns {boolean}
2891
+ */
2892
+ function triggerEvent(elt, eventName, detail) {
1933
2893
  elt = resolveTarget(elt)
1934
2894
  if (detail == null) {
1935
2895
  detail = {}
@@ -1949,7 +2909,7 @@ const htmx = (function () {
1949
2909
  const kebabedEvent = makeEvent(kebabName, event.detail)
1950
2910
  eventResult = eventResult && elt.dispatchEvent(kebabedEvent)
1951
2911
  }
1952
- withExtensions(elt, function (extension) {
2912
+ withExtensions(asElement(elt), function(extension) {
1953
2913
  eventResult = eventResult && (extension.onEvent(eventName, event) !== false && !event.defaultPrevented)
1954
2914
  })
1955
2915
  return eventResult
@@ -1960,16 +2920,28 @@ const htmx = (function () {
1960
2920
  //= ===================================================================
1961
2921
  let currentPathForHistory = location.pathname + location.search
1962
2922
 
1963
- function getHistoryElement () {
2923
+ /**
2924
+ * @returns {Element}
2925
+ */
2926
+ function getHistoryElement() {
1964
2927
  const historyElt = getDocument().querySelector('[hx-history-elt],[data-hx-history-elt]')
1965
2928
  return historyElt || getDocument().body
1966
2929
  }
1967
2930
 
1968
- function saveToHistoryCache (url, content, title, scroll) {
2931
+ /**
2932
+ * @param {string} url
2933
+ * @param {Element} rootElt
2934
+ */
2935
+ function saveToHistoryCache(url, rootElt) {
1969
2936
  if (!canAccessLocalStorage()) {
1970
2937
  return
1971
2938
  }
1972
2939
 
2940
+ // get state to save
2941
+ const innerHTML = cleanInnerHtmlForHistory(rootElt)
2942
+ const title = getDocument().title
2943
+ const scroll = window.scrollY
2944
+
1973
2945
  if (htmx.config.historyCacheSize <= 0) {
1974
2946
  // make sure that an eventually already existing cache is purged
1975
2947
  localStorage.removeItem('htmx-history-cache')
@@ -1985,12 +2957,18 @@ const htmx = (function () {
1985
2957
  break
1986
2958
  }
1987
2959
  }
1988
- const newHistoryItem = { url, content, title, scroll }
2960
+
2961
+ /** @type HtmxHistoryItem */
2962
+ const newHistoryItem = { url, content: innerHTML, title, scroll }
2963
+
1989
2964
  triggerEvent(getDocument().body, 'htmx:historyItemCreated', { item: newHistoryItem, cache: historyCache })
2965
+
1990
2966
  historyCache.push(newHistoryItem)
1991
2967
  while (historyCache.length > htmx.config.historyCacheSize) {
1992
2968
  historyCache.shift()
1993
2969
  }
2970
+
2971
+ // keep trying to save the cache until it succeeds or is empty
1994
2972
  while (historyCache.length > 0) {
1995
2973
  try {
1996
2974
  localStorage.setItem('htmx-history-cache', JSON.stringify(historyCache))
@@ -2002,7 +2980,19 @@ const htmx = (function () {
2002
2980
  }
2003
2981
  }
2004
2982
 
2005
- function getCachedHistory (url) {
2983
+ /**
2984
+ * @typedef {Object} HtmxHistoryItem
2985
+ * @property {string} url
2986
+ * @property {string} content
2987
+ * @property {string} title
2988
+ * @property {number} scroll
2989
+ */
2990
+
2991
+ /**
2992
+ * @param {string} url
2993
+ * @returns {HtmxHistoryItem|null}
2994
+ */
2995
+ function getCachedHistory(url) {
2006
2996
  if (!canAccessLocalStorage()) {
2007
2997
  return null
2008
2998
  }
@@ -2018,16 +3008,20 @@ const htmx = (function () {
2018
3008
  return null
2019
3009
  }
2020
3010
 
2021
- function cleanInnerHtmlForHistory (elt) {
3011
+ /**
3012
+ * @param {Element} elt
3013
+ * @returns {string}
3014
+ */
3015
+ function cleanInnerHtmlForHistory(elt) {
2022
3016
  const className = htmx.config.requestClass
2023
- const clone = elt.cloneNode(true)
2024
- forEach(findAll(clone, '.' + className), function (child) {
3017
+ const clone = /** @type Element */ (elt.cloneNode(true))
3018
+ forEach(findAll(clone, '.' + className), function(child) {
2025
3019
  removeClassFromElement(child, className)
2026
3020
  })
2027
3021
  return clone.innerHTML
2028
3022
  }
2029
3023
 
2030
- function saveCurrentPageToHistory () {
3024
+ function saveCurrentPageToHistory() {
2031
3025
  const elt = getHistoryElement()
2032
3026
  const path = currentPathForHistory || location.pathname + location.search
2033
3027
 
@@ -2045,13 +3039,16 @@ const htmx = (function () {
2045
3039
  }
2046
3040
  if (!disableHistoryCache) {
2047
3041
  triggerEvent(getDocument().body, 'htmx:beforeHistorySave', { path, historyElt: elt })
2048
- saveToHistoryCache(path, cleanInnerHtmlForHistory(elt), getDocument().title, window.scrollY)
3042
+ saveToHistoryCache(path, elt)
2049
3043
  }
2050
3044
 
2051
3045
  if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, getDocument().title, window.location.href)
2052
3046
  }
2053
3047
 
2054
- function pushUrlIntoHistory (path) {
3048
+ /**
3049
+ * @param {string} path
3050
+ */
3051
+ function pushUrlIntoHistory(path) {
2055
3052
  // remove the cache buster parameter, if any
2056
3053
  if (htmx.config.getCacheBusterParam) {
2057
3054
  path = path.replace(/org\.htmx\.cache-buster=[^&]*&?/, '')
@@ -2065,18 +3062,27 @@ const htmx = (function () {
2065
3062
  currentPathForHistory = path
2066
3063
  }
2067
3064
 
2068
- function replaceUrlInHistory (path) {
3065
+ /**
3066
+ * @param {string} path
3067
+ */
3068
+ function replaceUrlInHistory(path) {
2069
3069
  if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, '', path)
2070
3070
  currentPathForHistory = path
2071
3071
  }
2072
3072
 
2073
- function settleImmediately (tasks) {
2074
- forEach(tasks, function (task) {
2075
- task.call()
3073
+ /**
3074
+ * @param {HtmxSettleTask[]} tasks
3075
+ */
3076
+ function settleImmediately(tasks) {
3077
+ forEach(tasks, function(task) {
3078
+ task.call(undefined)
2076
3079
  })
2077
3080
  }
2078
3081
 
2079
- function loadHistoryFromServer (path) {
3082
+ /**
3083
+ * @param {string} path
3084
+ */
3085
+ function loadHistoryFromServer(path) {
2080
3086
  const request = new XMLHttpRequest()
2081
3087
  const details = { path, xhr: request }
2082
3088
  triggerEvent(getDocument().body, 'htmx:historyCacheMiss', details)
@@ -2084,25 +3090,17 @@ const htmx = (function () {
2084
3090
  request.setRequestHeader('HX-Request', 'true')
2085
3091
  request.setRequestHeader('HX-History-Restore-Request', 'true')
2086
3092
  request.setRequestHeader('HX-Current-URL', getDocument().location.href)
2087
- request.onload = function () {
3093
+ request.onload = function() {
2088
3094
  if (this.status >= 200 && this.status < 400) {
2089
3095
  triggerEvent(getDocument().body, 'htmx:historyCacheMissLoad', details)
2090
- let fragment = makeFragment(this.response)
2091
- // @ts-ignore
2092
- fragment = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment
3096
+ const fragment = makeFragment(this.response)
3097
+ /** @type ParentNode */
3098
+ const content = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment
2093
3099
  const historyElement = getHistoryElement()
2094
3100
  const settleInfo = makeSettleInfo(historyElement)
2095
- const title = findTitle(this.response)
2096
- if (title) {
2097
- const titleElt = find('title')
2098
- if (titleElt) {
2099
- titleElt.innerHTML = title
2100
- } else {
2101
- window.document.title = title
2102
- }
2103
- }
2104
- // @ts-ignore
2105
- swapInnerHTML(historyElement, fragment, settleInfo)
3101
+ handleTitle(fragment.title)
3102
+
3103
+ swapInnerHTML(historyElement, content, settleInfo)
2106
3104
  settleImmediately(settleInfo.tasks)
2107
3105
  currentPathForHistory = path
2108
3106
  triggerEvent(getDocument().body, 'htmx:historyRestore', { path, cacheMiss: true, serverResponse: this.response })
@@ -2113,7 +3111,10 @@ const htmx = (function () {
2113
3111
  request.send()
2114
3112
  }
2115
3113
 
2116
- function restoreHistory (path) {
3114
+ /**
3115
+ * @param {string} [path]
3116
+ */
3117
+ function restoreHistory(path) {
2117
3118
  saveCurrentPageToHistory()
2118
3119
  path = path || location.pathname + location.search
2119
3120
  const cached = getCachedHistory(path)
@@ -2121,17 +3122,18 @@ const htmx = (function () {
2121
3122
  const fragment = makeFragment(cached.content)
2122
3123
  const historyElement = getHistoryElement()
2123
3124
  const settleInfo = makeSettleInfo(historyElement)
3125
+ handleTitle(fragment.title)
2124
3126
  swapInnerHTML(historyElement, fragment, settleInfo)
2125
3127
  settleImmediately(settleInfo.tasks)
2126
- document.title = cached.title
2127
- setTimeout(function () {
3128
+ getWindow().setTimeout(function() {
2128
3129
  window.scrollTo(0, cached.scroll)
2129
3130
  }, 0) // next 'tick', so browser has time to render layout
2130
3131
  currentPathForHistory = path
2131
3132
  triggerEvent(getDocument().body, 'htmx:historyRestore', { path, item: cached })
2132
3133
  } else {
2133
3134
  if (htmx.config.refreshOnHistoryMiss) {
2134
- // @ts-ignore: optional parameter in reload() function throws error
3135
+ // @ts-ignore: optional parameter in reload() function throws error
3136
+ // noinspection JSUnresolvedReference
2135
3137
  window.location.reload(true)
2136
3138
  } else {
2137
3139
  loadHistoryFromServer(path)
@@ -2139,12 +3141,16 @@ const htmx = (function () {
2139
3141
  }
2140
3142
  }
2141
3143
 
2142
- function addRequestIndicatorClasses (elt) {
2143
- let indicators = findAttributeTargets(elt, 'hx-indicator')
3144
+ /**
3145
+ * @param {Element} elt
3146
+ * @returns {Element[]}
3147
+ */
3148
+ function addRequestIndicatorClasses(elt) {
3149
+ let indicators = /** @type Element[] */ (findAttributeTargets(elt, 'hx-indicator'))
2144
3150
  if (indicators == null) {
2145
3151
  indicators = [elt]
2146
3152
  }
2147
- forEach(indicators, function (ic) {
3153
+ forEach(indicators, function(ic) {
2148
3154
  const internalData = getInternalData(ic)
2149
3155
  internalData.requestCount = (internalData.requestCount || 0) + 1
2150
3156
  ic.classList.add.call(ic.classList, htmx.config.requestClass)
@@ -2152,12 +3158,16 @@ const htmx = (function () {
2152
3158
  return indicators
2153
3159
  }
2154
3160
 
2155
- function disableElements (elt) {
2156
- let disabledElts = findAttributeTargets(elt, 'hx-disabled-elt')
3161
+ /**
3162
+ * @param {Element} elt
3163
+ * @returns {Element[]}
3164
+ */
3165
+ function disableElements(elt) {
3166
+ let disabledElts = /** @type Element[] */ (findAttributeTargets(elt, 'hx-disabled-elt'))
2157
3167
  if (disabledElts == null) {
2158
3168
  disabledElts = []
2159
3169
  }
2160
- forEach(disabledElts, function (disabledElement) {
3170
+ forEach(disabledElts, function(disabledElement) {
2161
3171
  const internalData = getInternalData(disabledElement)
2162
3172
  internalData.requestCount = (internalData.requestCount || 0) + 1
2163
3173
  disabledElement.setAttribute('disabled', '')
@@ -2165,15 +3175,19 @@ const htmx = (function () {
2165
3175
  return disabledElts
2166
3176
  }
2167
3177
 
2168
- function removeRequestIndicators (indicators, disabled) {
2169
- forEach(indicators, function (ic) {
3178
+ /**
3179
+ * @param {Element[]} indicators
3180
+ * @param {Element[]} disabled
3181
+ */
3182
+ function removeRequestIndicators(indicators, disabled) {
3183
+ forEach(indicators, function(ic) {
2170
3184
  const internalData = getInternalData(ic)
2171
3185
  internalData.requestCount = (internalData.requestCount || 0) - 1
2172
3186
  if (internalData.requestCount === 0) {
2173
3187
  ic.classList.remove.call(ic.classList, htmx.config.requestClass)
2174
3188
  }
2175
3189
  })
2176
- forEach(disabled, function (disabledElement) {
3190
+ forEach(disabled, function(disabledElement) {
2177
3191
  const internalData = getInternalData(disabledElement)
2178
3192
  internalData.requestCount = (internalData.requestCount || 0) - 1
2179
3193
  if (internalData.requestCount === 0) {
@@ -2186,7 +3200,12 @@ const htmx = (function () {
2186
3200
  // Input Value Processing
2187
3201
  //= ===================================================================
2188
3202
 
2189
- function haveSeenNode (processed, elt) {
3203
+ /**
3204
+ * @param {Element[]} processed
3205
+ * @param {Element} elt
3206
+ * @returns {boolean}
3207
+ */
3208
+ function haveSeenNode(processed, elt) {
2190
3209
  for (let i = 0; i < processed.length; i++) {
2191
3210
  const node = processed[i]
2192
3211
  if (node.isSameNode(elt)) {
@@ -2196,8 +3215,14 @@ const htmx = (function () {
2196
3215
  return false
2197
3216
  }
2198
3217
 
2199
- function shouldInclude (elt) {
2200
- if (elt.name === '' || elt.name == null || elt.disabled) {
3218
+ /**
3219
+ * @param {Element} element
3220
+ * @return {boolean}
3221
+ */
3222
+ function shouldInclude(element) {
3223
+ // Cast to trick tsc, undefined values will work fine here
3224
+ const elt = /** @type {HTMLInputElement} */ (element)
3225
+ if (elt.name === '' || elt.name == null || elt.disabled || closest(elt, 'fieldset[disabled]')) {
2201
3226
  return false
2202
3227
  }
2203
3228
  // ignore "submitter" types (see jQuery src/serialize.js)
@@ -2210,30 +3235,43 @@ const htmx = (function () {
2210
3235
  return true
2211
3236
  }
2212
3237
 
2213
- function addValueToValues (name, value, values) {
2214
- // This is a little ugly because both the current value of the named value in the form
2215
- // and the new value could be arrays, so we have to handle all four cases :/
3238
+ /** @param {string} name
3239
+ * @param {string|Array|FormDataEntryValue} value
3240
+ * @param {FormData} formData */
3241
+ function addValueToFormData(name, value, formData) {
2216
3242
  if (name != null && value != null) {
2217
- const current = values[name]
2218
- if (current === undefined) {
2219
- values[name] = value
2220
- } else if (Array.isArray(current)) {
2221
- if (Array.isArray(value)) {
2222
- values[name] = current.concat(value)
2223
- } else {
2224
- current.push(value)
2225
- }
3243
+ if (Array.isArray(value)) {
3244
+ value.forEach(function(v) { formData.append(name, v) })
2226
3245
  } else {
2227
- if (Array.isArray(value)) {
2228
- values[name] = [current].concat(value)
2229
- } else {
2230
- values[name] = [current, value]
2231
- }
3246
+ formData.append(name, value)
3247
+ }
3248
+ }
3249
+ }
3250
+
3251
+ /** @param {string} name
3252
+ * @param {string|Array} value
3253
+ * @param {FormData} formData */
3254
+ function removeValueFromFormData(name, value, formData) {
3255
+ if (name != null && value != null) {
3256
+ let values = formData.getAll(name)
3257
+ if (Array.isArray(value)) {
3258
+ values = values.filter(v => value.indexOf(v) < 0)
3259
+ } else {
3260
+ values = values.filter(v => v !== value)
2232
3261
  }
3262
+ formData.delete(name)
3263
+ forEach(values, v => formData.append(name, v))
2233
3264
  }
2234
3265
  }
2235
3266
 
2236
- function processInputValue (processed, values, errors, elt, validate) {
3267
+ /**
3268
+ * @param {Element[]} processed
3269
+ * @param {FormData} formData
3270
+ * @param {HtmxElementValidationError[]} errors
3271
+ * @param {Element|HTMLInputElement|HTMLSelectElement|HTMLFormElement} elt
3272
+ * @param {boolean} validate
3273
+ */
3274
+ function processInputValue(processed, formData, errors, elt, validate) {
2237
3275
  if (elt == null || haveSeenNode(processed, elt)) {
2238
3276
  return
2239
3277
  } else {
@@ -2241,28 +3279,47 @@ const htmx = (function () {
2241
3279
  }
2242
3280
  if (shouldInclude(elt)) {
2243
3281
  const name = getRawAttribute(elt, 'name')
3282
+ // @ts-ignore value will be undefined for non-input elements, which is fine
2244
3283
  let value = elt.value
2245
- if (elt.multiple && elt.tagName === 'SELECT') {
2246
- value = toArray(elt.querySelectorAll('option:checked')).map(function (e) { return e.value })
3284
+ if (elt instanceof HTMLSelectElement && elt.multiple) {
3285
+ value = toArray(elt.querySelectorAll('option:checked')).map(function(e) { return (/** @type HTMLOptionElement */(e)).value })
2247
3286
  }
2248
3287
  // include file inputs
2249
- if (elt.files) {
3288
+ if (elt instanceof HTMLInputElement && elt.files) {
2250
3289
  value = toArray(elt.files)
2251
3290
  }
2252
- addValueToValues(name, value, values)
3291
+ addValueToFormData(name, value, formData)
2253
3292
  if (validate) {
2254
3293
  validateElement(elt, errors)
2255
3294
  }
2256
3295
  }
2257
- if (matches(elt, 'form')) {
2258
- const inputs = elt.elements
2259
- forEach(inputs, function (input) {
2260
- processInputValue(processed, values, errors, input, validate)
3296
+ if (elt instanceof HTMLFormElement) {
3297
+ forEach(elt.elements, function(input) {
3298
+ if (processed.indexOf(input) >= 0) {
3299
+ // The input has already been processed and added to the values, but the FormData that will be
3300
+ // constructed right after on the form, will include it once again. So remove that input's value
3301
+ // now to avoid duplicates
3302
+ removeValueFromFormData(input.name, input.value, formData)
3303
+ } else {
3304
+ processed.push(input)
3305
+ }
3306
+ if (validate) {
3307
+ validateElement(input, errors)
3308
+ }
3309
+ })
3310
+ new FormData(elt).forEach(function(value, name) {
3311
+ addValueToFormData(name, value, formData)
2261
3312
  })
2262
3313
  }
2263
3314
  }
2264
3315
 
2265
- function validateElement (element, errors) {
3316
+ /**
3317
+ *
3318
+ * @param {Element} elt
3319
+ * @param {HtmxElementValidationError[]} errors
3320
+ */
3321
+ function validateElement(elt, errors) {
3322
+ const element = /** @type {HTMLElement & ElementInternals} */ (elt)
2266
3323
  if (element.willValidate) {
2267
3324
  triggerEvent(element, 'htmx:validation:validate')
2268
3325
  if (!element.checkValidity()) {
@@ -2273,13 +3330,32 @@ const htmx = (function () {
2273
3330
  }
2274
3331
 
2275
3332
  /**
2276
- * @param {HTMLElement} elt
2277
- * @param {string} verb
3333
+ * Override values in the one FormData with those from another.
3334
+ * @param {FormData} receiver the formdata that will be mutated
3335
+ * @param {FormData} donor the formdata that will provide the overriding values
3336
+ * @returns {FormData} the {@linkcode receiver}
3337
+ */
3338
+ function overrideFormData(receiver, donor) {
3339
+ for (const key of donor.keys()) {
3340
+ receiver.delete(key)
3341
+ donor.getAll(key).forEach(function(value) {
3342
+ receiver.append(key, value)
3343
+ })
3344
+ }
3345
+ return receiver
3346
+ }
3347
+
3348
+ /**
3349
+ * @param {Element|HTMLFormElement} elt
3350
+ * @param {HttpVerb} verb
3351
+ * @returns {{errors: HtmxElementValidationError[], formData: FormData, values: Object}}
2278
3352
  */
2279
- function getInputValues (elt, verb) {
3353
+ function getInputValues(elt, verb) {
3354
+ /** @type Element[] */
2280
3355
  const processed = []
2281
- let values = {}
2282
- const formValues = {}
3356
+ const formData = new FormData()
3357
+ const priorityFormData = new FormData()
3358
+ /** @type HtmxElementValidationError[] */
2283
3359
  const errors = []
2284
3360
  const internalData = getInternalData(elt)
2285
3361
  if (internalData.lastButtonClicked && !bodyContains(internalData.lastButtonClicked)) {
@@ -2288,46 +3364,52 @@ const htmx = (function () {
2288
3364
 
2289
3365
  // only validate when form is directly submitted and novalidate or formnovalidate are not set
2290
3366
  // or if the element has an explicit hx-validate="true" on it
2291
- let validate = (matches(elt, 'form') && elt.noValidate !== true) || getAttributeValue(elt, 'hx-validate') === 'true'
3367
+ let validate = (elt instanceof HTMLFormElement && elt.noValidate !== true) || getAttributeValue(elt, 'hx-validate') === 'true'
2292
3368
  if (internalData.lastButtonClicked) {
2293
3369
  validate = validate && internalData.lastButtonClicked.formNoValidate !== true
2294
3370
  }
2295
3371
 
2296
3372
  // for a non-GET include the closest form
2297
3373
  if (verb !== 'get') {
2298
- processInputValue(processed, formValues, errors, closest(elt, 'form'), validate)
3374
+ processInputValue(processed, priorityFormData, errors, closest(elt, 'form'), validate)
2299
3375
  }
2300
3376
 
2301
3377
  // include the element itself
2302
- processInputValue(processed, values, errors, elt, validate)
3378
+ processInputValue(processed, formData, errors, elt, validate)
2303
3379
 
2304
3380
  // if a button or submit was clicked last, include its value
2305
3381
  if (internalData.lastButtonClicked || elt.tagName === 'BUTTON' ||
2306
3382
  (elt.tagName === 'INPUT' && getRawAttribute(elt, 'type') === 'submit')) {
2307
- const button = internalData.lastButtonClicked || elt
3383
+ const button = internalData.lastButtonClicked || (/** @type HTMLInputElement|HTMLButtonElement */(elt))
2308
3384
  const name = getRawAttribute(button, 'name')
2309
- addValueToValues(name, button.value, formValues)
3385
+ addValueToFormData(name, button.value, priorityFormData)
2310
3386
  }
2311
3387
 
2312
3388
  // include any explicit includes
2313
3389
  const includes = findAttributeTargets(elt, 'hx-include')
2314
- forEach(includes, function (node) {
2315
- processInputValue(processed, values, errors, node, validate)
3390
+ forEach(includes, function(node) {
3391
+ processInputValue(processed, formData, errors, asElement(node), validate)
2316
3392
  // if a non-form is included, include any input values within it
2317
3393
  if (!matches(node, 'form')) {
2318
- forEach(node.querySelectorAll(INPUT_SELECTOR), function (descendant) {
2319
- processInputValue(processed, values, errors, descendant, validate)
3394
+ forEach(asParentNode(node).querySelectorAll(INPUT_SELECTOR), function(descendant) {
3395
+ processInputValue(processed, formData, errors, descendant, validate)
2320
3396
  })
2321
3397
  }
2322
3398
  })
2323
3399
 
2324
- // form values take precedence, overriding the regular values
2325
- values = mergeObjects(values, formValues)
3400
+ // values from a <form> take precedence, overriding the regular values
3401
+ overrideFormData(formData, priorityFormData)
2326
3402
 
2327
- return { errors, values }
3403
+ return { errors, formData, values: formDataProxy(formData) }
2328
3404
  }
2329
3405
 
2330
- function appendParam (returnStr, name, realValue) {
3406
+ /**
3407
+ * @param {string} returnStr
3408
+ * @param {string} name
3409
+ * @param {any} realValue
3410
+ * @returns {string}
3411
+ */
3412
+ function appendParam(returnStr, name, realValue) {
2331
3413
  if (returnStr !== '') {
2332
3414
  returnStr += '&'
2333
3415
  }
@@ -2339,51 +3421,31 @@ const htmx = (function () {
2339
3421
  return returnStr
2340
3422
  }
2341
3423
 
2342
- function urlEncode (values) {
3424
+ /**
3425
+ * @param {FormData|Object} values
3426
+ * @returns string
3427
+ */
3428
+ function urlEncode(values) {
3429
+ values = formDataFromObject(values)
2343
3430
  let returnStr = ''
2344
- for (var name in values) {
2345
- if (values.hasOwnProperty(name)) {
2346
- const value = values[name]
2347
- if (Array.isArray(value)) {
2348
- forEach(value, function (v) {
2349
- returnStr = appendParam(returnStr, name, v)
2350
- })
2351
- } else {
2352
- returnStr = appendParam(returnStr, name, value)
2353
- }
2354
- }
2355
- }
3431
+ values.forEach(function(value, key) {
3432
+ returnStr = appendParam(returnStr, key, value)
3433
+ })
2356
3434
  return returnStr
2357
3435
  }
2358
3436
 
2359
- function makeFormData (values) {
2360
- const formData = new FormData()
2361
- for (var name in values) {
2362
- if (values.hasOwnProperty(name)) {
2363
- const value = values[name]
2364
- if (Array.isArray(value)) {
2365
- forEach(value, function (v) {
2366
- formData.append(name, v)
2367
- })
2368
- } else {
2369
- formData.append(name, value)
2370
- }
2371
- }
2372
- }
2373
- return formData
2374
- }
2375
-
2376
3437
  //= ===================================================================
2377
3438
  // Ajax
2378
3439
  //= ===================================================================
2379
3440
 
2380
3441
  /**
2381
- * @param {HTMLElement} elt
2382
- * @param {HTMLElement} target
3442
+ * @param {Element} elt
3443
+ * @param {Element} target
2383
3444
  * @param {string} prompt
2384
- * @returns {Object} // TODO: Define/Improve HtmxHeaderSpecification
3445
+ * @returns {HtmxHeaderSpecification}
2385
3446
  */
2386
- function getHeaders (elt, target, prompt) {
3447
+ function getHeaders(elt, target, prompt) {
3448
+ /** @type HtmxHeaderSpecification */
2387
3449
  const headers = {
2388
3450
  'HX-Request': 'true',
2389
3451
  'HX-Trigger': getRawAttribute(elt, 'id'),
@@ -2405,28 +3467,30 @@ const htmx = (function () {
2405
3467
  * filterValues takes an object containing form input values
2406
3468
  * and returns a new object that only contains keys that are
2407
3469
  * specified by the closest "hx-params" attribute
2408
- * @param {Object} inputValues
2409
- * @param {HTMLElement} elt
2410
- * @returns {Object}
3470
+ * @param {FormData} inputValues
3471
+ * @param {Element} elt
3472
+ * @returns {FormData}
2411
3473
  */
2412
- function filterValues (inputValues, elt) {
3474
+ function filterValues(inputValues, elt) {
2413
3475
  const paramsValue = getClosestAttributeValue(elt, 'hx-params')
2414
3476
  if (paramsValue) {
2415
3477
  if (paramsValue === 'none') {
2416
- return {}
3478
+ return new FormData()
2417
3479
  } else if (paramsValue === '*') {
2418
3480
  return inputValues
2419
3481
  } else if (paramsValue.indexOf('not ') === 0) {
2420
- forEach(paramsValue.substr(4).split(','), function (name) {
3482
+ forEach(paramsValue.substr(4).split(','), function(name) {
2421
3483
  name = name.trim()
2422
- delete inputValues[name]
3484
+ inputValues.delete(name)
2423
3485
  })
2424
3486
  return inputValues
2425
3487
  } else {
2426
- const newValues = {}
2427
- forEach(paramsValue.split(','), function (name) {
3488
+ const newValues = new FormData()
3489
+ forEach(paramsValue.split(','), function(name) {
2428
3490
  name = name.trim()
2429
- newValues[name] = inputValues[name]
3491
+ if (inputValues.has(name)) {
3492
+ inputValues.getAll(name).forEach(function(value) { newValues.append(name, value) })
3493
+ }
2430
3494
  })
2431
3495
  return newValues
2432
3496
  }
@@ -2435,18 +3499,22 @@ const htmx = (function () {
2435
3499
  }
2436
3500
  }
2437
3501
 
2438
- function isAnchorLink (elt) {
2439
- return getRawAttribute(elt, 'href') && getRawAttribute(elt, 'href').indexOf('#') >= 0
3502
+ /**
3503
+ * @param {Element} elt
3504
+ * @return {boolean}
3505
+ */
3506
+ function isAnchorLink(elt) {
3507
+ return !!getRawAttribute(elt, 'href') && getRawAttribute(elt, 'href').indexOf('#') >= 0
2440
3508
  }
2441
3509
 
2442
3510
  /**
2443
- *
2444
- * @param {HTMLElement} elt
2445
- * @param {string} swapInfoOverride
2446
- * @returns {import("./htmx").HtmxSwapSpecification}
3511
+ * @param {Element} elt
3512
+ * @param {HtmxSwapStyle} [swapInfoOverride]
3513
+ * @returns {HtmxSwapSpecification}
2447
3514
  */
2448
- function getSwapSpecification (elt, swapInfoOverride) {
3515
+ function getSwapSpecification(elt, swapInfoOverride) {
2449
3516
  const swapInfo = swapInfoOverride || getClosestAttributeValue(elt, 'hx-swap')
3517
+ /** @type HtmxSwapSpecification */
2450
3518
  const swapSpec = {
2451
3519
  swapStyle: getInternalData(elt).boosted ? 'innerHTML' : htmx.config.defaultSwapStyle,
2452
3520
  swapDelay: htmx.config.defaultSwapDelay,
@@ -2473,6 +3541,7 @@ const htmx = (function () {
2473
3541
  var splitSpec = scrollSpec.split(':')
2474
3542
  const scrollVal = splitSpec.pop()
2475
3543
  var selectorVal = splitSpec.length > 0 ? splitSpec.join(':') : null
3544
+ // @ts-ignore
2476
3545
  swapSpec.scroll = scrollVal
2477
3546
  swapSpec.scrollTarget = selectorVal
2478
3547
  } else if (value.indexOf('show:') === 0) {
@@ -2496,14 +3565,24 @@ const htmx = (function () {
2496
3565
  return swapSpec
2497
3566
  }
2498
3567
 
2499
- function usesFormData (elt) {
3568
+ /**
3569
+ * @param {Element} elt
3570
+ * @return {boolean}
3571
+ */
3572
+ function usesFormData(elt) {
2500
3573
  return getClosestAttributeValue(elt, 'hx-encoding') === 'multipart/form-data' ||
2501
3574
  (matches(elt, 'form') && getRawAttribute(elt, 'enctype') === 'multipart/form-data')
2502
3575
  }
2503
3576
 
2504
- function encodeParamsForBody (xhr, elt, filteredParameters) {
3577
+ /**
3578
+ * @param {XMLHttpRequest} xhr
3579
+ * @param {Element} elt
3580
+ * @param {FormData} filteredParameters
3581
+ * @returns {*|string|null}
3582
+ */
3583
+ function encodeParamsForBody(xhr, elt, filteredParameters) {
2505
3584
  let encodedParameters = null
2506
- withExtensions(elt, function (extension) {
3585
+ withExtensions(elt, function(extension) {
2507
3586
  if (encodedParameters == null) {
2508
3587
  encodedParameters = extension.encodeParameters(xhr, filteredParameters, elt)
2509
3588
  }
@@ -2512,7 +3591,7 @@ const htmx = (function () {
2512
3591
  return encodedParameters
2513
3592
  } else {
2514
3593
  if (usesFormData(elt)) {
2515
- return makeFormData(filteredParameters)
3594
+ return formDataFromObject(filteredParameters)
2516
3595
  } else {
2517
3596
  return urlEncode(filteredParameters)
2518
3597
  }
@@ -2522,19 +3601,23 @@ const htmx = (function () {
2522
3601
  /**
2523
3602
  *
2524
3603
  * @param {Element} target
2525
- * @returns {import("./htmx").HtmxSettleInfo}
3604
+ * @returns {HtmxSettleInfo}
2526
3605
  */
2527
- function makeSettleInfo (target) {
3606
+ function makeSettleInfo(target) {
2528
3607
  return { tasks: [], elts: [target] }
2529
3608
  }
2530
3609
 
2531
- function updateScrollState (content, swapSpec) {
3610
+ /**
3611
+ * @param {Element[]} content
3612
+ * @param {HtmxSwapSpecification} swapSpec
3613
+ */
3614
+ function updateScrollState(content, swapSpec) {
2532
3615
  const first = content[0]
2533
3616
  const last = content[content.length - 1]
2534
3617
  if (swapSpec.scroll) {
2535
3618
  var target = null
2536
3619
  if (swapSpec.scrollTarget) {
2537
- target = querySelectorExt(first, swapSpec.scrollTarget)
3620
+ target = asElement(querySelectorExt(first, swapSpec.scrollTarget))
2538
3621
  }
2539
3622
  if (swapSpec.scroll === 'top' && (first || target)) {
2540
3623
  target = target || first
@@ -2552,27 +3635,29 @@ const htmx = (function () {
2552
3635
  if (swapSpec.showTarget === 'window') {
2553
3636
  targetStr = 'body'
2554
3637
  }
2555
- target = querySelectorExt(first, targetStr)
3638
+ target = asElement(querySelectorExt(first, targetStr))
2556
3639
  }
2557
3640
  if (swapSpec.show === 'top' && (first || target)) {
2558
3641
  target = target || first
3642
+ // @ts-ignore For some reason tsc doesn't recognize "instant" as a valid option for now
2559
3643
  target.scrollIntoView({ block: 'start', behavior: htmx.config.scrollBehavior })
2560
3644
  }
2561
3645
  if (swapSpec.show === 'bottom' && (last || target)) {
2562
3646
  target = target || last
3647
+ // @ts-ignore For some reason tsc doesn't recognize "instant" as a valid option for now
2563
3648
  target.scrollIntoView({ block: 'end', behavior: htmx.config.scrollBehavior })
2564
3649
  }
2565
3650
  }
2566
3651
  }
2567
3652
 
2568
3653
  /**
2569
- * @param {HTMLElement} elt
3654
+ * @param {Element} elt
2570
3655
  * @param {string} attr
2571
3656
  * @param {boolean=} evalAsDefault
2572
3657
  * @param {Object=} values
2573
3658
  * @returns {Object}
2574
3659
  */
2575
- function getValuesForElement (elt, attr, evalAsDefault, values) {
3660
+ function getValuesForElement(elt, attr, evalAsDefault, values) {
2576
3661
  if (values == null) {
2577
3662
  values = {}
2578
3663
  }
@@ -2598,7 +3683,7 @@ const htmx = (function () {
2598
3683
  }
2599
3684
  let varsValues
2600
3685
  if (evaluateValue) {
2601
- varsValues = maybeEval(elt, function () { return Function('return (' + str + ')')() }, {})
3686
+ varsValues = maybeEval(elt, function() { return Function('return (' + str + ')')() }, {})
2602
3687
  } else {
2603
3688
  varsValues = parseJSON(str)
2604
3689
  }
@@ -2610,10 +3695,16 @@ const htmx = (function () {
2610
3695
  }
2611
3696
  }
2612
3697
  }
2613
- return getValuesForElement(parentElt(elt), attr, evalAsDefault, values)
3698
+ return getValuesForElement(asElement(parentElt(elt)), attr, evalAsDefault, values)
2614
3699
  }
2615
3700
 
2616
- function maybeEval (elt, toEval, defaultVal) {
3701
+ /**
3702
+ * @param {EventTarget|string} elt
3703
+ * @param {() => any} toEval
3704
+ * @param {any=} defaultVal
3705
+ * @returns {any}
3706
+ */
3707
+ function maybeEval(elt, toEval, defaultVal) {
2617
3708
  if (htmx.config.allowEval) {
2618
3709
  return toEval()
2619
3710
  } else {
@@ -2623,32 +3714,37 @@ const htmx = (function () {
2623
3714
  }
2624
3715
 
2625
3716
  /**
2626
- * @param {HTMLElement} elt
2627
- * @param {*} expressionVars
3717
+ * @param {Element} elt
3718
+ * @param {*?} expressionVars
2628
3719
  * @returns
2629
3720
  */
2630
- function getHXVarsForElement (elt, expressionVars) {
3721
+ function getHXVarsForElement(elt, expressionVars) {
2631
3722
  return getValuesForElement(elt, 'hx-vars', true, expressionVars)
2632
3723
  }
2633
3724
 
2634
3725
  /**
2635
- * @param {HTMLElement} elt
2636
- * @param {*} expressionVars
3726
+ * @param {Element} elt
3727
+ * @param {*?} expressionVars
2637
3728
  * @returns
2638
3729
  */
2639
- function getHXValsForElement (elt, expressionVars) {
3730
+ function getHXValsForElement(elt, expressionVars) {
2640
3731
  return getValuesForElement(elt, 'hx-vals', false, expressionVars)
2641
3732
  }
2642
3733
 
2643
3734
  /**
2644
- * @param {HTMLElement} elt
2645
- * @returns {Object}
3735
+ * @param {Element} elt
3736
+ * @returns {FormData}
2646
3737
  */
2647
- function getExpressionVars (elt) {
2648
- return mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt))
3738
+ function getExpressionVars(elt) {
3739
+ return formDataFromObject(mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt)))
2649
3740
  }
2650
3741
 
2651
- function safelySetHeaderValue (xhr, header, headerValue) {
3742
+ /**
3743
+ * @param {XMLHttpRequest} xhr
3744
+ * @param {string} header
3745
+ * @param {string|null} headerValue
3746
+ */
3747
+ function safelySetHeaderValue(xhr, header, headerValue) {
2652
3748
  if (headerValue !== null) {
2653
3749
  try {
2654
3750
  xhr.setRequestHeader(header, headerValue)
@@ -2660,7 +3756,11 @@ const htmx = (function () {
2660
3756
  }
2661
3757
  }
2662
3758
 
2663
- function getPathFromResponse (xhr) {
3759
+ /**
3760
+ * @param {XMLHttpRequest} xhr
3761
+ * @return {string}
3762
+ */
3763
+ function getPathFromResponse(xhr) {
2664
3764
  // NB: IE11 does not support this stuff
2665
3765
  if (xhr.responseURL && typeof (URL) !== 'undefined') {
2666
3766
  try {
@@ -2672,14 +3772,29 @@ const htmx = (function () {
2672
3772
  }
2673
3773
  }
2674
3774
 
2675
- function hasHeader (xhr, regexp) {
3775
+ /**
3776
+ * @param {XMLHttpRequest} xhr
3777
+ * @param {RegExp} regexp
3778
+ * @return {boolean}
3779
+ */
3780
+ function hasHeader(xhr, regexp) {
2676
3781
  return regexp.test(xhr.getAllResponseHeaders())
2677
3782
  }
2678
3783
 
2679
- function ajaxHelper (verb, path, context) {
2680
- verb = verb.toLowerCase()
3784
+ /**
3785
+ * Issues an htmx-style AJAX request
3786
+ *
3787
+ * @see https://htmx.org/api/#ajax
3788
+ *
3789
+ * @param {HttpVerb} verb
3790
+ * @param {string} path the URL path to make the AJAX
3791
+ * @param {Element|string|HtmxAjaxHelperContext} context the element to target (defaults to the **body**) | a selector for the target | a context object that contains any of the following
3792
+ * @return {Promise<void>} Promise that resolves immediately if no request is sent, or when the request is complete
3793
+ */
3794
+ function ajaxHelper(verb, path, context) {
3795
+ verb = (/** @type HttpVerb */(verb.toLowerCase()))
2681
3796
  if (context) {
2682
- if (context instanceof Element || isType(context, 'String')) {
3797
+ if (context instanceof Element || typeof context === 'string') {
2683
3798
  return issueAjaxRequest(verb, path, null, null, {
2684
3799
  targetOverride: resolveTarget(context),
2685
3800
  returnPromise: true
@@ -2703,7 +3818,11 @@ const htmx = (function () {
2703
3818
  }
2704
3819
  }
2705
3820
 
2706
- function hierarchyForElt (elt) {
3821
+ /**
3822
+ * @param {Element} elt
3823
+ * @return {Element[]}
3824
+ */
3825
+ function hierarchyForElt(elt) {
2707
3826
  const arr = []
2708
3827
  while (elt) {
2709
3828
  arr.push(elt)
@@ -2712,7 +3831,13 @@ const htmx = (function () {
2712
3831
  return arr
2713
3832
  }
2714
3833
 
2715
- function verifyPath (elt, path, requestConfig) {
3834
+ /**
3835
+ * @param {Element} elt
3836
+ * @param {string} path
3837
+ * @param {HtmxRequestConfig} requestConfig
3838
+ * @return {boolean}
3839
+ */
3840
+ function verifyPath(elt, path, requestConfig) {
2716
3841
  let sameHost
2717
3842
  let url
2718
3843
  if (typeof URL === 'function') {
@@ -2733,12 +3858,146 @@ const htmx = (function () {
2733
3858
  return triggerEvent(elt, 'htmx:validateUrl', mergeObjects({ url, sameHost }, requestConfig))
2734
3859
  }
2735
3860
 
2736
- function issueAjaxRequest (verb, path, elt, event, etc, confirmed) {
3861
+ /**
3862
+ * @param {Object|FormData} obj
3863
+ * @return {FormData}
3864
+ */
3865
+ function formDataFromObject(obj) {
3866
+ if (obj instanceof FormData) return obj
3867
+ const formData = new FormData()
3868
+ for (const key in obj) {
3869
+ if (obj.hasOwnProperty(key)) {
3870
+ if (typeof obj[key].forEach === 'function') {
3871
+ obj[key].forEach(function(v) { formData.append(key, v) })
3872
+ } else if (typeof obj[key] === 'object') {
3873
+ formData.append(key, JSON.stringify(obj[key]))
3874
+ } else {
3875
+ formData.append(key, obj[key])
3876
+ }
3877
+ }
3878
+ }
3879
+ return formData
3880
+ }
3881
+
3882
+ /**
3883
+ * @param {FormData} formData
3884
+ * @param {string} name
3885
+ * @param {Array} array
3886
+ * @returns {Array}
3887
+ */
3888
+ function formDataArrayProxy(formData, name, array) {
3889
+ // mutating the array should mutate the underlying form data
3890
+ return new Proxy(array, {
3891
+ get: function(target, key) {
3892
+ if (typeof key === 'number') return target[key]
3893
+ if (key === 'length') return target.length
3894
+ if (key === 'push') {
3895
+ return function(value) {
3896
+ target.push(value)
3897
+ formData.append(name, value)
3898
+ }
3899
+ }
3900
+ if (typeof target[key] === 'function') {
3901
+ return function() {
3902
+ target[key].apply(target, arguments)
3903
+ formData.delete(name)
3904
+ target.forEach(function(v) { formData.append(name, v) })
3905
+ }
3906
+ }
3907
+
3908
+ if (target[key] && target[key].length === 1) {
3909
+ return target[key][0]
3910
+ } else {
3911
+ return target[key]
3912
+ }
3913
+ },
3914
+ set: function(target, index, value) {
3915
+ target[index] = value
3916
+ formData.delete(name)
3917
+ target.forEach(function(v) { formData.append(name, v) })
3918
+ return true
3919
+ }
3920
+ })
3921
+ }
3922
+
3923
+ /**
3924
+ * @param {FormData} formData
3925
+ * @returns {Object}
3926
+ */
3927
+ function formDataProxy(formData) {
3928
+ return new Proxy(formData, {
3929
+ get: function(target, name) {
3930
+ if (typeof name === 'symbol') {
3931
+ // Forward symbol calls to the FormData itself directly
3932
+ return Reflect.get(target, name)
3933
+ }
3934
+ if (name === 'toJSON') {
3935
+ // Support JSON.stringify call on proxy
3936
+ return () => Object.fromEntries(formData)
3937
+ }
3938
+ if (name in target) {
3939
+ // Wrap in function with apply to correctly bind the FormData context, as a direct call would result in an illegal invocation error
3940
+ if (typeof target[name] === 'function') {
3941
+ return function() {
3942
+ return formData[name].apply(formData, arguments)
3943
+ }
3944
+ } else {
3945
+ return target[name]
3946
+ }
3947
+ }
3948
+ const array = formData.getAll(name)
3949
+ // Those 2 undefined & single value returns are for retro-compatibility as we weren't using FormData before
3950
+ if (array.length === 0) {
3951
+ return undefined
3952
+ } else if (array.length === 1) {
3953
+ return array[0]
3954
+ } else {
3955
+ return formDataArrayProxy(target, name, array)
3956
+ }
3957
+ },
3958
+ set: function(target, name, value) {
3959
+ if (typeof name !== 'string') {
3960
+ return false
3961
+ }
3962
+ target.delete(name)
3963
+ if (typeof value.forEach === 'function') {
3964
+ value.forEach(function(v) { target.append(name, v) })
3965
+ } else {
3966
+ target.append(name, value)
3967
+ }
3968
+ return true
3969
+ },
3970
+ deleteProperty: function(target, name) {
3971
+ if (typeof name === 'string') {
3972
+ target.delete(name)
3973
+ }
3974
+ return true
3975
+ },
3976
+ // Support Object.assign call from proxy
3977
+ ownKeys: function(target) {
3978
+ return Reflect.ownKeys(Object.fromEntries(target))
3979
+ },
3980
+ getOwnPropertyDescriptor: function(target, prop) {
3981
+ return Reflect.getOwnPropertyDescriptor(Object.fromEntries(target), prop)
3982
+ }
3983
+ })
3984
+ }
3985
+
3986
+ /**
3987
+ * @param {HttpVerb} verb
3988
+ * @param {string} path
3989
+ * @param {Element} elt
3990
+ * @param {Event} event
3991
+ * @param {HtmxAjaxEtc} [etc]
3992
+ * @param {boolean} [confirmed]
3993
+ * @return {Promise<void>}
3994
+ */
3995
+ function issueAjaxRequest(verb, path, elt, event, etc, confirmed) {
2737
3996
  let resolve = null
2738
3997
  let reject = null
2739
3998
  etc = etc != null ? etc : {}
2740
3999
  if (etc.returnPromise && typeof Promise !== 'undefined') {
2741
- var promise = new Promise(function (_resolve, _reject) {
4000
+ var promise = new Promise(function(_resolve, _reject) {
2742
4001
  resolve = _resolve
2743
4002
  reject = _reject
2744
4003
  })
@@ -2754,7 +4013,7 @@ const htmx = (function () {
2754
4013
  maybeCall(resolve)
2755
4014
  return promise
2756
4015
  }
2757
- const target = etc.targetOverride || getTarget(elt)
4016
+ const target = etc.targetOverride || asElement(getTarget(elt))
2758
4017
  if (target == null || target == DUMMY_ELT) {
2759
4018
  triggerErrorEvent(elt, 'htmx:targetError', { target: getAttributeValue(elt, 'hx-target') })
2760
4019
  maybeCall(reject)
@@ -2774,7 +4033,7 @@ const htmx = (function () {
2774
4033
  if (buttonVerb != null) {
2775
4034
  // ignore buttons with formmethod="dialog"
2776
4035
  if (buttonVerb.toLowerCase() !== 'dialog') {
2777
- verb = buttonVerb
4036
+ verb = (/** @type HttpVerb */(buttonVerb))
2778
4037
  }
2779
4038
  }
2780
4039
  }
@@ -2782,7 +4041,7 @@ const htmx = (function () {
2782
4041
  const confirmQuestion = getClosestAttributeValue(elt, 'hx-confirm')
2783
4042
  // allow event-based confirmation w/ a callback
2784
4043
  if (confirmed === undefined) {
2785
- const issueRequest = function (skipConfirmation) {
4044
+ const issueRequest = function(skipConfirmation) {
2786
4045
  return issueAjaxRequest(verb, path, elt, event, etc, !!skipConfirmation)
2787
4046
  }
2788
4047
  const confirmDetails = { target, elt, path, verb, triggeringEvent: event, etc, issueRequest, question: confirmQuestion }
@@ -2802,7 +4061,7 @@ const htmx = (function () {
2802
4061
  if (selector === 'this') {
2803
4062
  syncElt = findThisElement(elt, 'hx-sync')
2804
4063
  } else {
2805
- syncElt = querySelectorExt(elt, selector)
4064
+ syncElt = asElement(querySelectorExt(elt, selector))
2806
4065
  }
2807
4066
  // default to the drop strategy
2808
4067
  syncStrategy = (syncStrings[1] || 'drop').trim()
@@ -2844,16 +4103,16 @@ const htmx = (function () {
2844
4103
  eltData.queuedRequests = []
2845
4104
  }
2846
4105
  if (queueStrategy === 'first' && eltData.queuedRequests.length === 0) {
2847
- eltData.queuedRequests.push(function () {
4106
+ eltData.queuedRequests.push(function() {
2848
4107
  issueAjaxRequest(verb, path, elt, event, etc)
2849
4108
  })
2850
4109
  } else if (queueStrategy === 'all') {
2851
- eltData.queuedRequests.push(function () {
4110
+ eltData.queuedRequests.push(function() {
2852
4111
  issueAjaxRequest(verb, path, elt, event, etc)
2853
4112
  })
2854
4113
  } else if (queueStrategy === 'last') {
2855
4114
  eltData.queuedRequests = [] // dump existing queue
2856
- eltData.queuedRequests.push(function () {
4115
+ eltData.queuedRequests.push(function() {
2857
4116
  issueAjaxRequest(verb, path, elt, event, etc)
2858
4117
  })
2859
4118
  }
@@ -2865,7 +4124,7 @@ const htmx = (function () {
2865
4124
  const xhr = new XMLHttpRequest()
2866
4125
  eltData.xhr = xhr
2867
4126
  eltData.abortable = abortable
2868
- const endRequestLock = function () {
4127
+ const endRequestLock = function() {
2869
4128
  eltData.xhr = null
2870
4129
  eltData.abortable = false
2871
4130
  if (eltData.queuedRequests != null &&
@@ -2905,16 +4164,16 @@ const htmx = (function () {
2905
4164
  }
2906
4165
  const results = getInputValues(elt, verb)
2907
4166
  let errors = results.errors
2908
- let rawParameters = results.values
4167
+ const rawFormData = results.formData
2909
4168
  if (etc.values) {
2910
- rawParameters = mergeObjects(rawParameters, etc.values)
4169
+ overrideFormData(rawFormData, formDataFromObject(etc.values))
2911
4170
  }
2912
4171
  const expressionVars = getExpressionVars(elt)
2913
- const allParameters = mergeObjects(rawParameters, expressionVars)
2914
- let filteredParameters = filterValues(allParameters, elt)
4172
+ const allFormData = overrideFormData(rawFormData, expressionVars)
4173
+ let filteredFormData = filterValues(allFormData, elt)
2915
4174
 
2916
4175
  if (htmx.config.getCacheBusterParam && verb === 'get') {
2917
- filteredParameters['org.htmx.cache-buster'] = getRawAttribute(target, 'id') || 'true'
4176
+ filteredFormData.set('org.htmx.cache-buster', getRawAttribute(target, 'id') || 'true')
2918
4177
  }
2919
4178
 
2920
4179
  // behavior of anchors w/ empty href is to use the current URL
@@ -2922,17 +4181,26 @@ const htmx = (function () {
2922
4181
  path = getDocument().location.href
2923
4182
  }
2924
4183
 
4184
+ /**
4185
+ * @type {Object}
4186
+ * @property {boolean} [credentials]
4187
+ * @property {number} [timeout]
4188
+ * @property {boolean} [noHeaders]
4189
+ */
2925
4190
  const requestAttrValues = getValuesForElement(elt, 'hx-request')
2926
4191
 
2927
4192
  const eltIsBoosted = getInternalData(elt).boosted
2928
4193
 
2929
4194
  let useUrlParams = htmx.config.methodsThatUseUrlParams.indexOf(verb) >= 0
2930
4195
 
4196
+ /** @type HtmxRequestConfig */
2931
4197
  const requestConfig = {
2932
4198
  boosted: eltIsBoosted,
2933
4199
  useUrlParams,
2934
- parameters: filteredParameters,
2935
- unfilteredParameters: allParameters,
4200
+ formData: filteredFormData,
4201
+ parameters: formDataProxy(filteredFormData),
4202
+ unfilteredFormData: allFormData,
4203
+ unfilteredParameters: formDataProxy(allFormData),
2936
4204
  headers,
2937
4205
  target,
2938
4206
  verb,
@@ -2953,7 +4221,7 @@ const htmx = (function () {
2953
4221
  path = requestConfig.path
2954
4222
  verb = requestConfig.verb
2955
4223
  headers = requestConfig.headers
2956
- filteredParameters = requestConfig.parameters
4224
+ filteredFormData = formDataFromObject(requestConfig.parameters)
2957
4225
  errors = requestConfig.errors
2958
4226
  useUrlParams = requestConfig.useUrlParams
2959
4227
 
@@ -2971,14 +4239,14 @@ const htmx = (function () {
2971
4239
  let finalPath = path
2972
4240
  if (useUrlParams) {
2973
4241
  finalPath = pathNoAnchor
2974
- const values = Object.keys(filteredParameters).length !== 0
2975
- if (values) {
4242
+ const hasValues = !filteredFormData.keys().next().done
4243
+ if (hasValues) {
2976
4244
  if (finalPath.indexOf('?') < 0) {
2977
4245
  finalPath += '?'
2978
4246
  } else {
2979
4247
  finalPath += '&'
2980
4248
  }
2981
- finalPath += urlEncode(filteredParameters)
4249
+ finalPath += urlEncode(filteredFormData)
2982
4250
  if (anchor) {
2983
4251
  finalPath += '#' + anchor
2984
4252
  }
@@ -2989,7 +4257,7 @@ const htmx = (function () {
2989
4257
  triggerErrorEvent(elt, 'htmx:invalidPath', requestConfig)
2990
4258
  maybeCall(reject)
2991
4259
  return promise
2992
- };
4260
+ }
2993
4261
 
2994
4262
  xhr.open(verb.toUpperCase(), finalPath, true)
2995
4263
  xhr.overrideMimeType('text/html')
@@ -3008,6 +4276,7 @@ const htmx = (function () {
3008
4276
  }
3009
4277
  }
3010
4278
 
4279
+ /** @type {HtmxResponseInfo} */
3011
4280
  const responseInfo = {
3012
4281
  xhr,
3013
4282
  target,
@@ -3018,11 +4287,12 @@ const htmx = (function () {
3018
4287
  pathInfo: {
3019
4288
  requestPath: path,
3020
4289
  finalRequestPath: finalPath,
4290
+ responsePath: null,
3021
4291
  anchor
3022
4292
  }
3023
4293
  }
3024
4294
 
3025
- xhr.onload = function () {
4295
+ xhr.onload = function() {
3026
4296
  try {
3027
4297
  const hierarchy = hierarchyForElt(elt)
3028
4298
  responseInfo.pathInfo.responsePath = getPathFromResponse(xhr)
@@ -3052,21 +4322,21 @@ const htmx = (function () {
3052
4322
  throw e
3053
4323
  }
3054
4324
  }
3055
- xhr.onerror = function () {
4325
+ xhr.onerror = function() {
3056
4326
  removeRequestIndicators(indicators, disableElts)
3057
4327
  triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo)
3058
4328
  triggerErrorEvent(elt, 'htmx:sendError', responseInfo)
3059
4329
  maybeCall(reject)
3060
4330
  endRequestLock()
3061
4331
  }
3062
- xhr.onabort = function () {
4332
+ xhr.onabort = function() {
3063
4333
  removeRequestIndicators(indicators, disableElts)
3064
4334
  triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo)
3065
4335
  triggerErrorEvent(elt, 'htmx:sendAbort', responseInfo)
3066
4336
  maybeCall(reject)
3067
4337
  endRequestLock()
3068
4338
  }
3069
- xhr.ontimeout = function () {
4339
+ xhr.ontimeout = function() {
3070
4340
  removeRequestIndicators(indicators, disableElts)
3071
4341
  triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo)
3072
4342
  triggerErrorEvent(elt, 'htmx:timeout', responseInfo)
@@ -3081,9 +4351,9 @@ const htmx = (function () {
3081
4351
  var indicators = addRequestIndicatorClasses(elt)
3082
4352
  var disableElts = disableElements(elt)
3083
4353
 
3084
- forEach(['loadstart', 'loadend', 'progress', 'abort'], function (eventName) {
3085
- forEach([xhr, xhr.upload], function (target) {
3086
- target.addEventListener(eventName, function (event) {
4354
+ forEach(['loadstart', 'loadend', 'progress', 'abort'], function(eventName) {
4355
+ forEach([xhr, xhr.upload], function(target) {
4356
+ target.addEventListener(eventName, function(event) {
3087
4357
  triggerEvent(elt, 'htmx:xhr:' + eventName, {
3088
4358
  lengthComputable: event.lengthComputable,
3089
4359
  loaded: event.loaded,
@@ -3093,12 +4363,23 @@ const htmx = (function () {
3093
4363
  })
3094
4364
  })
3095
4365
  triggerEvent(elt, 'htmx:beforeSend', responseInfo)
3096
- const params = useUrlParams ? null : encodeParamsForBody(xhr, elt, filteredParameters)
4366
+ const params = useUrlParams ? null : encodeParamsForBody(xhr, elt, filteredFormData)
3097
4367
  xhr.send(params)
3098
4368
  return promise
3099
4369
  }
3100
4370
 
3101
- function determineHistoryUpdates (elt, responseInfo) {
4371
+ /**
4372
+ * @typedef {Object} HtmxHistoryUpdate
4373
+ * @property {string|null} [type]
4374
+ * @property {string|null} [path]
4375
+ */
4376
+
4377
+ /**
4378
+ * @param {Element} elt
4379
+ * @param {HtmxResponseInfo} responseInfo
4380
+ * @return {HtmxHistoryUpdate}
4381
+ */
4382
+ function determineHistoryUpdates(elt, responseInfo) {
3102
4383
  const xhr = responseInfo.xhr
3103
4384
 
3104
4385
  //= ==========================================
@@ -3165,8 +4446,7 @@ const htmx = (function () {
3165
4446
  }
3166
4447
 
3167
4448
  // restore any anchor associated with the request
3168
- if (responseInfo.pathInfo.anchor &&
3169
- path.indexOf('#') === -1) {
4449
+ if (responseInfo.pathInfo.anchor && path.indexOf('#') === -1) {
3170
4450
  path = path + '#' + responseInfo.pathInfo.anchor
3171
4451
  }
3172
4452
 
@@ -3179,30 +4459,76 @@ const htmx = (function () {
3179
4459
  }
3180
4460
  }
3181
4461
 
3182
- function handleAjaxResponse (elt, responseInfo) {
4462
+ /**
4463
+ * @param {HtmxResponseHandlingConfig} responseHandlingConfig
4464
+ * @param {number} status
4465
+ * @return {boolean}
4466
+ */
4467
+ function codeMatches(responseHandlingConfig, status) {
4468
+ var regExp = new RegExp(responseHandlingConfig.code)
4469
+ return regExp.test(status.toString(10))
4470
+ }
4471
+
4472
+ /**
4473
+ * @param {XMLHttpRequest} xhr
4474
+ * @return {HtmxResponseHandlingConfig}
4475
+ */
4476
+ function resolveResponseHandling(xhr) {
4477
+ for (var i = 0; i < htmx.config.responseHandling.length; i++) {
4478
+ /** @type HtmxResponseHandlingConfig */
4479
+ var responseHandlingElement = htmx.config.responseHandling[i]
4480
+ if (codeMatches(responseHandlingElement, xhr.status)) {
4481
+ return responseHandlingElement
4482
+ }
4483
+ }
4484
+ // no matches, return no swap
4485
+ return {
4486
+ swap: false
4487
+ }
4488
+ }
4489
+
4490
+ /**
4491
+ * @param {string} title
4492
+ */
4493
+ function handleTitle(title) {
4494
+ if (title) {
4495
+ const titleElt = find('title')
4496
+ if (titleElt) {
4497
+ titleElt.innerHTML = title
4498
+ } else {
4499
+ window.document.title = title
4500
+ }
4501
+ }
4502
+ }
4503
+
4504
+ /**
4505
+ * @param {Element} elt
4506
+ * @param {HtmxResponseInfo} responseInfo
4507
+ */
4508
+ function handleAjaxResponse(elt, responseInfo) {
3183
4509
  const xhr = responseInfo.xhr
3184
4510
  let target = responseInfo.target
3185
4511
  const etc = responseInfo.etc
3186
- const requestConfig = responseInfo.requestConfig
3187
- const select = responseInfo.select
4512
+ const responseInfoSelect = responseInfo.select
3188
4513
 
3189
4514
  if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return
3190
4515
 
3191
4516
  if (hasHeader(xhr, /HX-Trigger:/i)) {
3192
- handleTrigger(xhr, 'HX-Trigger', elt)
4517
+ handleTriggerHeader(xhr, 'HX-Trigger', elt)
3193
4518
  }
3194
4519
 
3195
4520
  if (hasHeader(xhr, /HX-Location:/i)) {
3196
4521
  saveCurrentPageToHistory()
3197
4522
  let redirectPath = xhr.getResponseHeader('HX-Location')
3198
- var swapSpec
4523
+ /** @type {HtmxAjaxHelperContext&{path:string}} */
4524
+ var redirectSwapSpec
3199
4525
  if (redirectPath.indexOf('{') === 0) {
3200
- swapSpec = parseJSON(redirectPath)
4526
+ redirectSwapSpec = parseJSON(redirectPath)
3201
4527
  // what's the best way to throw an error if the user didn't include this
3202
- redirectPath = swapSpec.path
3203
- delete swapSpec.path
4528
+ redirectPath = redirectSwapSpec.path
4529
+ delete redirectSwapSpec.path
3204
4530
  }
3205
- ajaxHelper('GET', redirectPath, swapSpec).then(function () {
4531
+ ajaxHelper('get', redirectPath, redirectSwapSpec).then(function() {
3206
4532
  pushUrlIntoHistory(redirectPath)
3207
4533
  })
3208
4534
  return
@@ -3225,27 +4551,56 @@ const htmx = (function () {
3225
4551
  if (xhr.getResponseHeader('HX-Retarget') === 'this') {
3226
4552
  responseInfo.target = elt
3227
4553
  } else {
3228
- responseInfo.target = querySelectorExt(elt, xhr.getResponseHeader('HX-Retarget'))
4554
+ responseInfo.target = asElement(querySelectorExt(elt, xhr.getResponseHeader('HX-Retarget')))
3229
4555
  }
3230
4556
  }
3231
4557
 
3232
4558
  const historyUpdate = determineHistoryUpdates(elt, responseInfo)
3233
4559
 
3234
- // by default htmx only swaps on 200 return codes and does not swap
3235
- // on 204 'No Content'
3236
- // this can be ovverriden by responding to the htmx:beforeSwap event and
3237
- // overriding the detail.shouldSwap property
3238
- const shouldSwap = xhr.status >= 200 && xhr.status < 400 && xhr.status !== 204
3239
- let serverResponse = xhr.response
3240
- let isError = xhr.status >= 400
3241
- let ignoreTitle = htmx.config.ignoreTitle
3242
- const beforeSwapDetails = mergeObjects({ shouldSwap, serverResponse, isError, ignoreTitle }, responseInfo)
4560
+ const responseHandling = resolveResponseHandling(xhr)
4561
+ const shouldSwap = responseHandling.swap
4562
+ let isError = !!responseHandling.error
4563
+ let ignoreTitle = htmx.config.ignoreTitle || responseHandling.ignoreTitle
4564
+ let selectOverride = responseHandling.select
4565
+ if (responseHandling.target) {
4566
+ responseInfo.target = asElement(querySelectorExt(elt, responseHandling.target))
4567
+ }
4568
+ var swapOverride = etc.swapOverride
4569
+ if (swapOverride == null && responseHandling.swapOverride) {
4570
+ swapOverride = responseHandling.swapOverride
4571
+ }
4572
+
4573
+ // response headers override response handling config
4574
+ if (hasHeader(xhr, /HX-Retarget:/i)) {
4575
+ if (xhr.getResponseHeader('HX-Retarget') === 'this') {
4576
+ responseInfo.target = elt
4577
+ } else {
4578
+ responseInfo.target = asElement(querySelectorExt(elt, xhr.getResponseHeader('HX-Retarget')))
4579
+ }
4580
+ }
4581
+ if (hasHeader(xhr, /HX-Reswap:/i)) {
4582
+ swapOverride = xhr.getResponseHeader('HX-Reswap')
4583
+ }
4584
+
4585
+ var serverResponse = xhr.response
4586
+ /** @type HtmxBeforeSwapDetails */
4587
+ var beforeSwapDetails = mergeObjects({
4588
+ shouldSwap,
4589
+ serverResponse,
4590
+ isError,
4591
+ ignoreTitle,
4592
+ selectOverride
4593
+ }, responseInfo)
4594
+
4595
+ if (responseHandling.event && !triggerEvent(target, responseHandling.event, beforeSwapDetails)) return
4596
+
3243
4597
  if (!triggerEvent(target, 'htmx:beforeSwap', beforeSwapDetails)) return
3244
4598
 
3245
4599
  target = beforeSwapDetails.target // allow re-targeting
3246
4600
  serverResponse = beforeSwapDetails.serverResponse // allow updating content
3247
4601
  isError = beforeSwapDetails.isError // allow updating error
3248
4602
  ignoreTitle = beforeSwapDetails.ignoreTitle // allow updating ignoring title
4603
+ selectOverride = beforeSwapDetails.selectOverride // allow updating select override
3249
4604
 
3250
4605
  responseInfo.target = target // Make updated target available to response events
3251
4606
  responseInfo.failed = isError // Make failed property available to response events
@@ -3256,7 +4611,7 @@ const htmx = (function () {
3256
4611
  cancelPolling(elt)
3257
4612
  }
3258
4613
 
3259
- withExtensions(elt, function (extension) {
4614
+ withExtensions(elt, function(extension) {
3260
4615
  serverResponse = extension.transformResponse(serverResponse, xhr, elt)
3261
4616
  })
3262
4617
 
@@ -3265,14 +4620,13 @@ const htmx = (function () {
3265
4620
  saveCurrentPageToHistory()
3266
4621
  }
3267
4622
 
3268
- let swapOverride = etc.swapOverride
3269
4623
  if (hasHeader(xhr, /HX-Reswap:/i)) {
3270
4624
  swapOverride = xhr.getResponseHeader('HX-Reswap')
3271
4625
  }
3272
4626
  var swapSpec = getSwapSpecification(elt, swapOverride)
3273
4627
 
3274
- if (swapSpec.hasOwnProperty('ignoreTitle')) {
3275
- ignoreTitle = swapSpec.ignoreTitle
4628
+ if (!swapSpec.hasOwnProperty('ignoreTitle')) {
4629
+ swapSpec.ignoreTitle = ignoreTitle
3276
4630
  }
3277
4631
 
3278
4632
  target.classList.add(htmx.config.swappingClass)
@@ -3281,31 +4635,19 @@ const htmx = (function () {
3281
4635
  let settleResolve = null
3282
4636
  let settleReject = null
3283
4637
 
3284
- let doSwap = function () {
3285
- try {
3286
- const activeElt = document.activeElement
3287
- let selectionInfo = {}
3288
- try {
3289
- selectionInfo = {
3290
- elt: activeElt,
3291
- // @ts-ignore
3292
- start: activeElt ? activeElt.selectionStart : null,
3293
- // @ts-ignore
3294
- end: activeElt ? activeElt.selectionEnd : null
3295
- }
3296
- } catch (e) {
3297
- // safari issue - see https://github.com/microsoft/playwright/issues/5894
3298
- }
4638
+ if (responseInfoSelect) {
4639
+ selectOverride = responseInfoSelect
4640
+ }
3299
4641
 
3300
- let selectOverride
3301
- if (select) {
3302
- selectOverride = select
3303
- }
4642
+ if (hasHeader(xhr, /HX-Reselect:/i)) {
4643
+ selectOverride = xhr.getResponseHeader('HX-Reselect')
4644
+ }
3304
4645
 
3305
- if (hasHeader(xhr, /HX-Reselect:/i)) {
3306
- selectOverride = xhr.getResponseHeader('HX-Reselect')
3307
- }
4646
+ const selectOOB = getClosestAttributeValue(elt, 'hx-select-oob')
4647
+ const select = getClosestAttributeValue(elt, 'hx-select')
3308
4648
 
4649
+ let doSwap = function() {
4650
+ try {
3309
4651
  // if we need to save history, do so, before swapping so that relative resources have the correct base URL
3310
4652
  if (historyUpdate.type) {
3311
4653
  triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo))
@@ -3318,88 +4660,32 @@ const htmx = (function () {
3318
4660
  }
3319
4661
  }
3320
4662
 
3321
- const settleInfo = makeSettleInfo(target)
3322
- selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, selectOverride)
3323
-
3324
- if (selectionInfo.elt &&
3325
- !bodyContains(selectionInfo.elt) &&
3326
- getRawAttribute(selectionInfo.elt, 'id')) {
3327
- const newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, 'id'))
3328
- const focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }
3329
- if (newActiveElt) {
3330
- // @ts-ignore
3331
- if (selectionInfo.start && newActiveElt.setSelectionRange) {
3332
- // @ts-ignore
3333
- try {
3334
- newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end)
3335
- } catch (e) {
3336
- // the setSelectionRange method is present on fields that don't support it, so just let this fail
4663
+ swap(target, serverResponse, swapSpec, {
4664
+ select: selectOverride || select,
4665
+ selectOOB,
4666
+ eventInfo: responseInfo,
4667
+ anchor: responseInfo.pathInfo.anchor,
4668
+ contextElement: elt,
4669
+ afterSwapCallback: function() {
4670
+ if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
4671
+ let finalElt = elt
4672
+ if (!bodyContains(elt)) {
4673
+ finalElt = getDocument().body
3337
4674
  }
4675
+ handleTriggerHeader(xhr, 'HX-Trigger-After-Swap', finalElt)
3338
4676
  }
3339
- newActiveElt.focus(focusOptions)
3340
- }
3341
- }
3342
-
3343
- target.classList.remove(htmx.config.swappingClass)
3344
- forEach(settleInfo.elts, function (elt) {
3345
- if (elt.classList) {
3346
- elt.classList.add(htmx.config.settlingClass)
3347
- }
3348
- triggerEvent(elt, 'htmx:afterSwap', responseInfo)
3349
- })
3350
-
3351
- if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
3352
- let finalElt = elt
3353
- if (!bodyContains(elt)) {
3354
- finalElt = getDocument().body
3355
- }
3356
- handleTrigger(xhr, 'HX-Trigger-After-Swap', finalElt)
3357
- }
3358
-
3359
- const doSettle = function () {
3360
- forEach(settleInfo.tasks, function (task) {
3361
- task.call()
3362
- })
3363
- forEach(settleInfo.elts, function (elt) {
3364
- if (elt.classList) {
3365
- elt.classList.remove(htmx.config.settlingClass)
3366
- }
3367
- triggerEvent(elt, 'htmx:afterSettle', responseInfo)
3368
- })
3369
-
3370
- if (responseInfo.pathInfo.anchor) {
3371
- const anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor)
3372
- if (anchorTarget) {
3373
- anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
3374
- }
3375
- }
3376
-
3377
- if (settleInfo.title && !ignoreTitle) {
3378
- const titleElt = find('title')
3379
- if (titleElt) {
3380
- titleElt.innerHTML = settleInfo.title
3381
- } else {
3382
- window.document.title = settleInfo.title
3383
- }
3384
- }
3385
-
3386
- updateScrollState(settleInfo.elts, swapSpec)
3387
-
3388
- if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
3389
- let finalElt = elt
3390
- if (!bodyContains(elt)) {
3391
- finalElt = getDocument().body
4677
+ },
4678
+ afterSettleCallback: function() {
4679
+ if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
4680
+ let finalElt = elt
4681
+ if (!bodyContains(elt)) {
4682
+ finalElt = getDocument().body
4683
+ }
4684
+ handleTriggerHeader(xhr, 'HX-Trigger-After-Settle', finalElt)
3392
4685
  }
3393
- handleTrigger(xhr, 'HX-Trigger-After-Settle', finalElt)
4686
+ maybeCall(settleResolve)
3394
4687
  }
3395
- maybeCall(settleResolve)
3396
- }
3397
-
3398
- if (swapSpec.settleDelay > 0) {
3399
- setTimeout(doSettle, swapSpec.settleDelay)
3400
- } else {
3401
- doSettle()
3402
- }
4688
+ })
3403
4689
  } catch (e) {
3404
4690
  triggerErrorEvent(elt, 'htmx:swapError', responseInfo)
3405
4691
  maybeCall(settleReject)
@@ -3413,16 +4699,19 @@ const htmx = (function () {
3413
4699
  }
3414
4700
 
3415
4701
  if (shouldTransition &&
3416
- triggerEvent(elt, 'htmx:beforeTransition', responseInfo) &&
3417
- typeof Promise !== 'undefined' && document.startViewTransition) {
3418
- const settlePromise = new Promise(function (_resolve, _reject) {
4702
+ triggerEvent(elt, 'htmx:beforeTransition', responseInfo) &&
4703
+ typeof Promise !== 'undefined' &&
4704
+ // @ts-ignore experimental feature atm
4705
+ document.startViewTransition) {
4706
+ const settlePromise = new Promise(function(_resolve, _reject) {
3419
4707
  settleResolve = _resolve
3420
4708
  settleReject = _reject
3421
4709
  })
3422
4710
  // wrap the original doSwap() in a call to startViewTransition()
3423
4711
  const innerDoSwap = doSwap
3424
- doSwap = function () {
3425
- document.startViewTransition(function () {
4712
+ doSwap = function() {
4713
+ // @ts-ignore experimental feature atm
4714
+ document.startViewTransition(function() {
3426
4715
  innerDoSwap()
3427
4716
  return settlePromise
3428
4717
  })
@@ -3430,7 +4719,7 @@ const htmx = (function () {
3430
4719
  }
3431
4720
 
3432
4721
  if (swapSpec.swapDelay > 0) {
3433
- setTimeout(doSwap, swapSpec.swapDelay)
4722
+ getWindow().setTimeout(doSwap, swapSpec.swapDelay)
3434
4723
  } else {
3435
4724
  doSwap()
3436
4725
  }
@@ -3444,31 +4733,33 @@ const htmx = (function () {
3444
4733
  // Extensions API
3445
4734
  //= ===================================================================
3446
4735
 
3447
- /** @type {Object<string, import("./htmx").HtmxExtension>} */
4736
+ /** @type {Object<string, HtmxExtension>} */
3448
4737
  const extensions = {}
3449
4738
 
3450
4739
  /**
3451
- * extensionBase defines the default functions for all extensions.
3452
- * @returns {import("./htmx").HtmxExtension}
3453
- */
3454
- function extensionBase () {
4740
+ * extensionBase defines the default functions for all extensions.
4741
+ * @returns {HtmxExtension}
4742
+ */
4743
+ function extensionBase() {
3455
4744
  return {
3456
- init: function (api) { return null },
3457
- onEvent: function (name, evt) { return true },
3458
- transformResponse: function (text, xhr, elt) { return text },
3459
- isInlineSwap: function (swapStyle) { return false },
3460
- handleSwap: function (swapStyle, target, fragment, settleInfo) { return false },
3461
- encodeParameters: function (xhr, parameters, elt) { return null }
4745
+ init: function(api) { return null },
4746
+ onEvent: function(name, evt) { return true },
4747
+ transformResponse: function(text, xhr, elt) { return text },
4748
+ isInlineSwap: function(swapStyle) { return false },
4749
+ handleSwap: function(swapStyle, target, fragment, settleInfo) { return false },
4750
+ encodeParameters: function(xhr, parameters, elt) { return null }
3462
4751
  }
3463
4752
  }
3464
4753
 
3465
4754
  /**
3466
- * defineExtension initializes the extension and adds it to the htmx registry
3467
- *
3468
- * @param {string} name
3469
- * @param {import("./htmx").HtmxExtension} extension
3470
- */
3471
- function defineExtension (name, extension) {
4755
+ * defineExtension initializes the extension and adds it to the htmx registry
4756
+ *
4757
+ * @see https://htmx.org/api/#defineExtension
4758
+ *
4759
+ * @param {string} name the extension name
4760
+ * @param {HtmxExtension} extension the extension definition
4761
+ */
4762
+ function defineExtension(name, extension) {
3472
4763
  if (extension.init) {
3473
4764
  extension.init(internalAPI)
3474
4765
  }
@@ -3476,34 +4767,37 @@ const htmx = (function () {
3476
4767
  }
3477
4768
 
3478
4769
  /**
3479
- * removeExtension removes an extension from the htmx registry
3480
- *
3481
- * @param {string} name
3482
- */
3483
- function removeExtension (name) {
4770
+ * removeExtension removes an extension from the htmx registry
4771
+ *
4772
+ * @see https://htmx.org/api/#removeExtension
4773
+ *
4774
+ * @param {string} name
4775
+ */
4776
+ function removeExtension(name) {
3484
4777
  delete extensions[name]
3485
4778
  }
3486
4779
 
3487
4780
  /**
3488
- * getExtensions searches up the DOM tree to return all extensions that can be applied to a given element
3489
- *
3490
- * @param {HTMLElement} elt
3491
- * @param {import("./htmx").HtmxExtension[]=} extensionsToReturn
3492
- * @param {import("./htmx").HtmxExtension[]=} extensionsToIgnore
3493
- */
3494
- function getExtensions (elt, extensionsToReturn, extensionsToIgnore) {
3495
- if (elt == undefined) {
3496
- return extensionsToReturn
3497
- }
4781
+ * getExtensions searches up the DOM tree to return all extensions that can be applied to a given element
4782
+ *
4783
+ * @param {Element} elt
4784
+ * @param {HtmxExtension[]=} extensionsToReturn
4785
+ * @param {string[]=} extensionsToIgnore
4786
+ * @returns {HtmxExtension[]}
4787
+ */
4788
+ function getExtensions(elt, extensionsToReturn, extensionsToIgnore) {
3498
4789
  if (extensionsToReturn == undefined) {
3499
4790
  extensionsToReturn = []
3500
4791
  }
4792
+ if (elt == undefined) {
4793
+ return extensionsToReturn
4794
+ }
3501
4795
  if (extensionsToIgnore == undefined) {
3502
4796
  extensionsToIgnore = []
3503
4797
  }
3504
4798
  const extensionsForElement = getAttributeValue(elt, 'hx-ext')
3505
4799
  if (extensionsForElement) {
3506
- forEach(extensionsForElement.split(','), function (extensionName) {
4800
+ forEach(extensionsForElement.split(','), function(extensionName) {
3507
4801
  extensionName = extensionName.replace(/ /g, '')
3508
4802
  if (extensionName.slice(0, 7) == 'ignore:') {
3509
4803
  extensionsToIgnore.push(extensionName.slice(7))
@@ -3517,43 +4811,35 @@ const htmx = (function () {
3517
4811
  }
3518
4812
  })
3519
4813
  }
3520
- return getExtensions(parentElt(elt), extensionsToReturn, extensionsToIgnore)
4814
+ return getExtensions(asElement(parentElt(elt)), extensionsToReturn, extensionsToIgnore)
3521
4815
  }
3522
4816
 
3523
4817
  //= ===================================================================
3524
4818
  // Initialization
3525
4819
  //= ===================================================================
3526
- /**
3527
- * We want to initialize the page elements after DOMContentLoaded
3528
- * fires, but there isn't always a good way to tell whether
3529
- * it has already fired when we get here or not.
3530
- */
3531
- function ready (functionToCall) {
3532
- // call the function exactly once no matter how many times this is called
3533
- const callReadyFunction = function () {
3534
- if (!functionToCall) return
3535
- functionToCall()
3536
- functionToCall = null
3537
- }
4820
+ var isReady = false
4821
+ getDocument().addEventListener('DOMContentLoaded', function() {
4822
+ isReady = true
4823
+ })
3538
4824
 
3539
- if (getDocument().readyState === 'complete') {
3540
- // DOMContentLoaded definitely fired, we can initialize the page
3541
- callReadyFunction()
4825
+ /**
4826
+ * Execute a function now if DOMContentLoaded has fired, otherwise listen for it.
4827
+ *
4828
+ * This function uses isReady because there is no reliable way to ask the browser whether
4829
+ * the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded
4830
+ * firing and readystate=complete.
4831
+ */
4832
+ function ready(fn) {
4833
+ // Checking readyState here is a failsafe in case the htmx script tag entered the DOM by
4834
+ // some means other than the initial page load.
4835
+ if (isReady || getDocument().readyState === 'complete') {
4836
+ fn()
3542
4837
  } else {
3543
- /* DOMContentLoaded *maybe* already fired, wait for
3544
- * the next DOMContentLoaded or readystatechange event
3545
- */
3546
- getDocument().addEventListener('DOMContentLoaded', function () {
3547
- callReadyFunction()
3548
- })
3549
- getDocument().addEventListener('readystatechange', function () {
3550
- if (getDocument().readyState !== 'complete') return
3551
- callReadyFunction()
3552
- })
4838
+ getDocument().addEventListener('DOMContentLoaded', fn)
3553
4839
  }
3554
4840
  }
3555
4841
 
3556
- function insertIndicatorStyles () {
4842
+ function insertIndicatorStyles() {
3557
4843
  if (htmx.config.includeIndicatorStyles !== false) {
3558
4844
  getDocument().head.insertAdjacentHTML('beforeend',
3559
4845
  '<style>\
@@ -3564,17 +4850,17 @@ const htmx = (function () {
3564
4850
  }
3565
4851
  }
3566
4852
 
3567
- function getMetaConfig () {
4853
+ function getMetaConfig() {
4854
+ /** @type HTMLMetaElement */
3568
4855
  const element = getDocument().querySelector('meta[name="htmx-config"]')
3569
4856
  if (element) {
3570
- // @ts-ignore
3571
4857
  return parseJSON(element.content)
3572
4858
  } else {
3573
4859
  return null
3574
4860
  }
3575
4861
  }
3576
4862
 
3577
- function mergeMetaConfig () {
4863
+ function mergeMetaConfig() {
3578
4864
  const metaConfig = getMetaConfig()
3579
4865
  if (metaConfig) {
3580
4866
  htmx.config = mergeObjects(htmx.config, metaConfig)
@@ -3582,7 +4868,7 @@ const htmx = (function () {
3582
4868
  }
3583
4869
 
3584
4870
  // initialize the document
3585
- ready(function () {
4871
+ ready(function() {
3586
4872
  mergeMetaConfig()
3587
4873
  insertIndicatorStyles()
3588
4874
  let body = getDocument().body
@@ -3590,7 +4876,7 @@ const htmx = (function () {
3590
4876
  const restoredElts = getDocument().querySelectorAll(
3591
4877
  "[hx-trigger='restored'],[data-hx-trigger='restored']"
3592
4878
  )
3593
- body.addEventListener('htmx:abort', function (evt) {
4879
+ body.addEventListener('htmx:abort', function(evt) {
3594
4880
  const target = evt.target
3595
4881
  const internalData = getInternalData(target)
3596
4882
  if (internalData && internalData.xhr) {
@@ -3600,10 +4886,10 @@ const htmx = (function () {
3600
4886
  /** @type {(ev: PopStateEvent) => any} */
3601
4887
  const originalPopstate = window.onpopstate ? window.onpopstate.bind(window) : null
3602
4888
  /** @type {(ev: PopStateEvent) => any} */
3603
- window.onpopstate = function (event) {
4889
+ window.onpopstate = function(event) {
3604
4890
  if (event.state && event.state.htmx) {
3605
4891
  restoreHistory()
3606
- forEach(restoredElts, function (elt) {
4892
+ forEach(restoredElts, function(elt) {
3607
4893
  triggerEvent(elt, 'htmx:restored', {
3608
4894
  document: getDocument(),
3609
4895
  triggerEvent
@@ -3615,12 +4901,184 @@ const htmx = (function () {
3615
4901
  }
3616
4902
  }
3617
4903
  }
3618
- setTimeout(function () {
4904
+ getWindow().setTimeout(function() {
3619
4905
  triggerEvent(body, 'htmx:load', {}) // give ready handlers a chance to load up before firing this event
3620
4906
  body = null // kill reference for gc
3621
4907
  }, 0)
3622
4908
  })
3623
4909
 
3624
4910
  return htmx
3625
- }
3626
- )()
4911
+ })()
4912
+
4913
+ /** @typedef {'get'|'head'|'post'|'put'|'delete'|'connect'|'options'|'trace'|'patch'} HttpVerb */
4914
+
4915
+ /**
4916
+ * @typedef {Object} SwapOptions
4917
+ * @property {string} [select]
4918
+ * @property {string} [selectOOB]
4919
+ * @property {*} [eventInfo]
4920
+ * @property {string} [anchor]
4921
+ * @property {Element} [contextElement]
4922
+ * @property {swapCallback} [afterSwapCallback]
4923
+ * @property {swapCallback} [afterSettleCallback]
4924
+ */
4925
+
4926
+ /**
4927
+ * @callback swapCallback
4928
+ */
4929
+
4930
+ /**
4931
+ * @typedef {'innerHTML' | 'outerHTML' | 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend' | 'delete' | 'none' | string} HtmxSwapStyle
4932
+ */
4933
+
4934
+ /**
4935
+ * @typedef HtmxSwapSpecification
4936
+ * @property {HtmxSwapStyle} swapStyle
4937
+ * @property {number} swapDelay
4938
+ * @property {number} settleDelay
4939
+ * @property {boolean} [transition]
4940
+ * @property {boolean} [ignoreTitle]
4941
+ * @property {string} [head]
4942
+ * @property {'top' | 'bottom'} [scroll]
4943
+ * @property {string} [scrollTarget]
4944
+ * @property {string} [show]
4945
+ * @property {string} [showTarget]
4946
+ * @property {boolean} [focusScroll]
4947
+ */
4948
+
4949
+ /**
4950
+ * @typedef {((this:Node, evt:Event) => boolean) & {source: string}} ConditionalFunction
4951
+ */
4952
+
4953
+ /**
4954
+ * @typedef {Object} HtmxTriggerSpecification
4955
+ * @property {string} trigger
4956
+ * @property {number} [pollInterval]
4957
+ * @property {ConditionalFunction} [eventFilter]
4958
+ * @property {boolean} [changed]
4959
+ * @property {boolean} [once]
4960
+ * @property {boolean} [consume]
4961
+ * @property {number} [delay]
4962
+ * @property {string} [from]
4963
+ * @property {string} [target]
4964
+ * @property {number} [throttle]
4965
+ * @property {string} [queue]
4966
+ * @property {string} [root]
4967
+ * @property {string} [threshold]
4968
+ */
4969
+
4970
+ /**
4971
+ * @typedef {{elt: Element, message: string, validity: ValidityState}} HtmxElementValidationError
4972
+ */
4973
+
4974
+ /**
4975
+ * @typedef {Record<string, string>} HtmxHeaderSpecification
4976
+ * @property {'true'} HX-Request
4977
+ * @property {string|null} HX-Trigger
4978
+ * @property {string|null} HX-Trigger-Name
4979
+ * @property {string|null} HX-Target
4980
+ * @property {string} HX-Current-URL
4981
+ * @property {string} [HX-Prompt]
4982
+ * @property {'true'} [HX-Boosted]
4983
+ * @property {string} [Content-Type]
4984
+ * @property {'true'} [HX-History-Restore-Request]
4985
+ */
4986
+
4987
+ /** @typedef HtmxAjaxHelperContext
4988
+ * @property {Element|string} [source]
4989
+ * @property {Event} [event]
4990
+ * @property {HtmxAjaxHandler} [handler]
4991
+ * @property {Element|string} target
4992
+ * @property {HtmxSwapStyle} [swap]
4993
+ * @property {Object|FormData} [values]
4994
+ * @property {Record<string,string>} [headers]
4995
+ * @property {string} [select]
4996
+ */
4997
+
4998
+ /**
4999
+ * @typedef {Object} HtmxRequestConfig
5000
+ * @property {boolean} boosted
5001
+ * @property {boolean} useUrlParams
5002
+ * @property {FormData} formData
5003
+ * @property {Object} parameters formData proxy
5004
+ * @property {FormData} unfilteredFormData
5005
+ * @property {Object} unfilteredParameters unfilteredFormData proxy
5006
+ * @property {HtmxHeaderSpecification} headers
5007
+ * @property {Element} target
5008
+ * @property {HttpVerb} verb
5009
+ * @property {HtmxElementValidationError[]} errors
5010
+ * @property {boolean} withCredentials
5011
+ * @property {number} timeout
5012
+ * @property {string} path
5013
+ * @property {Event} triggeringEvent
5014
+ */
5015
+
5016
+ /**
5017
+ * @typedef {Object} HtmxResponseInfo
5018
+ * @property {XMLHttpRequest} xhr
5019
+ * @property {Element} target
5020
+ * @property {HtmxRequestConfig} requestConfig
5021
+ * @property {HtmxAjaxEtc} etc
5022
+ * @property {boolean} boosted
5023
+ * @property {string} select
5024
+ * @property {{requestPath: string, finalRequestPath: string, responsePath: string|null, anchor: string}} pathInfo
5025
+ * @property {boolean} [failed]
5026
+ * @property {boolean} [successful]
5027
+ */
5028
+
5029
+ /**
5030
+ * @typedef {Object} HtmxAjaxEtc
5031
+ * @property {boolean} [returnPromise]
5032
+ * @property {HtmxAjaxHandler} [handler]
5033
+ * @property {string} [select]
5034
+ * @property {Element} [targetOverride]
5035
+ * @property {HtmxSwapStyle} [swapOverride]
5036
+ * @property {Record<string,string>} [headers]
5037
+ * @property {Object|FormData} [values]
5038
+ * @property {boolean} [credentials]
5039
+ * @property {number} [timeout]
5040
+ */
5041
+
5042
+ /**
5043
+ * @typedef {Object} HtmxResponseHandlingConfig
5044
+ * @property {string} [code]
5045
+ * @property {boolean} swap
5046
+ * @property {boolean} [error]
5047
+ * @property {boolean} [ignoreTitle]
5048
+ * @property {string} [select]
5049
+ * @property {string} [target]
5050
+ * @property {string} [swapOverride]
5051
+ * @property {string} [event]
5052
+ */
5053
+
5054
+ /**
5055
+ * @typedef {HtmxResponseInfo & {shouldSwap: boolean, serverResponse: any, isError: boolean, ignoreTitle: boolean, selectOverride:string}} HtmxBeforeSwapDetails
5056
+ */
5057
+
5058
+ /**
5059
+ * @callback HtmxAjaxHandler
5060
+ * @param {Element} elt
5061
+ * @param {HtmxResponseInfo} responseInfo
5062
+ */
5063
+
5064
+ /**
5065
+ * @typedef {(() => void)} HtmxSettleTask
5066
+ */
5067
+
5068
+ /**
5069
+ * @typedef {Object} HtmxSettleInfo
5070
+ * @property {HtmxSettleTask[]} tasks
5071
+ * @property {Element[]} elts
5072
+ * @property {string} [title]
5073
+ */
5074
+
5075
+ /**
5076
+ * @typedef {Object} HtmxExtension
5077
+ * @see https://htmx.org/extensions/#defining
5078
+ * @property {(api: any) => void} init
5079
+ * @property {(name: string, event: Event|CustomEvent) => boolean} onEvent
5080
+ * @property {(text: string, xhr: XMLHttpRequest, elt: Element) => string} transformResponse
5081
+ * @property {(swapStyle: HtmxSwapStyle) => boolean} isInlineSwap
5082
+ * @property {(swapStyle: HtmxSwapStyle, target: Element, fragment: Node, settleInfo: HtmxSettleInfo) => boolean} handleSwap
5083
+ * @property {(xhr: XMLHttpRequest, parameters: FormData, elt: Element) => *|string|null} encodeParameters
5084
+ */