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