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