tangled
alpha
login
or
join now
helpimnotdrowning.net
/
Utatane
0
fork
atom
Nice little directory browser :D
0
fork
atom
overview
issues
pulls
pipelines
upgrade HTMX to 2.0.6
helpimnotdrowning.net
1 day ago
e2d879cb
9fae6eb0
verified
This commit was signed with the committer's
known signature
.
helpimnotdrowning.net
SSH Key Fingerprint:
SHA256:45fjCnhlSb2EUOeKZ4SuV9/Y2aC56k+4iw2+xTdkqHQ=
+483
-436
1 changed file
expand all
collapse all
unified
split
public
htmx.js
+483
-436
public/htmx.js
···
82
*/
83
historyEnabled: true,
84
/**
85
-
* The number of pages to keep in **localStorage** for history support.
86
* @type number
87
* @default 10
88
*/
···
271
* @type boolean
272
* @default true
273
*/
274
-
allowNestedOobSwaps: true
0
0
0
0
0
0
0
275
},
276
/** @type {typeof parseInterval} */
277
parseInterval: null,
0
0
0
0
0
278
/** @type {typeof internalEval} */
279
_: null,
280
-
version: '2.0.4'
281
}
282
// Tsc madness part 2
283
htmx.onLoad = onLoadHelper
···
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
/**
···
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
···
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
}
0
842
}
843
844
//= =========================================================================================
···
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
}
0
1089
}
1090
1091
/**
···
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') {
···
1350
return [findThisElement(elt, attrName)]
1351
} else {
1352
const result = querySelectorAllExt(elt, attrTarget)
0
0
0
0
0
0
0
0
0
0
1353
if (result.length === 0) {
1354
logError('The selector "' + attrTarget + '" on ' + attrName + ' returned no matches!')
1355
return [DUMMY_ELT]
···
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
/**
···
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') {
···
1468
oobElement.removeAttribute('data-hx-swap-oob')
1469
1470
const targets = querySelectorAllExt(rootNode, selector, false)
1471
-
console.log('targets: ')
1472
-
console.log(targets)
1473
-
if (targets) {
1474
forEach(
1475
targets,
1476
function(target) {
···
1628
*/
1629
function attributeHash(elt) {
1630
let hash = 0
1631
-
// IE fix
1632
-
if (elt.attributes) {
1633
-
for (let i = 0; i < elt.attributes.length; i++) {
1634
-
const attribute = elt.attributes[i]
1635
-
if (attribute.value) { // only include attributes w/ actual values (empty is same as non-existent)
1636
-
hash = stringHash(attribute.name, hash)
1637
-
hash = stringHash(attribute.value, hash)
1638
-
}
1639
}
1640
}
1641
return hash
···
1680
function cleanUpElement(element) {
1681
triggerEvent(element, 'htmx:beforeCleanupElement')
1682
deInitNode(element)
1683
-
// @ts-ignore IE11 code
1684
-
// noinspection JSUnresolvedReference
1685
-
if (element.children) { // IE
1686
-
// @ts-ignore
1687
-
forEach(element.children, function(child) { cleanUpElement(child) })
1688
-
}
1689
}
1690
1691
/**
1692
-
* @param {Node} target
1693
* @param {ParentNode} fragment
1694
* @param {HtmxSettleInfo} settleInfo
1695
*/
1696
function swapOuterHTML(target, fragment, settleInfo) {
1697
-
if (target instanceof Element && target.tagName === 'BODY') { // special case the body to innerHTML because DocumentFragments can't contain a body elt unfortunately
1698
return swapInnerHTML(target, fragment, settleInfo)
1699
}
1700
/** @type {Node} */
···
1720
newElt = newElt.nextSibling
1721
}
1722
cleanUpElement(target)
1723
-
if (target instanceof Element) {
1724
-
target.remove()
1725
-
} else {
1726
-
target.parentNode.removeChild(target)
1727
-
}
1728
}
1729
1730
/**
1731
-
* @param {Node} target
1732
* @param {ParentNode} fragment
1733
* @param {HtmxSettleInfo} settleInfo
1734
*/
···
1737
}
1738
1739
/**
1740
-
* @param {Node} target
1741
* @param {ParentNode} fragment
1742
* @param {HtmxSettleInfo} settleInfo
1743
*/
···
1746
}
1747
1748
/**
1749
-
* @param {Node} target
1750
* @param {ParentNode} fragment
1751
* @param {HtmxSettleInfo} settleInfo
1752
*/
···
1755
}
1756
1757
/**
1758
-
* @param {Node} target
1759
* @param {ParentNode} fragment
1760
* @param {HtmxSettleInfo} settleInfo
1761
*/
···
1764
}
1765
1766
/**
1767
-
* @param {Node} target
1768
*/
1769
function swapDelete(target) {
1770
cleanUpElement(target)
···
1775
}
1776
1777
/**
1778
-
* @param {Node} target
1779
* @param {ParentNode} fragment
1780
* @param {HtmxSettleInfo} settleInfo
1781
*/
···
1795
/**
1796
* @param {HtmxSwapStyle} swapStyle
1797
* @param {Element} elt
1798
-
* @param {Node} target
1799
* @param {ParentNode} fragment
1800
* @param {HtmxSettleInfo} settleInfo
1801
*/
···
1873
}
1874
1875
/**
1876
-
* Implements complete swapping pipeline, including: focus and selection preservation,
1877
* title updates, scroll, OOB swapping, normal swapping and settling
1878
* @param {string|Element} target
1879
* @param {string} content
···
1884
if (!swapOptions) {
1885
swapOptions = {}
1886
}
0
0
0
1887
1888
-
target = resolveTarget(target)
1889
-
const rootNode = swapOptions.contextElement ? getRootNode(swapOptions.contextElement, false) : getDocument()
0
0
0
1890
1891
-
// preserve focus and selection
1892
-
const activeElt = document.activeElement
1893
-
let selectionInfo = {}
1894
-
try {
1895
selectionInfo = {
1896
elt: activeElt,
1897
// @ts-ignore
···
1899
// @ts-ignore
1900
end: activeElt ? activeElt.selectionEnd : null
1901
}
1902
-
} catch (e) {
1903
-
// safari issue - see https://github.com/microsoft/playwright/issues/5894
1904
-
}
1905
-
const settleInfo = makeSettleInfo(target)
1906
1907
-
// For text content swaps, don't parse the response as HTML, just insert it
1908
-
if (swapSpec.swapStyle === 'textContent') {
1909
-
target.textContent = content
1910
-
// Otherwise, make the fragment and process it
1911
-
} else {
1912
-
let fragment = makeFragment(content)
1913
1914
-
settleInfo.title = fragment.title
0
0
0
0
1915
1916
-
// select-oob swaps
1917
-
if (swapOptions.selectOOB) {
1918
-
const oobSelectValues = swapOptions.selectOOB.split(',')
1919
-
for (let i = 0; i < oobSelectValues.length; i++) {
1920
-
const oobSelectValue = oobSelectValues[i].split(':', 2)
1921
-
let id = oobSelectValue[0].trim()
1922
-
if (id.indexOf('#') === 0) {
1923
-
id = id.substring(1)
0
0
0
0
0
0
1924
}
1925
-
const oobValue = oobSelectValue[1] || 'true'
1926
-
const oobElement = fragment.querySelector('#' + id)
1927
-
if (oobElement) {
1928
-
oobSwap(oobValue, oobElement, settleInfo, rootNode)
0
0
0
1929
}
0
0
0
0
0
0
0
0
0
1930
}
0
0
0
1931
}
1932
-
// oob swaps
1933
-
findAndSwapOobElements(fragment, settleInfo, rootNode)
1934
-
forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) {
1935
-
if (template.content && findAndSwapOobElements(template.content, settleInfo, rootNode)) {
1936
-
// Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
1937
-
template.remove()
0
0
0
0
0
0
0
0
0
0
0
0
1938
}
0
0
0
0
0
0
0
0
1939
})
0
1940
1941
-
// normal swap
1942
-
if (swapOptions.select) {
1943
-
const newFragment = getDocument().createDocumentFragment()
1944
-
forEach(fragment.querySelectorAll(swapOptions.select), function(node) {
1945
-
newFragment.appendChild(node)
0
0
0
0
0
0
0
0
0
0
1946
})
1947
-
fragment = newFragment
1948
-
}
1949
-
handlePreservedElements(fragment)
1950
-
swapWithStyle(swapSpec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo)
1951
-
restorePreservedElements()
1952
-
}
1953
1954
-
// apply saved focus and selection information to swapped content
1955
-
if (selectionInfo.elt &&
1956
-
!bodyContains(selectionInfo.elt) &&
1957
-
getRawAttribute(selectionInfo.elt, 'id')) {
1958
-
const newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, 'id'))
1959
-
const focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }
1960
-
if (newActiveElt) {
1961
-
// @ts-ignore
1962
-
if (selectionInfo.start && newActiveElt.setSelectionRange) {
1963
-
try {
1964
-
// @ts-ignore
1965
-
newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end)
1966
-
} catch (e) {
1967
-
// the setSelectionRange method is present on fields that don't support it, so just let this fail
1968
}
1969
}
1970
-
newActiveElt.focus(focusOptions)
0
0
0
1971
}
1972
-
}
1973
1974
-
target.classList.remove(htmx.config.swappingClass)
1975
-
forEach(settleInfo.elts, function(elt) {
1976
-
if (elt.classList) {
1977
-
elt.classList.add(htmx.config.settlingClass)
1978
}
1979
-
triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo)
1980
-
})
1981
-
if (swapOptions.afterSwapCallback) {
1982
-
swapOptions.afterSwapCallback()
1983
}
1984
-
1985
-
// merge in new title after swap but before settle
1986
-
if (!swapSpec.ignoreTitle) {
1987
-
handleTitle(settleInfo.title)
1988
}
1989
1990
-
// settle
1991
-
const doSettle = function() {
1992
-
forEach(settleInfo.tasks, function(task) {
1993
-
task.call()
1994
-
})
1995
-
forEach(settleInfo.elts, function(elt) {
1996
-
if (elt.classList) {
1997
-
elt.classList.remove(htmx.config.settlingClass)
1998
-
}
1999
-
triggerEvent(elt, 'htmx:afterSettle', swapOptions.eventInfo)
2000
-
})
2001
-
2002
-
if (swapOptions.anchor) {
2003
-
const anchorTarget = asElement(resolveTarget('#' + swapOptions.anchor))
2004
-
if (anchorTarget) {
2005
-
anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
2006
-
}
2007
-
}
2008
2009
-
updateScrollState(settleInfo.elts, swapSpec)
2010
-
if (swapOptions.afterSettleCallback) {
2011
-
swapOptions.afterSettleCallback()
0
0
0
0
0
0
0
0
0
0
0
0
0
0
2012
}
2013
}
2014
2015
-
if (swapSpec.settleDelay > 0) {
2016
-
getWindow().setTimeout(doSettle, swapSpec.settleDelay)
2017
-
} else {
2018
-
doSettle()
0
0
0
0
0
0
2019
}
2020
}
2021
···
2369
if (path == null || path === '') {
2370
// if there is no action attribute on the form set path to current href before the
2371
// following logic to properly clear parameters on a GET (not on a POST!)
2372
-
path = getDocument().location.href
2373
}
2374
if (verb === 'get' && path.includes('?')) {
2375
path = path.replace(/\?[^#]+/, '')
···
2390
2391
/**
2392
* @param {Event} evt
2393
-
* @param {Node} node
2394
* @returns {boolean}
2395
*/
2396
-
function shouldCancel(evt, node) {
2397
-
const elt = asElement(node)
2398
-
if (!elt) {
2399
-
return false
2400
-
}
2401
if (evt.type === 'submit' || evt.type === 'click') {
0
0
2402
if (elt.tagName === 'FORM') {
2403
return true
2404
}
2405
-
if (matches(elt, 'input[type="submit"], button') &&
2406
-
(matches(elt, '[form]') || closest(elt, 'form') !== null)) {
0
2407
return true
2408
}
2409
-
if (elt instanceof HTMLAnchorElement && elt.href &&
0
0
2410
(elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf('#') !== 0)) {
2411
return true
2412
}
···
2446
}
2447
2448
/**
2449
-
* @param {Node} elt
2450
* @param {TriggerHandler} handler
2451
* @param {HtmxNodeInternalData} nodeData
2452
* @param {HtmxTriggerSpecification} triggerSpec
···
2513
}
2514
}
2515
if (triggerSpec.changed) {
2516
-
const node = event.target
2517
// @ts-ignore value will be undefined for non-input elements, which is fine
2518
const value = node.value
2519
const lastValue = elementData.lastValue.get(triggerSpec)
···
2636
triggerSpecs.forEach(function(triggerSpec) {
2637
addTriggerHandler(elt, triggerSpec, nodeData, function(node, evt) {
2638
const elt = asElement(node)
2639
-
if (closest(elt, htmx.config.disableSelector)) {
2640
cleanUpElement(elt)
2641
return
2642
}
···
2650
2651
/**
2652
* @callback TriggerHandler
2653
-
* @param {Node} elt
2654
* @param {Event} [evt]
2655
*/
2656
2657
/**
2658
-
* @param {Node} elt
2659
* @param {HtmxTriggerSpecification} triggerSpec
2660
* @param {HtmxNodeInternalData} nodeData
2661
* @param {TriggerHandler} handler
···
2780
* @param {Event} evt
2781
*/
2782
function maybeSetLastButtonClicked(evt) {
2783
-
const elt = /** @type {HTMLButtonElement|HTMLInputElement} */ (closest(asElement(evt.target), "button, input[type='submit']"))
2784
const internalData = getRelatedFormData(evt)
2785
if (internalData) {
2786
internalData.lastButtonClicked = elt
···
2795
if (internalData) {
2796
internalData.lastButtonClicked = null
2797
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
2798
}
2799
2800
/**
···
2802
* @returns {HtmxNodeInternalData|undefined}
2803
*/
2804
function getRelatedFormData(evt) {
2805
-
const elt = closest(asElement(evt.target), "button, input[type='submit']")
2806
if (!elt) {
2807
return
2808
}
2809
-
const form = resolveTarget('#' + getRawAttribute(elt, 'form'), elt.getRootNode()) || closest(elt, 'form')
2810
-
if (!form) {
2811
-
return
2812
-
}
2813
return getInternalData(form)
2814
}
2815
···
2886
* @param {Element|HTMLInputElement} elt
2887
*/
2888
function initNode(elt) {
2889
-
if (closest(elt, htmx.config.disableSelector)) {
2890
-
cleanUpElement(elt)
2891
-
return
2892
-
}
2893
const nodeData = getInternalData(elt)
2894
-
const attrHash = attributeHash(elt)
2895
-
if (nodeData.initHash !== attrHash) {
2896
-
// clean up any previously processed info
2897
-
deInitNode(elt)
2898
2899
-
nodeData.initHash = attrHash
0
0
0
0
0
0
0
0
0
0
2900
2901
-
triggerEvent(elt, 'htmx:beforeProcessNode')
0
0
0
0
2902
2903
-
const triggerSpecs = getTriggerSpecs(elt)
2904
-
const hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs)
0
2905
2906
-
if (!hasExplicitHttpAction) {
2907
-
if (getClosestAttributeValue(elt, 'hx-boost') === 'true') {
2908
-
boostElement(elt, nodeData, triggerSpecs)
2909
-
} else if (hasAttribute(elt, 'hx-trigger')) {
2910
-
triggerSpecs.forEach(function(triggerSpec) {
2911
-
// For "naked" triggers, don't do anything at all
2912
-
addTriggerHandler(elt, triggerSpec, nodeData, function() {
2913
-
})
2914
-
})
2915
-
}
2916
-
}
2917
-
2918
-
// Handle submit buttons/inputs that have the form attribute set
2919
-
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
2920
-
if (elt.tagName === 'FORM' || (getRawAttribute(elt, 'type') === 'submit' && hasAttribute(elt, 'form'))) {
2921
-
initButtonTracking(elt)
2922
-
}
2923
2924
-
nodeData.firstInitCompleted = true
2925
-
triggerEvent(elt, 'htmx:afterProcessNode')
0
0
0
0
2926
}
0
2927
}
2928
2929
/**
···
2935
*/
2936
function processNode(elt) {
2937
elt = resolveTarget(elt)
2938
-
if (closest(elt, htmx.config.disableSelector)) {
2939
cleanUpElement(elt)
2940
return
2941
}
2942
-
initNode(elt)
2943
-
forEach(findElementsToProcess(elt), function(child) { initNode(child) })
0
0
0
0
0
0
0
0
0
0
0
0
0
2944
forEach(findHxOnWildcardElements(elt), processHxOnWildcard)
0
2945
}
2946
2947
//= ===================================================================
···
2962
* @returns {CustomEvent}
2963
*/
2964
function makeEvent(eventName, detail) {
2965
-
let evt
2966
-
if (window.CustomEvent && typeof window.CustomEvent === 'function') {
2967
-
// TODO: `composed: true` here is a hack to make global event handlers work with events in shadow DOM
2968
-
// This breaks expected encapsulation but needs to be here until decided otherwise by core devs
2969
-
evt = new CustomEvent(eventName, { bubbles: true, cancelable: true, composed: true, detail })
2970
-
} else {
2971
-
evt = getDocument().createEvent('CustomEvent')
2972
-
evt.initCustomEvent(eventName, true, true, detail)
2973
-
}
2974
-
return evt
2975
}
2976
2977
/**
···
2993
2994
/**
2995
* `withExtensions` locates all active extensions for a provided element, then
2996
-
* executes the provided function using each of the active extensions. It should
0
2997
* be called internally at every extendable execution point in htmx.
2998
*
2999
* @param {Element} elt
3000
* @param {(extension:HtmxExtension) => void} toDo
0
3001
* @returns void
3002
*/
3003
-
function withExtensions(elt, toDo) {
3004
-
forEach(getExtensions(elt), function(extension) {
3005
try {
3006
toDo(extension)
3007
} catch (e) {
···
3011
}
3012
3013
function logError(msg) {
3014
-
if (console.error) {
3015
-
console.error(msg)
3016
-
} else if (console.log) {
3017
-
console.log('ERROR: ', msg)
3018
-
}
3019
}
3020
3021
/**
···
3060
let currentPathForHistory = location.pathname + location.search
3061
3062
/**
0
0
0
0
0
0
0
0
0
0
3063
* @returns {Element}
3064
*/
3065
function getHistoryElement() {
···
3083
3084
if (htmx.config.historyCacheSize <= 0) {
3085
// make sure that an eventually already existing cache is purged
3086
-
localStorage.removeItem('htmx-history-cache')
3087
return
3088
}
3089
3090
url = normalizePath(url)
3091
3092
-
const historyCache = parseJSON(localStorage.getItem('htmx-history-cache')) || []
3093
for (let i = 0; i < historyCache.length; i++) {
3094
if (historyCache[i].url === url) {
3095
historyCache.splice(i, 1)
···
3110
// keep trying to save the cache until it succeeds or is empty
3111
while (historyCache.length > 0) {
3112
try {
3113
-
localStorage.setItem('htmx-history-cache', JSON.stringify(historyCache))
3114
break
3115
} catch (e) {
3116
triggerErrorEvent(getDocument().body, 'htmx:historyCacheError', { cause: e, cache: historyCache })
···
3138
3139
url = normalizePath(url)
3140
3141
-
const historyCache = parseJSON(localStorage.getItem('htmx-history-cache')) || []
3142
for (let i = 0; i < historyCache.length; i++) {
3143
if (historyCache[i].url === url) {
3144
return historyCache[i]
···
3166
3167
function saveCurrentPageToHistory() {
3168
const elt = getHistoryElement()
3169
-
const path = currentPathForHistory || location.pathname + location.search
0
0
0
0
3170
3171
// Allow history snapshot feature to be disabled where hx-history="false"
3172
// is present *anywhere* in the current document we're about to save,
3173
// so we can prevent privileged data entering the cache.
3174
// The page will still be reachable as a history entry, but htmx will fetch it
3175
-
// live from the server onpopstate rather than look in the localStorage cache
3176
-
let disableHistoryCache
3177
-
try {
3178
-
disableHistoryCache = getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]')
3179
-
} catch (e) {
3180
-
// IE11: insensitive modifier not supported so fallback to case sensitive selector
3181
-
disableHistoryCache = getDocument().querySelector('[hx-history="false"],[data-hx-history="false"]')
3182
-
}
3183
if (!disableHistoryCache) {
3184
triggerEvent(getDocument().body, 'htmx:beforeHistorySave', { path, historyElt: elt })
3185
saveToHistoryCache(path, elt)
3186
}
3187
3188
-
if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, getDocument().title, window.location.href)
3189
}
3190
3191
/**
···
3202
if (htmx.config.historyEnabled) {
3203
history.pushState({ htmx: true }, '', path)
3204
}
3205
-
currentPathForHistory = path
3206
}
3207
3208
/**
···
3210
*/
3211
function replaceUrlInHistory(path) {
3212
if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, '', path)
3213
-
currentPathForHistory = path
3214
}
3215
3216
/**
···
3227
*/
3228
function loadHistoryFromServer(path) {
3229
const request = new XMLHttpRequest()
3230
-
const details = { path, xhr: request }
3231
-
triggerEvent(getDocument().body, 'htmx:historyCacheMiss', details)
3232
request.open('GET', path, true)
3233
-
request.setRequestHeader('HX-Request', 'true')
0
0
3234
request.setRequestHeader('HX-History-Restore-Request', 'true')
3235
-
request.setRequestHeader('HX-Current-URL', getDocument().location.href)
3236
request.onload = function() {
3237
if (this.status >= 200 && this.status < 400) {
0
3238
triggerEvent(getDocument().body, 'htmx:historyCacheMissLoad', details)
3239
-
const fragment = makeFragment(this.response)
3240
-
/** @type ParentNode */
3241
-
const content = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment
3242
-
const historyElement = getHistoryElement()
3243
-
const settleInfo = makeSettleInfo(historyElement)
3244
-
handleTitle(fragment.title)
3245
-
3246
-
handlePreservedElements(fragment)
3247
-
swapInnerHTML(historyElement, content, settleInfo)
3248
-
restorePreservedElements()
3249
-
settleImmediately(settleInfo.tasks)
3250
-
currentPathForHistory = path
3251
-
triggerEvent(getDocument().body, 'htmx:historyRestore', { path, cacheMiss: true, serverResponse: this.response })
3252
} else {
3253
triggerErrorEvent(getDocument().body, 'htmx:historyCacheMissLoadError', details)
3254
}
3255
}
3256
-
request.send()
0
0
3257
}
3258
3259
/**
···
3264
path = path || location.pathname + location.search
3265
const cached = getCachedHistory(path)
3266
if (cached) {
3267
-
const fragment = makeFragment(cached.content)
3268
-
const historyElement = getHistoryElement()
3269
-
const settleInfo = makeSettleInfo(historyElement)
3270
-
handleTitle(cached.title)
3271
-
handlePreservedElements(fragment)
3272
-
swapInnerHTML(historyElement, fragment, settleInfo)
3273
-
restorePreservedElements()
3274
-
settleImmediately(settleInfo.tasks)
3275
-
getWindow().setTimeout(function() {
3276
-
window.scrollTo(0, cached.scroll)
3277
-
}, 0) // next 'tick', so browser has time to render layout
3278
-
currentPathForHistory = path
3279
-
triggerEvent(getDocument().body, 'htmx:historyRestore', { path, item: cached })
3280
} else {
3281
if (htmx.config.refreshOnHistoryMiss) {
3282
// @ts-ignore: optional parameter in reload() function throws error
3283
// noinspection JSUnresolvedReference
3284
-
window.location.reload(true)
3285
} else {
3286
loadHistoryFromServer(path)
3287
}
···
3386
return true
3387
}
3388
3389
-
/** @param {string} name
0
3390
* @param {string|Array|FormDataEntryValue} value
3391
* @param {FormData} formData */
3392
function addValueToFormData(name, value, formData) {
···
3399
}
3400
}
3401
3402
-
/** @param {string} name
0
3403
* @param {string|Array} value
3404
* @param {FormData} formData */
3405
function removeValueFromFormData(name, value, formData) {
···
3416
}
3417
3418
/**
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
3419
* @param {Element[]} processed
3420
* @param {FormData} formData
3421
* @param {HtmxElementValidationError[]} errors
···
3430
}
3431
if (shouldInclude(elt)) {
3432
const name = getRawAttribute(elt, 'name')
3433
-
// @ts-ignore value will be undefined for non-input elements, which is fine
3434
-
let value = elt.value
3435
-
if (elt instanceof HTMLSelectElement && elt.multiple) {
3436
-
value = toArray(elt.querySelectorAll('option:checked')).map(function(e) { return (/** @type HTMLOptionElement */(e)).value })
3437
-
}
3438
-
// include file inputs
3439
-
if (elt instanceof HTMLInputElement && elt.files) {
3440
-
value = toArray(elt.files)
3441
-
}
3442
-
addValueToFormData(name, value, formData)
3443
if (validate) {
3444
validateElement(elt, errors)
3445
}
···
3450
// The input has already been processed and added to the values, but the FormData that will be
3451
// constructed right after on the form, will include it once again. So remove that input's value
3452
// now to avoid duplicates
3453
-
removeValueFromFormData(input.name, input.value, formData)
3454
} else {
3455
processed.push(input)
3456
}
···
3468
}
3469
3470
/**
3471
-
*
3472
* @param {Element} elt
3473
* @param {HtmxElementValidationError[]} errors
3474
*/
···
3523
validate = validate && internalData.lastButtonClicked.formNoValidate !== true
3524
}
3525
3526
-
// for a non-GET include the closest form
3527
if (verb !== 'get') {
3528
-
processInputValue(processed, priorityFormData, errors, closest(elt, 'form'), validate)
3529
}
3530
3531
// include the element itself
···
3605
'HX-Trigger': getRawAttribute(elt, 'id'),
3606
'HX-Trigger-Name': getRawAttribute(elt, 'name'),
3607
'HX-Target': getAttributeValue(target, 'id'),
3608
-
'HX-Current-URL': getDocument().location.href
3609
}
3610
getValuesForElement(elt, 'hx-headers', false, headers)
3611
if (prompt !== undefined) {
···
3783
target = target || last
3784
target.scrollTop = target.scrollHeight
3785
}
0
0
0
0
0
3786
}
3787
if (swapSpec.show) {
3788
var target = null
···
3811
* @param {string} attr
3812
* @param {boolean=} evalAsDefault
3813
* @param {Object=} values
0
3814
* @returns {Object}
3815
*/
3816
-
function getValuesForElement(elt, attr, evalAsDefault, values) {
3817
if (values == null) {
3818
values = {}
3819
}
···
3839
}
3840
let varsValues
3841
if (evaluateValue) {
3842
-
varsValues = maybeEval(elt, function() { return Function('return (' + str + ')')() }, {})
0
0
0
0
0
0
3843
} else {
3844
varsValues = parseJSON(str)
3845
}
···
3851
}
3852
}
3853
}
3854
-
return getValuesForElement(asElement(parentElt(elt)), attr, evalAsDefault, values)
3855
}
3856
3857
/**
···
3871
3872
/**
3873
* @param {Element} elt
3874
-
* @param {*?} expressionVars
0
3875
* @returns
3876
*/
3877
-
function getHXVarsForElement(elt, expressionVars) {
3878
-
return getValuesForElement(elt, 'hx-vars', true, expressionVars)
3879
}
3880
3881
/**
3882
* @param {Element} elt
3883
-
* @param {*?} expressionVars
0
3884
* @returns
3885
*/
3886
-
function getHXValsForElement(elt, expressionVars) {
3887
-
return getValuesForElement(elt, 'hx-vals', false, expressionVars)
3888
}
3889
3890
/**
3891
* @param {Element} elt
0
3892
* @returns {FormData}
3893
*/
3894
-
function getExpressionVars(elt) {
3895
-
return mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt))
3896
}
3897
3898
/**
···
3917
* @return {string}
3918
*/
3919
function getPathFromResponse(xhr) {
3920
-
// NB: IE11 does not support this stuff
3921
-
if (xhr.responseURL && typeof (URL) !== 'undefined') {
3922
try {
3923
const url = new URL(xhr.responseURL)
3924
return url.pathname + url.search
···
4000
* @return {boolean}
4001
*/
4002
function verifyPath(elt, path, requestConfig) {
4003
-
let sameHost
4004
-
let url
4005
-
if (typeof URL === 'function') {
4006
-
url = new URL(path, document.location.href)
4007
-
const origin = document.location.origin
4008
-
sameHost = origin === url.origin
4009
-
} else {
4010
-
// IE11 doesn't support URL
4011
-
url = path
4012
-
sameHost = startsWith(path, document.location.origin)
4013
-
}
4014
4015
if (htmx.config.selfRequestsOnly) {
4016
if (!sameHost) {
···
4111
return function() {
4112
return formData[name].apply(formData, arguments)
4113
}
4114
-
} else {
4115
-
return target[name]
4116
}
4117
}
4118
const array = formData.getAll(name)
···
4187
}
4188
const target = etc.targetOverride || asElement(getTarget(elt))
4189
if (target == null || target == DUMMY_ELT) {
4190
-
triggerErrorEvent(elt, 'htmx:targetError', { target: getAttributeValue(elt, 'hx-target') })
4191
maybeCall(reject)
4192
return promise
4193
}
···
4203
4204
const buttonVerb = getRawAttribute(submitter, 'formmethod')
4205
if (buttonVerb != null) {
4206
-
// ignore buttons with formmethod="dialog"
4207
-
if (buttonVerb.toLowerCase() !== 'dialog') {
4208
verb = (/** @type HttpVerb */(buttonVerb))
0
0
0
4209
}
4210
}
4211
}
···
4340
if (etc.values) {
4341
overrideFormData(rawFormData, formDataFromObject(etc.values))
4342
}
4343
-
const expressionVars = formDataFromObject(getExpressionVars(elt))
4344
const allFormData = overrideFormData(rawFormData, expressionVars)
4345
let filteredFormData = filterValues(allFormData, elt)
4346
···
4350
4351
// behavior of anchors w/ empty href is to use the current URL
4352
if (path == null || path === '') {
4353
-
path = getDocument().location.href
4354
}
4355
4356
/**
···
4374
unfilteredFormData: allFormData,
4375
unfilteredParameters: formDataProxy(allFormData),
4376
headers,
0
4377
target,
4378
verb,
4379
errors,
···
4428
if (!verifyPath(elt, finalPath, requestConfig)) {
4429
triggerErrorEvent(elt, 'htmx:invalidPath', requestConfig)
4430
maybeCall(reject)
0
4431
return promise
4432
}
4433
···
4490
}
4491
}
4492
maybeCall(resolve)
4493
-
endRequestLock()
4494
} catch (e) {
4495
triggerErrorEvent(elt, 'htmx:onLoadError', mergeObjects({ error: e }, responseInfo))
4496
throw e
0
0
4497
}
4498
}
4499
xhr.onerror = function() {
···
4668
if (title) {
4669
const titleElt = find('title')
4670
if (titleElt) {
4671
-
titleElt.innerHTML = title
4672
} else {
4673
window.document.title = title
4674
}
···
4676
}
4677
4678
/**
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
4679
* @param {Element} elt
4680
* @param {HtmxResponseInfo} responseInfo
4681
*/
···
4712
4713
if (hasHeader(xhr, /HX-Redirect:/i)) {
4714
responseInfo.keepIndicators = true
4715
-
location.href = xhr.getResponseHeader('HX-Redirect')
4716
-
shouldRefresh && location.reload()
4717
return
4718
}
4719
4720
if (shouldRefresh) {
4721
responseInfo.keepIndicators = true
4722
-
location.reload()
4723
return
4724
}
4725
4726
-
if (hasHeader(xhr, /HX-Retarget:/i)) {
4727
-
if (xhr.getResponseHeader('HX-Retarget') === 'this') {
4728
-
responseInfo.target = elt
4729
-
} else {
4730
-
responseInfo.target = asElement(querySelectorExt(elt, xhr.getResponseHeader('HX-Retarget')))
4731
-
}
4732
-
}
4733
-
4734
const historyUpdate = determineHistoryUpdates(elt, responseInfo)
4735
4736
const responseHandling = resolveResponseHandling(xhr)
···
4739
let ignoreTitle = htmx.config.ignoreTitle || responseHandling.ignoreTitle
4740
let selectOverride = responseHandling.select
4741
if (responseHandling.target) {
4742
-
responseInfo.target = asElement(querySelectorExt(elt, responseHandling.target))
4743
}
4744
var swapOverride = etc.swapOverride
4745
if (swapOverride == null && responseHandling.swapOverride) {
···
4748
4749
// response headers override response handling config
4750
if (hasHeader(xhr, /HX-Retarget:/i)) {
4751
-
if (xhr.getResponseHeader('HX-Retarget') === 'this') {
4752
-
responseInfo.target = elt
4753
-
} else {
4754
-
responseInfo.target = asElement(querySelectorExt(elt, xhr.getResponseHeader('HX-Retarget')))
4755
-
}
4756
}
0
4757
if (hasHeader(xhr, /HX-Reswap:/i)) {
4758
swapOverride = xhr.getResponseHeader('HX-Reswap')
4759
}
···
4806
4807
target.classList.add(htmx.config.swappingClass)
4808
4809
-
// optional transition API promise callbacks
4810
-
let settleResolve = null
4811
-
let settleReject = null
4812
-
4813
if (responseInfoSelect) {
4814
selectOverride = responseInfoSelect
4815
}
···
4821
const selectOOB = getClosestAttributeValue(elt, 'hx-select-oob')
4822
const select = getClosestAttributeValue(elt, 'hx-select')
4823
4824
-
let doSwap = function() {
4825
-
try {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
4826
// if we need to save history, do so, before swapping so that relative resources have the correct base URL
4827
if (historyUpdate.type) {
4828
triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo))
···
4834
triggerEvent(getDocument().body, 'htmx:replacedInHistory', { path: historyUpdate.path })
4835
}
4836
}
4837
-
4838
-
swap(target, serverResponse, swapSpec, {
4839
-
select: selectOverride || select,
4840
-
selectOOB,
4841
-
eventInfo: responseInfo,
4842
-
anchor: responseInfo.pathInfo.anchor,
4843
-
contextElement: elt,
4844
-
afterSwapCallback: function() {
4845
-
if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
4846
-
let finalElt = elt
4847
-
if (!bodyContains(elt)) {
4848
-
finalElt = getDocument().body
4849
-
}
4850
-
handleTriggerHeader(xhr, 'HX-Trigger-After-Swap', finalElt)
4851
-
}
4852
-
},
4853
-
afterSettleCallback: function() {
4854
-
if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
4855
-
let finalElt = elt
4856
-
if (!bodyContains(elt)) {
4857
-
finalElt = getDocument().body
4858
-
}
4859
-
handleTriggerHeader(xhr, 'HX-Trigger-After-Settle', finalElt)
4860
-
}
4861
-
maybeCall(settleResolve)
4862
-
}
4863
-
})
4864
-
} catch (e) {
4865
-
triggerErrorEvent(elt, 'htmx:swapError', responseInfo)
4866
-
maybeCall(settleReject)
4867
-
throw e
4868
}
4869
-
}
4870
-
4871
-
let shouldTransition = htmx.config.globalViewTransitions
4872
-
if (swapSpec.hasOwnProperty('transition')) {
4873
-
shouldTransition = swapSpec.transition
4874
-
}
4875
-
4876
-
if (shouldTransition &&
4877
-
triggerEvent(elt, 'htmx:beforeTransition', responseInfo) &&
4878
-
typeof Promise !== 'undefined' &&
4879
-
// @ts-ignore experimental feature atm
4880
-
document.startViewTransition) {
4881
-
const settlePromise = new Promise(function(_resolve, _reject) {
4882
-
settleResolve = _resolve
4883
-
settleReject = _reject
4884
-
})
4885
-
// wrap the original doSwap() in a call to startViewTransition()
4886
-
const innerDoSwap = doSwap
4887
-
doSwap = function() {
4888
-
// @ts-ignore experimental feature atm
4889
-
document.startViewTransition(function() {
4890
-
innerDoSwap()
4891
-
return settlePromise
4892
-
})
4893
-
}
4894
-
}
4895
-
4896
-
if (swapSpec.swapDelay > 0) {
4897
-
getWindow().setTimeout(doSwap, swapSpec.swapDelay)
4898
-
} else {
4899
-
doSwap()
4900
-
}
4901
}
4902
if (isError) {
4903
triggerErrorEvent(elt, 'htmx:responseError', mergeObjects({ error: 'Response Status Error Code ' + xhr.status + ' from ' + responseInfo.pathInfo.requestPath }, responseInfo))
···
5098
* @property {Element} [contextElement]
5099
* @property {swapCallback} [afterSwapCallback]
5100
* @property {swapCallback} [afterSettleCallback]
0
0
0
5101
*/
5102
5103
/**
···
5116
* @property {boolean} [transition]
5117
* @property {boolean} [ignoreTitle]
5118
* @property {string} [head]
5119
-
* @property {'top' | 'bottom'} [scroll]
5120
* @property {string} [scrollTarget]
5121
* @property {string} [show]
5122
* @property {string} [showTarget]
···
5161
* @property {'true'} [HX-History-Restore-Request]
5162
*/
5163
5164
-
/** @typedef HtmxAjaxHelperContext
0
5165
* @property {Element|string} [source]
5166
* @property {Event} [event]
5167
* @property {HtmxAjaxHandler} [handler]
···
5181
* @property {FormData} unfilteredFormData
5182
* @property {Object} unfilteredParameters unfilteredFormData proxy
5183
* @property {HtmxHeaderSpecification} headers
0
5184
* @property {Element} target
5185
* @property {HttpVerb} verb
5186
* @property {HtmxElementValidationError[]} errors
···
5254
* @see https://github.com/bigskysoftware/htmx-extensions/blob/main/README.md
5255
* @typedef {Object} HtmxExtension
5256
* @property {(api: any) => void} init
5257
-
* @property {(name: string, event: Event|CustomEvent) => boolean} onEvent
5258
* @property {(text: string, xhr: XMLHttpRequest, elt: Element) => string} transformResponse
5259
* @property {(swapStyle: HtmxSwapStyle) => boolean} isInlineSwap
5260
* @property {(swapStyle: HtmxSwapStyle, target: Node, fragment: Node, settleInfo: HtmxSettleInfo) => boolean|Node[]} handleSwap
···
82
*/
83
historyEnabled: true,
84
/**
85
+
* The number of pages to keep in **sessionStorage** for history support.
86
* @type number
87
* @default 10
88
*/
···
271
* @type boolean
272
* @default true
273
*/
274
+
allowNestedOobSwaps: true,
275
+
/**
276
+
* Whether to treat history cache miss full page reload requests as a "HX-Request" by returning this response header
277
+
* This should always be disabled when using HX-Request header to optionally return partial responses
278
+
* @type boolean
279
+
* @default true
280
+
*/
281
+
historyRestoreAsHxRequest: true
282
},
283
/** @type {typeof parseInterval} */
284
parseInterval: null,
285
+
/**
286
+
* proxy of window.location used for page reload functions
287
+
* @type location
288
+
*/
289
+
location,
290
/** @type {typeof internalEval} */
291
_: null,
292
+
version: '2.0.6'
293
}
294
// Tsc madness part 2
295
htmx.onLoad = onLoadHelper
···
496
* @returns {boolean}
497
*/
498
function matches(elt, selector) {
499
+
return elt instanceof Element && elt.matches(selector)
0
0
0
500
}
501
502
/**
···
819
* @returns {boolean}
820
*/
821
function canAccessLocalStorage() {
822
+
const test = 'htmx:sessionStorageTest'
823
try {
824
+
sessionStorage.setItem(test, test)
825
+
sessionStorage.removeItem(test)
826
return true
827
} catch (e) {
828
return false
···
834
* @returns {string}
835
*/
836
function normalizePath(path) {
837
+
// use dummy base URL to allow normalize on path only
838
+
const url = new URL(path, 'http://x')
839
+
if (url) {
840
+
path = url.pathname + url.search
841
+
}
842
+
// remove trailing slash, unless index page
843
+
if (path != '/') {
844
+
path = path.replace(/\/+$/, '')
0
0
0
0
0
845
}
846
+
return path
847
}
848
849
//= =========================================================================================
···
1079
*/
1080
function closest(elt, selector) {
1081
elt = asElement(resolveTarget(elt))
1082
+
if (elt) {
1083
return elt.closest(selector)
0
0
0
0
0
0
0
0
0
1084
}
1085
+
return null
1086
}
1087
1088
/**
···
1157
const selector = normalizeSelector(parts.shift())
1158
let item
1159
if (selector.indexOf('closest ') === 0) {
1160
+
item = closest(asElement(elt), normalizeSelector(selector.slice(8)))
1161
} else if (selector.indexOf('find ') === 0) {
1162
+
item = find(asParentNode(elt), normalizeSelector(selector.slice(5)))
1163
} else if (selector === 'next' || selector === 'nextElementSibling') {
1164
item = asElement(elt).nextElementSibling
1165
} else if (selector.indexOf('next ') === 0) {
1166
+
item = scanForwardQuery(elt, normalizeSelector(selector.slice(5)), !!global)
1167
} else if (selector === 'previous' || selector === 'previousElementSibling') {
1168
item = asElement(elt).previousElementSibling
1169
} else if (selector.indexOf('previous ') === 0) {
1170
+
item = scanBackwardsQuery(elt, normalizeSelector(selector.slice(9)), !!global)
1171
} else if (selector === 'document') {
1172
item = document
1173
} else if (selector === 'window') {
···
1347
return [findThisElement(elt, attrName)]
1348
} else {
1349
const result = querySelectorAllExt(elt, attrTarget)
1350
+
// find `inherit` whole word in value, make sure it's surrounded by commas or is at the start/end of string
1351
+
const shouldInherit = /(^|,)(\s*)inherit(\s*)($|,)/.test(attrTarget)
1352
+
if (shouldInherit) {
1353
+
const eltToInheritFrom = asElement(getClosestMatch(elt, function(parent) {
1354
+
return parent !== elt && hasAttribute(asElement(parent), attrName)
1355
+
}))
1356
+
if (eltToInheritFrom) {
1357
+
result.push(...findAttributeTargets(eltToInheritFrom, attrName))
1358
+
}
1359
+
}
1360
if (result.length === 0) {
1361
logError('The selector "' + attrTarget + '" on ' + attrName + ' returned no matches!')
1362
return [DUMMY_ELT]
···
1405
* @returns {boolean}
1406
*/
1407
function shouldSettleAttribute(name) {
1408
+
return htmx.config.attributesToSettle.includes(name)
0
0
0
0
0
0
1409
}
1410
1411
/**
···
1454
*/
1455
function oobSwap(oobValue, oobElement, settleInfo, rootNode) {
1456
rootNode = rootNode || getDocument()
1457
+
let selector = '#' + CSS.escape(getRawAttribute(oobElement, 'id'))
1458
/** @type HtmxSwapStyle */
1459
let swapStyle = 'outerHTML'
1460
if (oobValue === 'true') {
···
1469
oobElement.removeAttribute('data-hx-swap-oob')
1470
1471
const targets = querySelectorAllExt(rootNode, selector, false)
1472
+
if (targets.length) {
0
0
1473
forEach(
1474
targets,
1475
function(target) {
···
1627
*/
1628
function attributeHash(elt) {
1629
let hash = 0
1630
+
for (let i = 0; i < elt.attributes.length; i++) {
1631
+
const attribute = elt.attributes[i]
1632
+
if (attribute.value) { // only include attributes w/ actual values (empty is same as non-existent)
1633
+
hash = stringHash(attribute.name, hash)
1634
+
hash = stringHash(attribute.value, hash)
0
0
0
1635
}
1636
}
1637
return hash
···
1676
function cleanUpElement(element) {
1677
triggerEvent(element, 'htmx:beforeCleanupElement')
1678
deInitNode(element)
1679
+
// @ts-ignore
1680
+
forEach(element.children, function(child) { cleanUpElement(child) })
0
0
0
0
1681
}
1682
1683
/**
1684
+
* @param {Element} target
1685
* @param {ParentNode} fragment
1686
* @param {HtmxSettleInfo} settleInfo
1687
*/
1688
function swapOuterHTML(target, fragment, settleInfo) {
1689
+
if (target.tagName === 'BODY') { // special case the body to innerHTML because DocumentFragments can't contain a body elt unfortunately
1690
return swapInnerHTML(target, fragment, settleInfo)
1691
}
1692
/** @type {Node} */
···
1712
newElt = newElt.nextSibling
1713
}
1714
cleanUpElement(target)
1715
+
target.remove()
0
0
0
0
1716
}
1717
1718
/**
1719
+
* @param {Element} target
1720
* @param {ParentNode} fragment
1721
* @param {HtmxSettleInfo} settleInfo
1722
*/
···
1725
}
1726
1727
/**
1728
+
* @param {Element} target
1729
* @param {ParentNode} fragment
1730
* @param {HtmxSettleInfo} settleInfo
1731
*/
···
1734
}
1735
1736
/**
1737
+
* @param {Element} target
1738
* @param {ParentNode} fragment
1739
* @param {HtmxSettleInfo} settleInfo
1740
*/
···
1743
}
1744
1745
/**
1746
+
* @param {Element} target
1747
* @param {ParentNode} fragment
1748
* @param {HtmxSettleInfo} settleInfo
1749
*/
···
1752
}
1753
1754
/**
1755
+
* @param {Element} target
1756
*/
1757
function swapDelete(target) {
1758
cleanUpElement(target)
···
1763
}
1764
1765
/**
1766
+
* @param {Element} target
1767
* @param {ParentNode} fragment
1768
* @param {HtmxSettleInfo} settleInfo
1769
*/
···
1783
/**
1784
* @param {HtmxSwapStyle} swapStyle
1785
* @param {Element} elt
1786
+
* @param {Element} target
1787
* @param {ParentNode} fragment
1788
* @param {HtmxSettleInfo} settleInfo
1789
*/
···
1861
}
1862
1863
/**
1864
+
* Implements complete swapping pipeline, including: delay, view transitions, focus and selection preservation,
1865
* title updates, scroll, OOB swapping, normal swapping and settling
1866
* @param {string|Element} target
1867
* @param {string} content
···
1872
if (!swapOptions) {
1873
swapOptions = {}
1874
}
1875
+
// optional transition API promise callbacks
1876
+
let settleResolve = null
1877
+
let settleReject = null
1878
1879
+
let doSwap = function() {
1880
+
maybeCall(swapOptions.beforeSwapCallback)
1881
+
1882
+
target = resolveTarget(target)
1883
+
const rootNode = swapOptions.contextElement ? getRootNode(swapOptions.contextElement, false) : getDocument()
1884
1885
+
// preserve focus and selection
1886
+
const activeElt = document.activeElement
1887
+
let selectionInfo = {}
0
1888
selectionInfo = {
1889
elt: activeElt,
1890
// @ts-ignore
···
1892
// @ts-ignore
1893
end: activeElt ? activeElt.selectionEnd : null
1894
}
1895
+
const settleInfo = makeSettleInfo(target)
0
0
0
1896
1897
+
// For text content swaps, don't parse the response as HTML, just insert it
1898
+
if (swapSpec.swapStyle === 'textContent') {
1899
+
target.textContent = content
1900
+
// Otherwise, make the fragment and process it
1901
+
} else {
1902
+
let fragment = makeFragment(content)
1903
1904
+
settleInfo.title = swapOptions.title || fragment.title
1905
+
if (swapOptions.historyRequest) {
1906
+
// @ts-ignore fragment can be a parentNode Element
1907
+
fragment = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment
1908
+
}
1909
1910
+
// select-oob swaps
1911
+
if (swapOptions.selectOOB) {
1912
+
const oobSelectValues = swapOptions.selectOOB.split(',')
1913
+
for (let i = 0; i < oobSelectValues.length; i++) {
1914
+
const oobSelectValue = oobSelectValues[i].split(':', 2)
1915
+
let id = oobSelectValue[0].trim()
1916
+
if (id.indexOf('#') === 0) {
1917
+
id = id.substring(1)
1918
+
}
1919
+
const oobValue = oobSelectValue[1] || 'true'
1920
+
const oobElement = fragment.querySelector('#' + id)
1921
+
if (oobElement) {
1922
+
oobSwap(oobValue, oobElement, settleInfo, rootNode)
1923
+
}
1924
}
1925
+
}
1926
+
// oob swaps
1927
+
findAndSwapOobElements(fragment, settleInfo, rootNode)
1928
+
forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) {
1929
+
if (template.content && findAndSwapOobElements(template.content, settleInfo, rootNode)) {
1930
+
// Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
1931
+
template.remove()
1932
}
1933
+
})
1934
+
1935
+
// normal swap
1936
+
if (swapOptions.select) {
1937
+
const newFragment = getDocument().createDocumentFragment()
1938
+
forEach(fragment.querySelectorAll(swapOptions.select), function(node) {
1939
+
newFragment.appendChild(node)
1940
+
})
1941
+
fragment = newFragment
1942
}
1943
+
handlePreservedElements(fragment)
1944
+
swapWithStyle(swapSpec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo)
1945
+
restorePreservedElements()
1946
}
1947
+
1948
+
// apply saved focus and selection information to swapped content
1949
+
if (selectionInfo.elt &&
1950
+
!bodyContains(selectionInfo.elt) &&
1951
+
getRawAttribute(selectionInfo.elt, 'id')) {
1952
+
const newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, 'id'))
1953
+
const focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }
1954
+
if (newActiveElt) {
1955
+
// @ts-ignore
1956
+
if (selectionInfo.start && newActiveElt.setSelectionRange) {
1957
+
try {
1958
+
// @ts-ignore
1959
+
newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end)
1960
+
} catch (e) {
1961
+
// the setSelectionRange method is present on fields that don't support it, so just let this fail
1962
+
}
1963
+
}
1964
+
newActiveElt.focus(focusOptions)
1965
}
1966
+
}
1967
+
1968
+
target.classList.remove(htmx.config.swappingClass)
1969
+
forEach(settleInfo.elts, function(elt) {
1970
+
if (elt.classList) {
1971
+
elt.classList.add(htmx.config.settlingClass)
1972
+
}
1973
+
triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo)
1974
})
1975
+
maybeCall(swapOptions.afterSwapCallback)
1976
1977
+
// merge in new title after swap but before settle
1978
+
if (!swapSpec.ignoreTitle) {
1979
+
handleTitle(settleInfo.title)
1980
+
}
1981
+
1982
+
// settle
1983
+
const doSettle = function() {
1984
+
forEach(settleInfo.tasks, function(task) {
1985
+
task.call()
1986
+
})
1987
+
forEach(settleInfo.elts, function(elt) {
1988
+
if (elt.classList) {
1989
+
elt.classList.remove(htmx.config.settlingClass)
1990
+
}
1991
+
triggerEvent(elt, 'htmx:afterSettle', swapOptions.eventInfo)
1992
})
0
0
0
0
0
0
1993
1994
+
if (swapOptions.anchor) {
1995
+
const anchorTarget = asElement(resolveTarget('#' + swapOptions.anchor))
1996
+
if (anchorTarget) {
1997
+
anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
0
0
0
0
0
0
0
0
0
0
1998
}
1999
}
2000
+
2001
+
updateScrollState(settleInfo.elts, swapSpec)
2002
+
maybeCall(swapOptions.afterSettleCallback)
2003
+
maybeCall(settleResolve)
2004
}
0
2005
2006
+
if (swapSpec.settleDelay > 0) {
2007
+
getWindow().setTimeout(doSettle, swapSpec.settleDelay)
2008
+
} else {
2009
+
doSettle()
2010
}
0
0
0
0
2011
}
2012
+
let shouldTransition = htmx.config.globalViewTransitions
2013
+
if (swapSpec.hasOwnProperty('transition')) {
2014
+
shouldTransition = swapSpec.transition
0
2015
}
2016
2017
+
const elt = swapOptions.contextElement || getDocument()
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
2018
2019
+
if (shouldTransition &&
2020
+
triggerEvent(elt, 'htmx:beforeTransition', swapOptions.eventInfo) &&
2021
+
typeof Promise !== 'undefined' &&
2022
+
// @ts-ignore experimental feature atm
2023
+
document.startViewTransition) {
2024
+
const settlePromise = new Promise(function(_resolve, _reject) {
2025
+
settleResolve = _resolve
2026
+
settleReject = _reject
2027
+
})
2028
+
// wrap the original doSwap() in a call to startViewTransition()
2029
+
const innerDoSwap = doSwap
2030
+
doSwap = function() {
2031
+
// @ts-ignore experimental feature atm
2032
+
document.startViewTransition(function() {
2033
+
innerDoSwap()
2034
+
return settlePromise
2035
+
})
2036
}
2037
}
2038
2039
+
try {
2040
+
if (swapSpec?.swapDelay && swapSpec.swapDelay > 0) {
2041
+
getWindow().setTimeout(doSwap, swapSpec.swapDelay)
2042
+
} else {
2043
+
doSwap()
2044
+
}
2045
+
} catch (e) {
2046
+
triggerErrorEvent(elt, 'htmx:swapError', swapOptions.eventInfo)
2047
+
maybeCall(settleReject)
2048
+
throw e
2049
}
2050
}
2051
···
2399
if (path == null || path === '') {
2400
// if there is no action attribute on the form set path to current href before the
2401
// following logic to properly clear parameters on a GET (not on a POST!)
2402
+
path = location.href
2403
}
2404
if (verb === 'get' && path.includes('?')) {
2405
path = path.replace(/\?[^#]+/, '')
···
2420
2421
/**
2422
* @param {Event} evt
2423
+
* @param {Element} elt
2424
* @returns {boolean}
2425
*/
2426
+
function shouldCancel(evt, elt) {
0
0
0
0
2427
if (evt.type === 'submit' || evt.type === 'click') {
2428
+
// use elt from event that was submitted/clicked where possible to determining if default form/link behavior should be canceled
2429
+
elt = asElement(evt.target) || elt
2430
if (elt.tagName === 'FORM') {
2431
return true
2432
}
2433
+
// @ts-ignore Do not cancel on buttons that 1) don't have a related form or 2) have a type attribute of 'reset'/'button'.
2434
+
// The properties will resolve to undefined for elements that don't define 'type' or 'form', which is fine
2435
+
if (elt.form && elt.type === 'submit') {
2436
return true
2437
}
2438
+
elt = elt.closest('a')
2439
+
// @ts-ignore check for a link wrapping the event elt or if elt is a link. elt will be link so href check is fine
2440
+
if (elt && elt.href &&
2441
(elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf('#') !== 0)) {
2442
return true
2443
}
···
2477
}
2478
2479
/**
2480
+
* @param {Element} elt
2481
* @param {TriggerHandler} handler
2482
* @param {HtmxNodeInternalData} nodeData
2483
* @param {HtmxTriggerSpecification} triggerSpec
···
2544
}
2545
}
2546
if (triggerSpec.changed) {
2547
+
const node = evt.target
2548
// @ts-ignore value will be undefined for non-input elements, which is fine
2549
const value = node.value
2550
const lastValue = elementData.lastValue.get(triggerSpec)
···
2667
triggerSpecs.forEach(function(triggerSpec) {
2668
addTriggerHandler(elt, triggerSpec, nodeData, function(node, evt) {
2669
const elt = asElement(node)
2670
+
if (eltIsDisabled(elt)) {
2671
cleanUpElement(elt)
2672
return
2673
}
···
2681
2682
/**
2683
* @callback TriggerHandler
2684
+
* @param {Element} elt
2685
* @param {Event} [evt]
2686
*/
2687
2688
/**
2689
+
* @param {Element} elt
2690
* @param {HtmxTriggerSpecification} triggerSpec
2691
* @param {HtmxNodeInternalData} nodeData
2692
* @param {TriggerHandler} handler
···
2811
* @param {Event} evt
2812
*/
2813
function maybeSetLastButtonClicked(evt) {
2814
+
const elt = getTargetButton(evt.target)
2815
const internalData = getRelatedFormData(evt)
2816
if (internalData) {
2817
internalData.lastButtonClicked = elt
···
2826
if (internalData) {
2827
internalData.lastButtonClicked = null
2828
}
2829
+
}
2830
+
2831
+
/**
2832
+
* @param {EventTarget} target
2833
+
* @returns {HTMLButtonElement|HTMLInputElement|null}
2834
+
*/
2835
+
function getTargetButton(target) {
2836
+
return /** @type {HTMLButtonElement|HTMLInputElement|null} */ (closest(asElement(target), "button, input[type='submit']"))
2837
+
}
2838
+
2839
+
/**
2840
+
* @param {Element} elt
2841
+
* @returns {HTMLFormElement|null}
2842
+
*/
2843
+
function getRelatedForm(elt) {
2844
+
// @ts-ignore Get the related form if available, else find the closest parent form
2845
+
return elt.form || closest(elt, 'form')
2846
}
2847
2848
/**
···
2850
* @returns {HtmxNodeInternalData|undefined}
2851
*/
2852
function getRelatedFormData(evt) {
2853
+
const elt = getTargetButton(evt.target)
2854
if (!elt) {
2855
return
2856
}
2857
+
const form = getRelatedForm(elt)
0
0
0
2858
return getInternalData(form)
2859
}
2860
···
2931
* @param {Element|HTMLInputElement} elt
2932
*/
2933
function initNode(elt) {
2934
+
triggerEvent(elt, 'htmx:beforeProcessNode')
2935
+
0
0
2936
const nodeData = getInternalData(elt)
2937
+
const triggerSpecs = getTriggerSpecs(elt)
2938
+
const hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs)
0
0
2939
2940
+
if (!hasExplicitHttpAction) {
2941
+
if (getClosestAttributeValue(elt, 'hx-boost') === 'true') {
2942
+
boostElement(elt, nodeData, triggerSpecs)
2943
+
} else if (hasAttribute(elt, 'hx-trigger')) {
2944
+
triggerSpecs.forEach(function(triggerSpec) {
2945
+
// For "naked" triggers, don't do anything at all
2946
+
addTriggerHandler(elt, triggerSpec, nodeData, function() {
2947
+
})
2948
+
})
2949
+
}
2950
+
}
2951
2952
+
// Handle submit buttons/inputs that have the form attribute set
2953
+
// see https://developer.mozilla.org/docs/Web/HTML/Element/button
2954
+
if (elt.tagName === 'FORM' || (getRawAttribute(elt, 'type') === 'submit' && hasAttribute(elt, 'form'))) {
2955
+
initButtonTracking(elt)
2956
+
}
2957
2958
+
nodeData.firstInitCompleted = true
2959
+
triggerEvent(elt, 'htmx:afterProcessNode')
2960
+
}
2961
2962
+
/**
2963
+
* @param {Element} elt
2964
+
* @returns {boolean}
2965
+
*/
2966
+
function maybeDeInitAndHash(elt) {
2967
+
// Ensure only valid Elements and not shadow DOM roots are inited
2968
+
if (!(elt instanceof Element)) {
2969
+
return false
2970
+
}
0
0
0
0
0
0
0
0
2971
2972
+
const nodeData = getInternalData(elt)
2973
+
const hash = attributeHash(elt)
2974
+
if (nodeData.initHash !== hash) {
2975
+
deInitNode(elt)
2976
+
nodeData.initHash = hash
2977
+
return true
2978
}
2979
+
return false
2980
}
2981
2982
/**
···
2988
*/
2989
function processNode(elt) {
2990
elt = resolveTarget(elt)
2991
+
if (eltIsDisabled(elt)) {
2992
cleanUpElement(elt)
2993
return
2994
}
2995
+
2996
+
const elementsToInit = []
2997
+
if (maybeDeInitAndHash(elt)) {
2998
+
elementsToInit.push(elt)
2999
+
}
3000
+
forEach(findElementsToProcess(elt), function(child) {
3001
+
if (eltIsDisabled(child)) {
3002
+
cleanUpElement(child)
3003
+
return
3004
+
}
3005
+
if (maybeDeInitAndHash(child)) {
3006
+
elementsToInit.push(child)
3007
+
}
3008
+
})
3009
+
3010
forEach(findHxOnWildcardElements(elt), processHxOnWildcard)
3011
+
forEach(elementsToInit, initNode)
3012
}
3013
3014
//= ===================================================================
···
3029
* @returns {CustomEvent}
3030
*/
3031
function makeEvent(eventName, detail) {
3032
+
// TODO: `composed: true` here is a hack to make global event handlers work with events in shadow DOM
3033
+
// This breaks expected encapsulation but needs to be here until decided otherwise by core devs
3034
+
return new CustomEvent(eventName, { bubbles: true, cancelable: true, composed: true, detail })
0
0
0
0
0
0
0
3035
}
3036
3037
/**
···
3053
3054
/**
3055
* `withExtensions` locates all active extensions for a provided element, then
3056
+
* executes the provided function using each of the active extensions. You can filter
3057
+
* the element's extensions by giving it a list of extensions to ignore. It should
3058
* be called internally at every extendable execution point in htmx.
3059
*
3060
* @param {Element} elt
3061
* @param {(extension:HtmxExtension) => void} toDo
3062
+
* @param {string[]=} extensionsToIgnore
3063
* @returns void
3064
*/
3065
+
function withExtensions(elt, toDo, extensionsToIgnore) {
3066
+
forEach(getExtensions(elt, [], extensionsToIgnore), function(extension) {
3067
try {
3068
toDo(extension)
3069
} catch (e) {
···
3073
}
3074
3075
function logError(msg) {
3076
+
console.error(msg)
0
0
0
0
3077
}
3078
3079
/**
···
3118
let currentPathForHistory = location.pathname + location.search
3119
3120
/**
3121
+
* @param {string} path
3122
+
*/
3123
+
function setCurrentPathForHistory(path) {
3124
+
currentPathForHistory = path
3125
+
if (canAccessLocalStorage()) {
3126
+
sessionStorage.setItem('htmx-current-path-for-history', path)
3127
+
}
3128
+
}
3129
+
3130
+
/**
3131
* @returns {Element}
3132
*/
3133
function getHistoryElement() {
···
3151
3152
if (htmx.config.historyCacheSize <= 0) {
3153
// make sure that an eventually already existing cache is purged
3154
+
sessionStorage.removeItem('htmx-history-cache')
3155
return
3156
}
3157
3158
url = normalizePath(url)
3159
3160
+
const historyCache = parseJSON(sessionStorage.getItem('htmx-history-cache')) || []
3161
for (let i = 0; i < historyCache.length; i++) {
3162
if (historyCache[i].url === url) {
3163
historyCache.splice(i, 1)
···
3178
// keep trying to save the cache until it succeeds or is empty
3179
while (historyCache.length > 0) {
3180
try {
3181
+
sessionStorage.setItem('htmx-history-cache', JSON.stringify(historyCache))
3182
break
3183
} catch (e) {
3184
triggerErrorEvent(getDocument().body, 'htmx:historyCacheError', { cause: e, cache: historyCache })
···
3206
3207
url = normalizePath(url)
3208
3209
+
const historyCache = parseJSON(sessionStorage.getItem('htmx-history-cache')) || []
3210
for (let i = 0; i < historyCache.length; i++) {
3211
if (historyCache[i].url === url) {
3212
return historyCache[i]
···
3234
3235
function saveCurrentPageToHistory() {
3236
const elt = getHistoryElement()
3237
+
let path = currentPathForHistory
3238
+
if (canAccessLocalStorage()) {
3239
+
path = sessionStorage.getItem('htmx-current-path-for-history')
3240
+
}
3241
+
path = path || location.pathname + location.search
3242
3243
// Allow history snapshot feature to be disabled where hx-history="false"
3244
// is present *anywhere* in the current document we're about to save,
3245
// so we can prevent privileged data entering the cache.
3246
// The page will still be reachable as a history entry, but htmx will fetch it
3247
+
// live from the server onpopstate rather than look in the sessionStorage cache
3248
+
const disableHistoryCache = getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]')
0
0
0
0
0
0
3249
if (!disableHistoryCache) {
3250
triggerEvent(getDocument().body, 'htmx:beforeHistorySave', { path, historyElt: elt })
3251
saveToHistoryCache(path, elt)
3252
}
3253
3254
+
if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, getDocument().title, location.href)
3255
}
3256
3257
/**
···
3268
if (htmx.config.historyEnabled) {
3269
history.pushState({ htmx: true }, '', path)
3270
}
3271
+
setCurrentPathForHistory(path)
3272
}
3273
3274
/**
···
3276
*/
3277
function replaceUrlInHistory(path) {
3278
if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, '', path)
3279
+
setCurrentPathForHistory(path)
3280
}
3281
3282
/**
···
3293
*/
3294
function loadHistoryFromServer(path) {
3295
const request = new XMLHttpRequest()
3296
+
const swapSpec = { swapStyle: 'innerHTML', swapDelay: 0, settleDelay: 0 }
3297
+
const details = { path, xhr: request, historyElt: getHistoryElement(), swapSpec }
3298
request.open('GET', path, true)
3299
+
if (htmx.config.historyRestoreAsHxRequest) {
3300
+
request.setRequestHeader('HX-Request', 'true')
3301
+
}
3302
request.setRequestHeader('HX-History-Restore-Request', 'true')
3303
+
request.setRequestHeader('HX-Current-URL', location.href)
3304
request.onload = function() {
3305
if (this.status >= 200 && this.status < 400) {
3306
+
details.response = this.response
3307
triggerEvent(getDocument().body, 'htmx:historyCacheMissLoad', details)
3308
+
swap(details.historyElt, details.response, swapSpec, {
3309
+
contextElement: details.historyElt,
3310
+
historyRequest: true
3311
+
})
3312
+
setCurrentPathForHistory(details.path)
3313
+
triggerEvent(getDocument().body, 'htmx:historyRestore', { path, cacheMiss: true, serverResponse: details.response })
0
0
0
0
0
0
0
3314
} else {
3315
triggerErrorEvent(getDocument().body, 'htmx:historyCacheMissLoadError', details)
3316
}
3317
}
3318
+
if (triggerEvent(getDocument().body, 'htmx:historyCacheMiss', details)) {
3319
+
request.send() // only send request if event not prevented
3320
+
}
3321
}
3322
3323
/**
···
3328
path = path || location.pathname + location.search
3329
const cached = getCachedHistory(path)
3330
if (cached) {
3331
+
const swapSpec = { swapStyle: 'innerHTML', swapDelay: 0, settleDelay: 0, scroll: cached.scroll }
3332
+
const details = { path, item: cached, historyElt: getHistoryElement(), swapSpec }
3333
+
if (triggerEvent(getDocument().body, 'htmx:historyCacheHit', details)) {
3334
+
swap(details.historyElt, cached.content, swapSpec, {
3335
+
contextElement: details.historyElt,
3336
+
title: cached.title
3337
+
})
3338
+
setCurrentPathForHistory(details.path)
3339
+
triggerEvent(getDocument().body, 'htmx:historyRestore', details)
3340
+
}
0
0
0
3341
} else {
3342
if (htmx.config.refreshOnHistoryMiss) {
3343
// @ts-ignore: optional parameter in reload() function throws error
3344
// noinspection JSUnresolvedReference
3345
+
htmx.location.reload(true)
3346
} else {
3347
loadHistoryFromServer(path)
3348
}
···
3447
return true
3448
}
3449
3450
+
/**
3451
+
* @param {string} name
3452
* @param {string|Array|FormDataEntryValue} value
3453
* @param {FormData} formData */
3454
function addValueToFormData(name, value, formData) {
···
3461
}
3462
}
3463
3464
+
/**
3465
+
* @param {string} name
3466
* @param {string|Array} value
3467
* @param {FormData} formData */
3468
function removeValueFromFormData(name, value, formData) {
···
3479
}
3480
3481
/**
3482
+
* @param {Element} elt
3483
+
* @returns {string|Array}
3484
+
*/
3485
+
function getValueFromInput(elt) {
3486
+
if (elt instanceof HTMLSelectElement && elt.multiple) {
3487
+
return toArray(elt.querySelectorAll('option:checked')).map(function(e) { return (/** @type HTMLOptionElement */(e)).value })
3488
+
}
3489
+
// include file inputs
3490
+
if (elt instanceof HTMLInputElement && elt.files) {
3491
+
return toArray(elt.files)
3492
+
}
3493
+
// @ts-ignore value will be undefined for non-input elements, which is fine
3494
+
return elt.value
3495
+
}
3496
+
3497
+
/**
3498
* @param {Element[]} processed
3499
* @param {FormData} formData
3500
* @param {HtmxElementValidationError[]} errors
···
3509
}
3510
if (shouldInclude(elt)) {
3511
const name = getRawAttribute(elt, 'name')
3512
+
addValueToFormData(name, getValueFromInput(elt), formData)
0
0
0
0
0
0
0
0
0
3513
if (validate) {
3514
validateElement(elt, errors)
3515
}
···
3520
// The input has already been processed and added to the values, but the FormData that will be
3521
// constructed right after on the form, will include it once again. So remove that input's value
3522
// now to avoid duplicates
3523
+
removeValueFromFormData(input.name, getValueFromInput(input), formData)
3524
} else {
3525
processed.push(input)
3526
}
···
3538
}
3539
3540
/**
0
3541
* @param {Element} elt
3542
* @param {HtmxElementValidationError[]} errors
3543
*/
···
3592
validate = validate && internalData.lastButtonClicked.formNoValidate !== true
3593
}
3594
3595
+
// for a non-GET include the related form, which may or may not be a parent element of elt
3596
if (verb !== 'get') {
3597
+
processInputValue(processed, priorityFormData, errors, getRelatedForm(elt), validate)
3598
}
3599
3600
// include the element itself
···
3674
'HX-Trigger': getRawAttribute(elt, 'id'),
3675
'HX-Trigger-Name': getRawAttribute(elt, 'name'),
3676
'HX-Target': getAttributeValue(target, 'id'),
3677
+
'HX-Current-URL': location.href
3678
}
3679
getValuesForElement(elt, 'hx-headers', false, headers)
3680
if (prompt !== undefined) {
···
3852
target = target || last
3853
target.scrollTop = target.scrollHeight
3854
}
3855
+
if (typeof swapSpec.scroll === 'number') {
3856
+
getWindow().setTimeout(function() {
3857
+
window.scrollTo(0, /** @type number */ (swapSpec.scroll))
3858
+
}, 0) // next 'tick', so browser has time to render layout
3859
+
}
3860
}
3861
if (swapSpec.show) {
3862
var target = null
···
3885
* @param {string} attr
3886
* @param {boolean=} evalAsDefault
3887
* @param {Object=} values
3888
+
* @param {Event=} event
3889
* @returns {Object}
3890
*/
3891
+
function getValuesForElement(elt, attr, evalAsDefault, values, event) {
3892
if (values == null) {
3893
values = {}
3894
}
···
3914
}
3915
let varsValues
3916
if (evaluateValue) {
3917
+
varsValues = maybeEval(elt, function() {
3918
+
if (event) {
3919
+
return Function('event', 'return (' + str + ')').call(elt, event)
3920
+
} else { // allow window.event to be accessible
3921
+
return Function('return (' + str + ')').call(elt)
3922
+
}
3923
+
}, {})
3924
} else {
3925
varsValues = parseJSON(str)
3926
}
···
3932
}
3933
}
3934
}
3935
+
return getValuesForElement(asElement(parentElt(elt)), attr, evalAsDefault, values, event)
3936
}
3937
3938
/**
···
3952
3953
/**
3954
* @param {Element} elt
3955
+
* @param {Event=} event
3956
+
* @param {*?=} expressionVars
3957
* @returns
3958
*/
3959
+
function getHXVarsForElement(elt, event, expressionVars) {
3960
+
return getValuesForElement(elt, 'hx-vars', true, expressionVars, event)
3961
}
3962
3963
/**
3964
* @param {Element} elt
3965
+
* @param {Event=} event
3966
+
* @param {*?=} expressionVars
3967
* @returns
3968
*/
3969
+
function getHXValsForElement(elt, event, expressionVars) {
3970
+
return getValuesForElement(elt, 'hx-vals', false, expressionVars, event)
3971
}
3972
3973
/**
3974
* @param {Element} elt
3975
+
* @param {Event=} event
3976
* @returns {FormData}
3977
*/
3978
+
function getExpressionVars(elt, event) {
3979
+
return mergeObjects(getHXVarsForElement(elt, event), getHXValsForElement(elt, event))
3980
}
3981
3982
/**
···
4001
* @return {string}
4002
*/
4003
function getPathFromResponse(xhr) {
4004
+
if (xhr.responseURL) {
0
4005
try {
4006
const url = new URL(xhr.responseURL)
4007
return url.pathname + url.search
···
4083
* @return {boolean}
4084
*/
4085
function verifyPath(elt, path, requestConfig) {
4086
+
const url = new URL(path, location.protocol !== 'about:' ? location.href : window.origin)
4087
+
const origin = location.protocol !== 'about:' ? location.origin : window.origin
4088
+
const sameHost = origin === url.origin
0
0
0
0
0
0
0
0
4089
4090
if (htmx.config.selfRequestsOnly) {
4091
if (!sameHost) {
···
4186
return function() {
4187
return formData[name].apply(formData, arguments)
4188
}
0
0
4189
}
4190
}
4191
const array = formData.getAll(name)
···
4260
}
4261
const target = etc.targetOverride || asElement(getTarget(elt))
4262
if (target == null || target == DUMMY_ELT) {
4263
+
triggerErrorEvent(elt, 'htmx:targetError', { target: getClosestAttributeValue(elt, 'hx-target') })
4264
maybeCall(reject)
4265
return promise
4266
}
···
4276
4277
const buttonVerb = getRawAttribute(submitter, 'formmethod')
4278
if (buttonVerb != null) {
4279
+
if (VERBS.includes(buttonVerb.toLowerCase())) {
0
4280
verb = (/** @type HttpVerb */(buttonVerb))
4281
+
} else {
4282
+
maybeCall(resolve)
4283
+
return promise
4284
}
4285
}
4286
}
···
4415
if (etc.values) {
4416
overrideFormData(rawFormData, formDataFromObject(etc.values))
4417
}
4418
+
const expressionVars = formDataFromObject(getExpressionVars(elt, event))
4419
const allFormData = overrideFormData(rawFormData, expressionVars)
4420
let filteredFormData = filterValues(allFormData, elt)
4421
···
4425
4426
// behavior of anchors w/ empty href is to use the current URL
4427
if (path == null || path === '') {
4428
+
path = location.href
4429
}
4430
4431
/**
···
4449
unfilteredFormData: allFormData,
4450
unfilteredParameters: formDataProxy(allFormData),
4451
headers,
4452
+
elt,
4453
target,
4454
verb,
4455
errors,
···
4504
if (!verifyPath(elt, finalPath, requestConfig)) {
4505
triggerErrorEvent(elt, 'htmx:invalidPath', requestConfig)
4506
maybeCall(reject)
4507
+
endRequestLock()
4508
return promise
4509
}
4510
···
4567
}
4568
}
4569
maybeCall(resolve)
0
4570
} catch (e) {
4571
triggerErrorEvent(elt, 'htmx:onLoadError', mergeObjects({ error: e }, responseInfo))
4572
throw e
4573
+
} finally {
4574
+
endRequestLock()
4575
}
4576
}
4577
xhr.onerror = function() {
···
4746
if (title) {
4747
const titleElt = find('title')
4748
if (titleElt) {
4749
+
titleElt.textContent = title
4750
} else {
4751
window.document.title = title
4752
}
···
4754
}
4755
4756
/**
4757
+
* Resove the Retarget selector and throw if not found
4758
+
* @param {Element} elt
4759
+
* @param {String} target
4760
+
* @returns {Element}
4761
+
*/
4762
+
function resolveRetarget(elt, target) {
4763
+
if (target === 'this') {
4764
+
return elt
4765
+
}
4766
+
const resolvedTarget = asElement(querySelectorExt(elt, target))
4767
+
if (resolvedTarget == null) {
4768
+
triggerErrorEvent(elt, 'htmx:targetError', { target })
4769
+
throw new Error(`Invalid re-target ${target}`)
4770
+
}
4771
+
return resolvedTarget
4772
+
}
4773
+
4774
+
/**
4775
* @param {Element} elt
4776
* @param {HtmxResponseInfo} responseInfo
4777
*/
···
4808
4809
if (hasHeader(xhr, /HX-Redirect:/i)) {
4810
responseInfo.keepIndicators = true
4811
+
htmx.location.href = xhr.getResponseHeader('HX-Redirect')
4812
+
shouldRefresh && htmx.location.reload()
4813
return
4814
}
4815
4816
if (shouldRefresh) {
4817
responseInfo.keepIndicators = true
4818
+
htmx.location.reload()
4819
return
4820
}
4821
0
0
0
0
0
0
0
0
4822
const historyUpdate = determineHistoryUpdates(elt, responseInfo)
4823
4824
const responseHandling = resolveResponseHandling(xhr)
···
4827
let ignoreTitle = htmx.config.ignoreTitle || responseHandling.ignoreTitle
4828
let selectOverride = responseHandling.select
4829
if (responseHandling.target) {
4830
+
responseInfo.target = resolveRetarget(elt, responseHandling.target)
4831
}
4832
var swapOverride = etc.swapOverride
4833
if (swapOverride == null && responseHandling.swapOverride) {
···
4836
4837
// response headers override response handling config
4838
if (hasHeader(xhr, /HX-Retarget:/i)) {
4839
+
responseInfo.target = resolveRetarget(elt, xhr.getResponseHeader('HX-Retarget'))
0
0
0
0
4840
}
4841
+
4842
if (hasHeader(xhr, /HX-Reswap:/i)) {
4843
swapOverride = xhr.getResponseHeader('HX-Reswap')
4844
}
···
4891
4892
target.classList.add(htmx.config.swappingClass)
4893
0
0
0
0
4894
if (responseInfoSelect) {
4895
selectOverride = responseInfoSelect
4896
}
···
4902
const selectOOB = getClosestAttributeValue(elt, 'hx-select-oob')
4903
const select = getClosestAttributeValue(elt, 'hx-select')
4904
4905
+
swap(target, serverResponse, swapSpec, {
4906
+
select: selectOverride === 'unset' ? null : selectOverride || select,
4907
+
selectOOB,
4908
+
eventInfo: responseInfo,
4909
+
anchor: responseInfo.pathInfo.anchor,
4910
+
contextElement: elt,
4911
+
afterSwapCallback: function() {
4912
+
if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
4913
+
let finalElt = elt
4914
+
if (!bodyContains(elt)) {
4915
+
finalElt = getDocument().body
4916
+
}
4917
+
handleTriggerHeader(xhr, 'HX-Trigger-After-Swap', finalElt)
4918
+
}
4919
+
},
4920
+
afterSettleCallback: function() {
4921
+
if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
4922
+
let finalElt = elt
4923
+
if (!bodyContains(elt)) {
4924
+
finalElt = getDocument().body
4925
+
}
4926
+
handleTriggerHeader(xhr, 'HX-Trigger-After-Settle', finalElt)
4927
+
}
4928
+
},
4929
+
beforeSwapCallback: function() {
4930
// if we need to save history, do so, before swapping so that relative resources have the correct base URL
4931
if (historyUpdate.type) {
4932
triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo))
···
4938
triggerEvent(getDocument().body, 'htmx:replacedInHistory', { path: historyUpdate.path })
4939
}
4940
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
4941
}
4942
+
})
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
4943
}
4944
if (isError) {
4945
triggerErrorEvent(elt, 'htmx:responseError', mergeObjects({ error: 'Response Status Error Code ' + xhr.status + ' from ' + responseInfo.pathInfo.requestPath }, responseInfo))
···
5140
* @property {Element} [contextElement]
5141
* @property {swapCallback} [afterSwapCallback]
5142
* @property {swapCallback} [afterSettleCallback]
5143
+
* @property {swapCallback} [beforeSwapCallback]
5144
+
* @property {string} [title]
5145
+
* @property {boolean} [historyRequest]
5146
*/
5147
5148
/**
···
5161
* @property {boolean} [transition]
5162
* @property {boolean} [ignoreTitle]
5163
* @property {string} [head]
5164
+
* @property {'top' | 'bottom' | number } [scroll]
5165
* @property {string} [scrollTarget]
5166
* @property {string} [show]
5167
* @property {string} [showTarget]
···
5206
* @property {'true'} [HX-History-Restore-Request]
5207
*/
5208
5209
+
/**
5210
+
* @typedef HtmxAjaxHelperContext
5211
* @property {Element|string} [source]
5212
* @property {Event} [event]
5213
* @property {HtmxAjaxHandler} [handler]
···
5227
* @property {FormData} unfilteredFormData
5228
* @property {Object} unfilteredParameters unfilteredFormData proxy
5229
* @property {HtmxHeaderSpecification} headers
5230
+
* @property {Element} elt
5231
* @property {Element} target
5232
* @property {HttpVerb} verb
5233
* @property {HtmxElementValidationError[]} errors
···
5301
* @see https://github.com/bigskysoftware/htmx-extensions/blob/main/README.md
5302
* @typedef {Object} HtmxExtension
5303
* @property {(api: any) => void} init
5304
+
* @property {(name: string, event: CustomEvent) => boolean} onEvent
5305
* @property {(text: string, xhr: XMLHttpRequest, elt: Element) => string} transformResponse
5306
* @property {(swapStyle: HtmxSwapStyle) => boolean} isInlineSwap
5307
* @property {(swapStyle: HtmxSwapStyle, target: Node, fragment: Node, settleInfo: HtmxSettleInfo) => boolean|Node[]} handleSwap