htmx.org 2.0.3 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/htmx.cjs.js CHANGED
@@ -82,7 +82,7 @@ var htmx = (function() {
82
82
  */
83
83
  historyEnabled: true,
84
84
  /**
85
- * The number of pages to keep in **localStorage** for history support.
85
+ * The number of pages to keep in **sessionStorage** for history support.
86
86
  * @type number
87
87
  * @default 10
88
88
  */
@@ -271,13 +271,25 @@ var htmx = (function() {
271
271
  * @type boolean
272
272
  * @default true
273
273
  */
274
- allowNestedOobSwaps: true
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
275
282
  },
276
283
  /** @type {typeof parseInterval} */
277
284
  parseInterval: null,
285
+ /**
286
+ * proxy of window.location used for page reload functions
287
+ * @type location
288
+ */
289
+ location,
278
290
  /** @type {typeof internalEval} */
279
291
  _: null,
280
- version: '2.0.3'
292
+ version: '2.0.5'
281
293
  }
282
294
  // Tsc madness part 2
283
295
  htmx.onLoad = onLoadHelper
@@ -484,10 +496,7 @@ var htmx = (function() {
484
496
  * @returns {boolean}
485
497
  */
486
498
  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)
499
+ return elt instanceof Element && elt.matches(selector)
491
500
  }
492
501
 
493
502
  /**
@@ -693,6 +702,7 @@ var htmx = (function() {
693
702
  * @property {XMLHttpRequest} [xhr]
694
703
  * @property {(() => void)[]} [queuedRequests]
695
704
  * @property {boolean} [abortable]
705
+ * @property {boolean} [firstInitCompleted]
696
706
  *
697
707
  * Event data
698
708
  * @property {HtmxTriggerSpecification} [triggerSpec]
@@ -754,17 +764,14 @@ var htmx = (function() {
754
764
  }
755
765
 
756
766
  /**
767
+ * Checks whether the element is in the document (includes shadow roots).
768
+ * This function this is a slight misnomer; it will return true even for elements in the head.
769
+ *
757
770
  * @param {Node} elt
758
771
  * @returns {boolean}
759
772
  */
760
773
  function bodyContains(elt) {
761
- // IE Fix
762
- const rootNode = elt.getRootNode && elt.getRootNode()
763
- if (rootNode && rootNode instanceof window.ShadowRoot) {
764
- return getDocument().body.contains(rootNode.host)
765
- } else {
766
- return getDocument().body.contains(elt)
767
- }
774
+ return elt.getRootNode({ composed: true }) === document
768
775
  }
769
776
 
770
777
  /**
@@ -812,10 +819,10 @@ var htmx = (function() {
812
819
  * @returns {boolean}
813
820
  */
814
821
  function canAccessLocalStorage() {
815
- const test = 'htmx:localStorageTest'
822
+ const test = 'htmx:sessionStorageTest'
816
823
  try {
817
- localStorage.setItem(test, test)
818
- localStorage.removeItem(test)
824
+ sessionStorage.setItem(test, test)
825
+ sessionStorage.removeItem(test)
819
826
  return true
820
827
  } catch (e) {
821
828
  return false
@@ -827,20 +834,16 @@ var htmx = (function() {
827
834
  * @returns {string}
828
835
  */
829
836
  function normalizePath(path) {
830
- try {
831
- const url = new URL(path)
832
- if (url) {
833
- path = url.pathname + url.search
834
- }
835
- // remove trailing slash, unless index page
836
- if (!(/^\/$/.test(path))) {
837
- path = path.replace(/\/+$/, '')
838
- }
839
- return path
840
- } catch (e) {
841
- // be kind to IE11, which doesn't support URL()
842
- return 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(/\/+$/, '')
843
845
  }
846
+ return path
844
847
  }
845
848
 
846
849
  //= =========================================================================================
@@ -1076,18 +1079,10 @@ var htmx = (function() {
1076
1079
  */
1077
1080
  function closest(elt, selector) {
1078
1081
  elt = asElement(resolveTarget(elt))
1079
- if (elt && elt.closest) {
1082
+ if (elt) {
1080
1083
  return elt.closest(selector)
1081
- } else {
1082
- // TODO remove when IE goes away
1083
- do {
1084
- if (elt == null || matches(elt, selector)) {
1085
- return elt
1086
- }
1087
- }
1088
- while (elt = elt && asElement(parentElt(elt)))
1089
- return null
1090
1084
  }
1085
+ return null
1091
1086
  }
1092
1087
 
1093
1088
  /**
@@ -1128,34 +1123,77 @@ var htmx = (function() {
1128
1123
  * @returns {(Node|Window)[]}
1129
1124
  */
1130
1125
  function querySelectorAllExt(elt, selector, global) {
1131
- elt = resolveTarget(elt)
1132
- if (selector.indexOf('closest ') === 0) {
1133
- return [closest(asElement(elt), normalizeSelector(selector.substr(8)))]
1134
- } else if (selector.indexOf('find ') === 0) {
1135
- return [find(asParentNode(elt), normalizeSelector(selector.substr(5)))]
1136
- } else if (selector === 'next') {
1137
- return [asElement(elt).nextElementSibling]
1138
- } else if (selector.indexOf('next ') === 0) {
1139
- return [scanForwardQuery(elt, normalizeSelector(selector.substr(5)), !!global)]
1140
- } else if (selector === 'previous') {
1141
- return [asElement(elt).previousElementSibling]
1142
- } else if (selector.indexOf('previous ') === 0) {
1143
- return [scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)), !!global)]
1144
- } else if (selector === 'document') {
1145
- return [document]
1146
- } else if (selector === 'window') {
1147
- return [window]
1148
- } else if (selector === 'body') {
1149
- return [document.body]
1150
- } else if (selector === 'root') {
1151
- return [getRootNode(elt, !!global)]
1152
- } else if (selector === 'host') {
1153
- return [(/** @type ShadowRoot */(elt.getRootNode())).host]
1154
- } else if (selector.indexOf('global ') === 0) {
1126
+ if (selector.indexOf('global ') === 0) {
1155
1127
  return querySelectorAllExt(elt, selector.slice(7), true)
1156
- } else {
1157
- return toArray(asParentNode(getRootNode(elt, !!global)).querySelectorAll(normalizeSelector(selector)))
1158
1128
  }
1129
+
1130
+ elt = resolveTarget(elt)
1131
+
1132
+ const parts = []
1133
+ {
1134
+ let chevronsCount = 0
1135
+ let offset = 0
1136
+ for (let i = 0; i < selector.length; i++) {
1137
+ const char = selector[i]
1138
+ if (char === ',' && chevronsCount === 0) {
1139
+ parts.push(selector.substring(offset, i))
1140
+ offset = i + 1
1141
+ continue
1142
+ }
1143
+ if (char === '<') {
1144
+ chevronsCount++
1145
+ } else if (char === '/' && i < selector.length - 1 && selector[i + 1] === '>') {
1146
+ chevronsCount--
1147
+ }
1148
+ }
1149
+ if (offset < selector.length) {
1150
+ parts.push(selector.substring(offset))
1151
+ }
1152
+ }
1153
+
1154
+ const result = []
1155
+ const unprocessedParts = []
1156
+ while (parts.length > 0) {
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') {
1174
+ item = window
1175
+ } else if (selector === 'body') {
1176
+ item = document.body
1177
+ } else if (selector === 'root') {
1178
+ item = getRootNode(elt, !!global)
1179
+ } else if (selector === 'host') {
1180
+ item = (/** @type ShadowRoot */(elt.getRootNode())).host
1181
+ } else {
1182
+ unprocessedParts.push(selector)
1183
+ }
1184
+
1185
+ if (item) {
1186
+ result.push(item)
1187
+ }
1188
+ }
1189
+
1190
+ if (unprocessedParts.length > 0) {
1191
+ const standardSelector = unprocessedParts.join(',')
1192
+ const rootNode = asParentNode(getRootNode(elt, !!global))
1193
+ result.push(...toArray(rootNode.querySelectorAll(standardSelector)))
1194
+ }
1195
+
1196
+ return result
1159
1197
  }
1160
1198
 
1161
1199
  /**
@@ -1309,6 +1347,16 @@ var htmx = (function() {
1309
1347
  return [findThisElement(elt, attrName)]
1310
1348
  } else {
1311
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
+ }
1312
1360
  if (result.length === 0) {
1313
1361
  logError('The selector "' + attrTarget + '" on ' + attrName + ' returned no matches!')
1314
1362
  return [DUMMY_ELT]
@@ -1357,13 +1405,7 @@ var htmx = (function() {
1357
1405
  * @returns {boolean}
1358
1406
  */
1359
1407
  function shouldSettleAttribute(name) {
1360
- const attributesToSettle = htmx.config.attributesToSettle
1361
- for (let i = 0; i < attributesToSettle.length; i++) {
1362
- if (name === attributesToSettle[i]) {
1363
- return true
1364
- }
1365
- }
1366
- return false
1408
+ return htmx.config.attributesToSettle.includes(name)
1367
1409
  }
1368
1410
 
1369
1411
  /**
@@ -1412,14 +1454,14 @@ var htmx = (function() {
1412
1454
  */
1413
1455
  function oobSwap(oobValue, oobElement, settleInfo, rootNode) {
1414
1456
  rootNode = rootNode || getDocument()
1415
- let selector = '#' + getRawAttribute(oobElement, 'id')
1457
+ let selector = '#' + CSS.escape(getRawAttribute(oobElement, 'id'))
1416
1458
  /** @type HtmxSwapStyle */
1417
1459
  let swapStyle = 'outerHTML'
1418
1460
  if (oobValue === 'true') {
1419
1461
  // do nothing
1420
1462
  } else if (oobValue.indexOf(':') > 0) {
1421
- swapStyle = oobValue.substr(0, oobValue.indexOf(':'))
1422
- selector = oobValue.substr(oobValue.indexOf(':') + 1, oobValue.length)
1463
+ swapStyle = oobValue.substring(0, oobValue.indexOf(':'))
1464
+ selector = oobValue.substring(oobValue.indexOf(':') + 1)
1423
1465
  } else {
1424
1466
  swapStyle = oobValue
1425
1467
  }
@@ -1427,7 +1469,7 @@ var htmx = (function() {
1427
1469
  oobElement.removeAttribute('data-hx-swap-oob')
1428
1470
 
1429
1471
  const targets = querySelectorAllExt(rootNode, selector, false)
1430
- if (targets) {
1472
+ if (targets.length) {
1431
1473
  forEach(
1432
1474
  targets,
1433
1475
  function(target) {
@@ -1585,14 +1627,11 @@ var htmx = (function() {
1585
1627
  */
1586
1628
  function attributeHash(elt) {
1587
1629
  let hash = 0
1588
- // IE fix
1589
- if (elt.attributes) {
1590
- for (let i = 0; i < elt.attributes.length; i++) {
1591
- const attribute = elt.attributes[i]
1592
- if (attribute.value) { // only include attributes w/ actual values (empty is same as non-existent)
1593
- hash = stringHash(attribute.name, hash)
1594
- hash = stringHash(attribute.value, hash)
1595
- }
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)
1596
1635
  }
1597
1636
  }
1598
1637
  return hash
@@ -1628,7 +1667,7 @@ var htmx = (function() {
1628
1667
  })
1629
1668
  }
1630
1669
  deInitOnHandlers(element)
1631
- forEach(Object.keys(internalData), function(key) { delete internalData[key] })
1670
+ forEach(Object.keys(internalData), function(key) { if (key !== 'firstInitCompleted') delete internalData[key] })
1632
1671
  }
1633
1672
 
1634
1673
  /**
@@ -1637,21 +1676,17 @@ var htmx = (function() {
1637
1676
  function cleanUpElement(element) {
1638
1677
  triggerEvent(element, 'htmx:beforeCleanupElement')
1639
1678
  deInitNode(element)
1640
- // @ts-ignore IE11 code
1641
- // noinspection JSUnresolvedReference
1642
- if (element.children) { // IE
1643
- // @ts-ignore
1644
- forEach(element.children, function(child) { cleanUpElement(child) })
1645
- }
1679
+ // @ts-ignore
1680
+ forEach(element.children, function(child) { cleanUpElement(child) })
1646
1681
  }
1647
1682
 
1648
1683
  /**
1649
- * @param {Node} target
1684
+ * @param {Element} target
1650
1685
  * @param {ParentNode} fragment
1651
1686
  * @param {HtmxSettleInfo} settleInfo
1652
1687
  */
1653
1688
  function swapOuterHTML(target, fragment, settleInfo) {
1654
- if (target instanceof Element && target.tagName === 'BODY') { // special case the body to innerHTML because DocumentFragments can't contain a body elt unfortunately
1689
+ if (target.tagName === 'BODY') { // special case the body to innerHTML because DocumentFragments can't contain a body elt unfortunately
1655
1690
  return swapInnerHTML(target, fragment, settleInfo)
1656
1691
  }
1657
1692
  /** @type {Node} */
@@ -1677,15 +1712,11 @@ var htmx = (function() {
1677
1712
  newElt = newElt.nextSibling
1678
1713
  }
1679
1714
  cleanUpElement(target)
1680
- if (target instanceof Element) {
1681
- target.remove()
1682
- } else {
1683
- target.parentNode.removeChild(target)
1684
- }
1715
+ target.remove()
1685
1716
  }
1686
1717
 
1687
1718
  /**
1688
- * @param {Node} target
1719
+ * @param {Element} target
1689
1720
  * @param {ParentNode} fragment
1690
1721
  * @param {HtmxSettleInfo} settleInfo
1691
1722
  */
@@ -1694,7 +1725,7 @@ var htmx = (function() {
1694
1725
  }
1695
1726
 
1696
1727
  /**
1697
- * @param {Node} target
1728
+ * @param {Element} target
1698
1729
  * @param {ParentNode} fragment
1699
1730
  * @param {HtmxSettleInfo} settleInfo
1700
1731
  */
@@ -1703,7 +1734,7 @@ var htmx = (function() {
1703
1734
  }
1704
1735
 
1705
1736
  /**
1706
- * @param {Node} target
1737
+ * @param {Element} target
1707
1738
  * @param {ParentNode} fragment
1708
1739
  * @param {HtmxSettleInfo} settleInfo
1709
1740
  */
@@ -1712,7 +1743,7 @@ var htmx = (function() {
1712
1743
  }
1713
1744
 
1714
1745
  /**
1715
- * @param {Node} target
1746
+ * @param {Element} target
1716
1747
  * @param {ParentNode} fragment
1717
1748
  * @param {HtmxSettleInfo} settleInfo
1718
1749
  */
@@ -1721,7 +1752,7 @@ var htmx = (function() {
1721
1752
  }
1722
1753
 
1723
1754
  /**
1724
- * @param {Node} target
1755
+ * @param {Element} target
1725
1756
  */
1726
1757
  function swapDelete(target) {
1727
1758
  cleanUpElement(target)
@@ -1732,7 +1763,7 @@ var htmx = (function() {
1732
1763
  }
1733
1764
 
1734
1765
  /**
1735
- * @param {Node} target
1766
+ * @param {Element} target
1736
1767
  * @param {ParentNode} fragment
1737
1768
  * @param {HtmxSettleInfo} settleInfo
1738
1769
  */
@@ -1752,7 +1783,7 @@ var htmx = (function() {
1752
1783
  /**
1753
1784
  * @param {HtmxSwapStyle} swapStyle
1754
1785
  * @param {Element} elt
1755
- * @param {Node} target
1786
+ * @param {Element} target
1756
1787
  * @param {ParentNode} fragment
1757
1788
  * @param {HtmxSettleInfo} settleInfo
1758
1789
  */
@@ -1830,7 +1861,7 @@ var htmx = (function() {
1830
1861
  }
1831
1862
 
1832
1863
  /**
1833
- * Implements complete swapping pipeline, including: focus and selection preservation,
1864
+ * Implements complete swapping pipeline, including: delay, view transitions, focus and selection preservation,
1834
1865
  * title updates, scroll, OOB swapping, normal swapping and settling
1835
1866
  * @param {string|Element} target
1836
1867
  * @param {string} content
@@ -1841,14 +1872,19 @@ var htmx = (function() {
1841
1872
  if (!swapOptions) {
1842
1873
  swapOptions = {}
1843
1874
  }
1875
+ // optional transition API promise callbacks
1876
+ let settleResolve = null
1877
+ let settleReject = null
1844
1878
 
1845
- target = resolveTarget(target)
1846
- const rootNode = swapOptions.contextElement ? getRootNode(swapOptions.contextElement, false) : getDocument()
1879
+ let doSwap = function() {
1880
+ maybeCall(swapOptions.beforeSwapCallback)
1847
1881
 
1848
- // preserve focus and selection
1849
- const activeElt = document.activeElement
1850
- let selectionInfo = {}
1851
- try {
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 = {}
1852
1888
  selectionInfo = {
1853
1889
  elt: activeElt,
1854
1890
  // @ts-ignore
@@ -1856,123 +1892,160 @@ var htmx = (function() {
1856
1892
  // @ts-ignore
1857
1893
  end: activeElt ? activeElt.selectionEnd : null
1858
1894
  }
1859
- } catch (e) {
1860
- // safari issue - see https://github.com/microsoft/playwright/issues/5894
1861
- }
1862
- const settleInfo = makeSettleInfo(target)
1895
+ const settleInfo = makeSettleInfo(target)
1863
1896
 
1864
- // For text content swaps, don't parse the response as HTML, just insert it
1865
- if (swapSpec.swapStyle === 'textContent') {
1866
- target.textContent = content
1867
- // Otherwise, make the fragment and process it
1868
- } else {
1869
- let fragment = makeFragment(content)
1870
-
1871
- settleInfo.title = fragment.title
1872
-
1873
- // select-oob swaps
1874
- if (swapOptions.selectOOB) {
1875
- const oobSelectValues = swapOptions.selectOOB.split(',')
1876
- for (let i = 0; i < oobSelectValues.length; i++) {
1877
- const oobSelectValue = oobSelectValues[i].split(':', 2)
1878
- let id = oobSelectValue[0].trim()
1879
- if (id.indexOf('#') === 0) {
1880
- id = id.substring(1)
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
+ }
1881
1924
  }
1882
- const oobValue = oobSelectValue[1] || 'true'
1883
- const oobElement = fragment.querySelector('#' + id)
1884
- if (oobElement) {
1885
- oobSwap(oobValue, oobElement, settleInfo, rootNode)
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()
1886
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)
1887
1965
  }
1888
1966
  }
1889
- // oob swaps
1890
- findAndSwapOobElements(fragment, settleInfo, rootNode)
1891
- forEach(findAll(fragment, 'template'), /** @param {HTMLTemplateElement} template */function(template) {
1892
- if (findAndSwapOobElements(template.content, settleInfo, rootNode)) {
1893
- // Avoid polluting the DOM with empty templates that were only used to encapsulate oob swap
1894
- template.remove()
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)
1895
1972
  }
1973
+ triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo)
1896
1974
  })
1975
+ maybeCall(swapOptions.afterSwapCallback)
1897
1976
 
1898
- // normal swap
1899
- if (swapOptions.select) {
1900
- const newFragment = getDocument().createDocumentFragment()
1901
- forEach(fragment.querySelectorAll(swapOptions.select), function(node) {
1902
- newFragment.appendChild(node)
1903
- })
1904
- fragment = newFragment
1977
+ // merge in new title after swap but before settle
1978
+ if (!swapSpec.ignoreTitle) {
1979
+ handleTitle(settleInfo.title)
1905
1980
  }
1906
- handlePreservedElements(fragment)
1907
- swapWithStyle(swapSpec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo)
1908
- restorePreservedElements()
1909
- }
1910
1981
 
1911
- // apply saved focus and selection information to swapped content
1912
- if (selectionInfo.elt &&
1913
- !bodyContains(selectionInfo.elt) &&
1914
- getRawAttribute(selectionInfo.elt, 'id')) {
1915
- const newActiveElt = document.getElementById(getRawAttribute(selectionInfo.elt, 'id'))
1916
- const focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }
1917
- if (newActiveElt) {
1918
- // @ts-ignore
1919
- if (selectionInfo.start && newActiveElt.setSelectionRange) {
1920
- try {
1921
- // @ts-ignore
1922
- newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end)
1923
- } catch (e) {
1924
- // the setSelectionRange method is present on fields that don't support it, so just let this fail
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
+ })
1993
+
1994
+ if (swapOptions.anchor) {
1995
+ const anchorTarget = asElement(resolveTarget('#' + swapOptions.anchor))
1996
+ if (anchorTarget) {
1997
+ anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
1925
1998
  }
1926
1999
  }
1927
- newActiveElt.focus(focusOptions)
2000
+
2001
+ updateScrollState(settleInfo.elts, swapSpec)
2002
+ maybeCall(swapOptions.afterSettleCallback)
2003
+ maybeCall(settleResolve)
1928
2004
  }
1929
- }
1930
2005
 
1931
- target.classList.remove(htmx.config.swappingClass)
1932
- forEach(settleInfo.elts, function(elt) {
1933
- if (elt.classList) {
1934
- elt.classList.add(htmx.config.settlingClass)
2006
+ if (swapSpec.settleDelay > 0) {
2007
+ getWindow().setTimeout(doSettle, swapSpec.settleDelay)
2008
+ } else {
2009
+ doSettle()
1935
2010
  }
1936
- triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo)
1937
- })
1938
- if (swapOptions.afterSwapCallback) {
1939
- swapOptions.afterSwapCallback()
1940
2011
  }
1941
-
1942
- // merge in new title after swap but before settle
1943
- if (!swapSpec.ignoreTitle) {
1944
- handleTitle(settleInfo.title)
2012
+ let shouldTransition = htmx.config.globalViewTransitions
2013
+ if (swapSpec.hasOwnProperty('transition')) {
2014
+ shouldTransition = swapSpec.transition
1945
2015
  }
1946
2016
 
1947
- // settle
1948
- const doSettle = function() {
1949
- forEach(settleInfo.tasks, function(task) {
1950
- task.call()
1951
- })
1952
- forEach(settleInfo.elts, function(elt) {
1953
- if (elt.classList) {
1954
- elt.classList.remove(htmx.config.settlingClass)
1955
- }
1956
- triggerEvent(elt, 'htmx:afterSettle', swapOptions.eventInfo)
1957
- })
2017
+ const elt = swapOptions.contextElement || getDocument()
1958
2018
 
1959
- if (swapOptions.anchor) {
1960
- const anchorTarget = asElement(resolveTarget('#' + swapOptions.anchor))
1961
- if (anchorTarget) {
1962
- anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' })
1963
- }
1964
- }
1965
-
1966
- updateScrollState(settleInfo.elts, swapSpec)
1967
- if (swapOptions.afterSettleCallback) {
1968
- swapOptions.afterSettleCallback()
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
+ })
1969
2036
  }
1970
2037
  }
1971
2038
 
1972
- if (swapSpec.settleDelay > 0) {
1973
- getWindow().setTimeout(doSettle, swapSpec.settleDelay)
1974
- } else {
1975
- doSettle()
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
1976
2049
  }
1977
2050
  }
1978
2051
 
@@ -2028,7 +2101,7 @@ var htmx = (function() {
2028
2101
  while (SYMBOL_CONT.exec(str.charAt(position + 1))) {
2029
2102
  position++
2030
2103
  }
2031
- tokens.push(str.substr(startPosition, position - startPosition + 1))
2104
+ tokens.push(str.substring(startPosition, position + 1))
2032
2105
  } else if (STRINGISH_START.indexOf(str.charAt(position)) !== -1) {
2033
2106
  const startChar = str.charAt(position)
2034
2107
  var startPosition = position
@@ -2039,7 +2112,7 @@ var htmx = (function() {
2039
2112
  }
2040
2113
  position++
2041
2114
  }
2042
- tokens.push(str.substr(startPosition, position - startPosition + 1))
2115
+ tokens.push(str.substring(startPosition, position + 1))
2043
2116
  } else {
2044
2117
  const symbol = str.charAt(position)
2045
2118
  tokens.push(symbol)
@@ -2323,6 +2396,11 @@ var htmx = (function() {
2323
2396
  const rawAttribute = getRawAttribute(elt, 'method')
2324
2397
  verb = (/** @type HttpVerb */(rawAttribute ? rawAttribute.toLowerCase() : 'get'))
2325
2398
  path = getRawAttribute(elt, 'action')
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
+ }
2326
2404
  if (verb === 'get' && path.includes('?')) {
2327
2405
  path = path.replace(/\?[^#]+/, '')
2328
2406
  }
@@ -2342,19 +2420,19 @@ var htmx = (function() {
2342
2420
 
2343
2421
  /**
2344
2422
  * @param {Event} evt
2345
- * @param {Node} node
2423
+ * @param {Element} elt
2346
2424
  * @returns {boolean}
2347
2425
  */
2348
- function shouldCancel(evt, node) {
2349
- const elt = asElement(node)
2350
- if (!elt) {
2351
- return false
2352
- }
2426
+ function shouldCancel(evt, elt) {
2353
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
2354
2430
  if (elt.tagName === 'FORM') {
2355
2431
  return true
2356
2432
  }
2357
- if (matches(elt, 'input[type="submit"], button') && closest(elt, 'form') !== null) {
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') {
2358
2436
  return true
2359
2437
  }
2360
2438
  if (elt instanceof HTMLAnchorElement && elt.href &&
@@ -2397,7 +2475,7 @@ var htmx = (function() {
2397
2475
  }
2398
2476
 
2399
2477
  /**
2400
- * @param {Node} elt
2478
+ * @param {Element} elt
2401
2479
  * @param {TriggerHandler} handler
2402
2480
  * @param {HtmxNodeInternalData} nodeData
2403
2481
  * @param {HtmxTriggerSpecification} triggerSpec
@@ -2464,7 +2542,7 @@ var htmx = (function() {
2464
2542
  }
2465
2543
  }
2466
2544
  if (triggerSpec.changed) {
2467
- const node = event.target
2545
+ const node = evt.target
2468
2546
  // @ts-ignore value will be undefined for non-input elements, which is fine
2469
2547
  const value = node.value
2470
2548
  const lastValue = elementData.lastValue.get(triggerSpec)
@@ -2559,6 +2637,7 @@ var htmx = (function() {
2559
2637
  const load = function() {
2560
2638
  if (!nodeData.loaded) {
2561
2639
  nodeData.loaded = true
2640
+ triggerEvent(elt, 'htmx:trigger')
2562
2641
  handler(elt)
2563
2642
  }
2564
2643
  }
@@ -2586,7 +2665,7 @@ var htmx = (function() {
2586
2665
  triggerSpecs.forEach(function(triggerSpec) {
2587
2666
  addTriggerHandler(elt, triggerSpec, nodeData, function(node, evt) {
2588
2667
  const elt = asElement(node)
2589
- if (closest(elt, htmx.config.disableSelector)) {
2668
+ if (eltIsDisabled(elt)) {
2590
2669
  cleanUpElement(elt)
2591
2670
  return
2592
2671
  }
@@ -2600,12 +2679,12 @@ var htmx = (function() {
2600
2679
 
2601
2680
  /**
2602
2681
  * @callback TriggerHandler
2603
- * @param {Node} elt
2682
+ * @param {Element} elt
2604
2683
  * @param {Event} [evt]
2605
2684
  */
2606
2685
 
2607
2686
  /**
2608
- * @param {Node} elt
2687
+ * @param {Element} elt
2609
2688
  * @param {HtmxTriggerSpecification} triggerSpec
2610
2689
  * @param {HtmxNodeInternalData} nodeData
2611
2690
  * @param {TriggerHandler} handler
@@ -2634,7 +2713,7 @@ var htmx = (function() {
2634
2713
  }, observerOptions)
2635
2714
  observer.observe(asElement(elt))
2636
2715
  addEventListener(asElement(elt), handler, nodeData, triggerSpec)
2637
- } else if (triggerSpec.trigger === 'load') {
2716
+ } else if (!nodeData.firstInitCompleted && triggerSpec.trigger === 'load') {
2638
2717
  if (!maybeFilterEvent(triggerSpec, elt, makeEvent('load', { elt }))) {
2639
2718
  loadImmediately(asElement(elt), handler, nodeData, triggerSpec.delay)
2640
2719
  }
@@ -2730,7 +2809,7 @@ var htmx = (function() {
2730
2809
  * @param {Event} evt
2731
2810
  */
2732
2811
  function maybeSetLastButtonClicked(evt) {
2733
- const elt = /** @type {HTMLButtonElement|HTMLInputElement} */ (closest(asElement(evt.target), "button, input[type='submit']"))
2812
+ const elt = getTargetButton(evt.target)
2734
2813
  const internalData = getRelatedFormData(evt)
2735
2814
  if (internalData) {
2736
2815
  internalData.lastButtonClicked = elt
@@ -2747,19 +2826,33 @@ var htmx = (function() {
2747
2826
  }
2748
2827
  }
2749
2828
 
2829
+ /**
2830
+ * @param {EventTarget} target
2831
+ * @returns {HTMLButtonElement|HTMLInputElement|null}
2832
+ */
2833
+ function getTargetButton(target) {
2834
+ return /** @type {HTMLButtonElement|HTMLInputElement|null} */ (closest(asElement(target), "button, input[type='submit']"))
2835
+ }
2836
+
2837
+ /**
2838
+ * @param {Element} elt
2839
+ * @returns {HTMLFormElement|null}
2840
+ */
2841
+ function getRelatedForm(elt) {
2842
+ // @ts-ignore Get the related form if available, else find the closest parent form
2843
+ return elt.form || closest(elt, 'form')
2844
+ }
2845
+
2750
2846
  /**
2751
2847
  * @param {Event} evt
2752
2848
  * @returns {HtmxNodeInternalData|undefined}
2753
2849
  */
2754
2850
  function getRelatedFormData(evt) {
2755
- const elt = closest(asElement(evt.target), "button, input[type='submit']")
2851
+ const elt = getTargetButton(evt.target)
2756
2852
  if (!elt) {
2757
2853
  return
2758
2854
  }
2759
- const form = resolveTarget('#' + getRawAttribute(elt, 'form'), elt.getRootNode()) || closest(elt, 'form')
2760
- if (!form) {
2761
- return
2762
- }
2855
+ const form = getRelatedForm(elt)
2763
2856
  return getInternalData(form)
2764
2857
  }
2765
2858
 
@@ -2836,42 +2929,52 @@ var htmx = (function() {
2836
2929
  * @param {Element|HTMLInputElement} elt
2837
2930
  */
2838
2931
  function initNode(elt) {
2839
- if (closest(elt, htmx.config.disableSelector)) {
2840
- cleanUpElement(elt)
2841
- return
2842
- }
2843
- const nodeData = getInternalData(elt)
2844
- if (nodeData.initHash !== attributeHash(elt)) {
2845
- // clean up any previously processed info
2846
- deInitNode(elt)
2932
+ triggerEvent(elt, 'htmx:beforeProcessNode')
2847
2933
 
2848
- nodeData.initHash = attributeHash(elt)
2849
-
2850
- triggerEvent(elt, 'htmx:beforeProcessNode')
2851
-
2852
- const triggerSpecs = getTriggerSpecs(elt)
2853
- const hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs)
2934
+ const nodeData = getInternalData(elt)
2935
+ const triggerSpecs = getTriggerSpecs(elt)
2936
+ const hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs)
2854
2937
 
2855
- if (!hasExplicitHttpAction) {
2856
- if (getClosestAttributeValue(elt, 'hx-boost') === 'true') {
2857
- boostElement(elt, nodeData, triggerSpecs)
2858
- } else if (hasAttribute(elt, 'hx-trigger')) {
2859
- triggerSpecs.forEach(function(triggerSpec) {
2860
- // For "naked" triggers, don't do anything at all
2861
- addTriggerHandler(elt, triggerSpec, nodeData, function() {
2862
- })
2938
+ if (!hasExplicitHttpAction) {
2939
+ if (getClosestAttributeValue(elt, 'hx-boost') === 'true') {
2940
+ boostElement(elt, nodeData, triggerSpecs)
2941
+ } else if (hasAttribute(elt, 'hx-trigger')) {
2942
+ triggerSpecs.forEach(function(triggerSpec) {
2943
+ // For "naked" triggers, don't do anything at all
2944
+ addTriggerHandler(elt, triggerSpec, nodeData, function() {
2863
2945
  })
2864
- }
2946
+ })
2865
2947
  }
2948
+ }
2866
2949
 
2867
- // Handle submit buttons/inputs that have the form attribute set
2868
- // see https://developer.mozilla.org/docs/Web/HTML/Element/button
2869
- if (elt.tagName === 'FORM' || (getRawAttribute(elt, 'type') === 'submit' && hasAttribute(elt, 'form'))) {
2870
- initButtonTracking(elt)
2871
- }
2950
+ // Handle submit buttons/inputs that have the form attribute set
2951
+ // see https://developer.mozilla.org/docs/Web/HTML/Element/button
2952
+ if (elt.tagName === 'FORM' || (getRawAttribute(elt, 'type') === 'submit' && hasAttribute(elt, 'form'))) {
2953
+ initButtonTracking(elt)
2954
+ }
2872
2955
 
2873
- triggerEvent(elt, 'htmx:afterProcessNode')
2956
+ nodeData.firstInitCompleted = true
2957
+ triggerEvent(elt, 'htmx:afterProcessNode')
2958
+ }
2959
+
2960
+ /**
2961
+ * @param {Element} elt
2962
+ * @returns {boolean}
2963
+ */
2964
+ function maybeDeInitAndHash(elt) {
2965
+ // Ensure only valid Elements and not shadow DOM roots are inited
2966
+ if (!(elt instanceof Element)) {
2967
+ return false
2874
2968
  }
2969
+
2970
+ const nodeData = getInternalData(elt)
2971
+ const hash = attributeHash(elt)
2972
+ if (nodeData.initHash !== hash) {
2973
+ deInitNode(elt)
2974
+ nodeData.initHash = hash
2975
+ return true
2976
+ }
2977
+ return false
2875
2978
  }
2876
2979
 
2877
2980
  /**
@@ -2883,13 +2986,27 @@ var htmx = (function() {
2883
2986
  */
2884
2987
  function processNode(elt) {
2885
2988
  elt = resolveTarget(elt)
2886
- if (closest(elt, htmx.config.disableSelector)) {
2989
+ if (eltIsDisabled(elt)) {
2887
2990
  cleanUpElement(elt)
2888
2991
  return
2889
2992
  }
2890
- initNode(elt)
2891
- forEach(findElementsToProcess(elt), function(child) { initNode(child) })
2993
+
2994
+ const elementsToInit = []
2995
+ if (maybeDeInitAndHash(elt)) {
2996
+ elementsToInit.push(elt)
2997
+ }
2998
+ forEach(findElementsToProcess(elt), function(child) {
2999
+ if (eltIsDisabled(child)) {
3000
+ cleanUpElement(child)
3001
+ return
3002
+ }
3003
+ if (maybeDeInitAndHash(child)) {
3004
+ elementsToInit.push(child)
3005
+ }
3006
+ })
3007
+
2892
3008
  forEach(findHxOnWildcardElements(elt), processHxOnWildcard)
3009
+ forEach(elementsToInit, initNode)
2893
3010
  }
2894
3011
 
2895
3012
  //= ===================================================================
@@ -2910,16 +3027,9 @@ var htmx = (function() {
2910
3027
  * @returns {CustomEvent}
2911
3028
  */
2912
3029
  function makeEvent(eventName, detail) {
2913
- let evt
2914
- if (window.CustomEvent && typeof window.CustomEvent === 'function') {
2915
- // TODO: `composed: true` here is a hack to make global event handlers work with events in shadow DOM
2916
- // This breaks expected encapsulation but needs to be here until decided otherwise by core devs
2917
- evt = new CustomEvent(eventName, { bubbles: true, cancelable: true, composed: true, detail })
2918
- } else {
2919
- evt = getDocument().createEvent('CustomEvent')
2920
- evt.initCustomEvent(eventName, true, true, detail)
2921
- }
2922
- return evt
3030
+ // TODO: `composed: true` here is a hack to make global event handlers work with events in shadow DOM
3031
+ // This breaks expected encapsulation but needs to be here until decided otherwise by core devs
3032
+ return new CustomEvent(eventName, { bubbles: true, cancelable: true, composed: true, detail })
2923
3033
  }
2924
3034
 
2925
3035
  /**
@@ -2941,15 +3051,17 @@ var htmx = (function() {
2941
3051
 
2942
3052
  /**
2943
3053
  * `withExtensions` locates all active extensions for a provided element, then
2944
- * executes the provided function using each of the active extensions. It should
3054
+ * executes the provided function using each of the active extensions. You can filter
3055
+ * the element's extensions by giving it a list of extensions to ignore. It should
2945
3056
  * be called internally at every extendable execution point in htmx.
2946
3057
  *
2947
3058
  * @param {Element} elt
2948
3059
  * @param {(extension:HtmxExtension) => void} toDo
3060
+ * @param {string[]=} extensionsToIgnore
2949
3061
  * @returns void
2950
3062
  */
2951
- function withExtensions(elt, toDo) {
2952
- forEach(getExtensions(elt), function(extension) {
3063
+ function withExtensions(elt, toDo, extensionsToIgnore) {
3064
+ forEach(getExtensions(elt, [], extensionsToIgnore), function(extension) {
2953
3065
  try {
2954
3066
  toDo(extension)
2955
3067
  } catch (e) {
@@ -2959,11 +3071,7 @@ var htmx = (function() {
2959
3071
  }
2960
3072
 
2961
3073
  function logError(msg) {
2962
- if (console.error) {
2963
- console.error(msg)
2964
- } else if (console.log) {
2965
- console.log('ERROR: ', msg)
2966
- }
3074
+ console.error(msg)
2967
3075
  }
2968
3076
 
2969
3077
  /**
@@ -3007,6 +3115,16 @@ var htmx = (function() {
3007
3115
  //= ===================================================================
3008
3116
  let currentPathForHistory = location.pathname + location.search
3009
3117
 
3118
+ /**
3119
+ * @param {string} path
3120
+ */
3121
+ function setCurrentPathForHistory(path) {
3122
+ currentPathForHistory = path
3123
+ if (canAccessLocalStorage()) {
3124
+ sessionStorage.setItem('htmx-current-path-for-history', path)
3125
+ }
3126
+ }
3127
+
3010
3128
  /**
3011
3129
  * @returns {Element}
3012
3130
  */
@@ -3031,13 +3149,13 @@ var htmx = (function() {
3031
3149
 
3032
3150
  if (htmx.config.historyCacheSize <= 0) {
3033
3151
  // make sure that an eventually already existing cache is purged
3034
- localStorage.removeItem('htmx-history-cache')
3152
+ sessionStorage.removeItem('htmx-history-cache')
3035
3153
  return
3036
3154
  }
3037
3155
 
3038
3156
  url = normalizePath(url)
3039
3157
 
3040
- const historyCache = parseJSON(localStorage.getItem('htmx-history-cache')) || []
3158
+ const historyCache = parseJSON(sessionStorage.getItem('htmx-history-cache')) || []
3041
3159
  for (let i = 0; i < historyCache.length; i++) {
3042
3160
  if (historyCache[i].url === url) {
3043
3161
  historyCache.splice(i, 1)
@@ -3058,7 +3176,7 @@ var htmx = (function() {
3058
3176
  // keep trying to save the cache until it succeeds or is empty
3059
3177
  while (historyCache.length > 0) {
3060
3178
  try {
3061
- localStorage.setItem('htmx-history-cache', JSON.stringify(historyCache))
3179
+ sessionStorage.setItem('htmx-history-cache', JSON.stringify(historyCache))
3062
3180
  break
3063
3181
  } catch (e) {
3064
3182
  triggerErrorEvent(getDocument().body, 'htmx:historyCacheError', { cause: e, cache: historyCache })
@@ -3086,7 +3204,7 @@ var htmx = (function() {
3086
3204
 
3087
3205
  url = normalizePath(url)
3088
3206
 
3089
- const historyCache = parseJSON(localStorage.getItem('htmx-history-cache')) || []
3207
+ const historyCache = parseJSON(sessionStorage.getItem('htmx-history-cache')) || []
3090
3208
  for (let i = 0; i < historyCache.length; i++) {
3091
3209
  if (historyCache[i].url === url) {
3092
3210
  return historyCache[i]
@@ -3114,26 +3232,24 @@ var htmx = (function() {
3114
3232
 
3115
3233
  function saveCurrentPageToHistory() {
3116
3234
  const elt = getHistoryElement()
3117
- const path = currentPathForHistory || location.pathname + location.search
3235
+ let path = currentPathForHistory
3236
+ if (canAccessLocalStorage()) {
3237
+ path = sessionStorage.getItem('htmx-current-path-for-history')
3238
+ }
3239
+ path = path || location.pathname + location.search
3118
3240
 
3119
3241
  // Allow history snapshot feature to be disabled where hx-history="false"
3120
3242
  // is present *anywhere* in the current document we're about to save,
3121
3243
  // so we can prevent privileged data entering the cache.
3122
3244
  // The page will still be reachable as a history entry, but htmx will fetch it
3123
- // live from the server onpopstate rather than look in the localStorage cache
3124
- let disableHistoryCache
3125
- try {
3126
- disableHistoryCache = getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]')
3127
- } catch (e) {
3128
- // IE11: insensitive modifier not supported so fallback to case sensitive selector
3129
- disableHistoryCache = getDocument().querySelector('[hx-history="false"],[data-hx-history="false"]')
3130
- }
3245
+ // live from the server onpopstate rather than look in the sessionStorage cache
3246
+ const disableHistoryCache = getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]')
3131
3247
  if (!disableHistoryCache) {
3132
3248
  triggerEvent(getDocument().body, 'htmx:beforeHistorySave', { path, historyElt: elt })
3133
3249
  saveToHistoryCache(path, elt)
3134
3250
  }
3135
3251
 
3136
- if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, getDocument().title, window.location.href)
3252
+ if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, getDocument().title, location.href)
3137
3253
  }
3138
3254
 
3139
3255
  /**
@@ -3150,7 +3266,7 @@ var htmx = (function() {
3150
3266
  if (htmx.config.historyEnabled) {
3151
3267
  history.pushState({ htmx: true }, '', path)
3152
3268
  }
3153
- currentPathForHistory = path
3269
+ setCurrentPathForHistory(path)
3154
3270
  }
3155
3271
 
3156
3272
  /**
@@ -3158,7 +3274,7 @@ var htmx = (function() {
3158
3274
  */
3159
3275
  function replaceUrlInHistory(path) {
3160
3276
  if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, '', path)
3161
- currentPathForHistory = path
3277
+ setCurrentPathForHistory(path)
3162
3278
  }
3163
3279
 
3164
3280
  /**
@@ -3175,33 +3291,31 @@ var htmx = (function() {
3175
3291
  */
3176
3292
  function loadHistoryFromServer(path) {
3177
3293
  const request = new XMLHttpRequest()
3178
- const details = { path, xhr: request }
3179
- triggerEvent(getDocument().body, 'htmx:historyCacheMiss', details)
3294
+ const swapSpec = { swapStyle: 'innerHTML', swapDelay: 0, settleDelay: 0 }
3295
+ const details = { path, xhr: request, historyElt: getHistoryElement(), swapSpec }
3180
3296
  request.open('GET', path, true)
3181
- request.setRequestHeader('HX-Request', 'true')
3297
+ if (htmx.config.historyRestoreAsHxRequest) {
3298
+ request.setRequestHeader('HX-Request', 'true')
3299
+ }
3182
3300
  request.setRequestHeader('HX-History-Restore-Request', 'true')
3183
- request.setRequestHeader('HX-Current-URL', getDocument().location.href)
3301
+ request.setRequestHeader('HX-Current-URL', location.href)
3184
3302
  request.onload = function() {
3185
3303
  if (this.status >= 200 && this.status < 400) {
3304
+ details.response = this.response
3186
3305
  triggerEvent(getDocument().body, 'htmx:historyCacheMissLoad', details)
3187
- const fragment = makeFragment(this.response)
3188
- /** @type ParentNode */
3189
- const content = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment
3190
- const historyElement = getHistoryElement()
3191
- const settleInfo = makeSettleInfo(historyElement)
3192
- handleTitle(fragment.title)
3193
-
3194
- handlePreservedElements(fragment)
3195
- swapInnerHTML(historyElement, content, settleInfo)
3196
- restorePreservedElements()
3197
- settleImmediately(settleInfo.tasks)
3198
- currentPathForHistory = path
3199
- triggerEvent(getDocument().body, 'htmx:historyRestore', { path, cacheMiss: true, serverResponse: this.response })
3306
+ swap(details.historyElt, details.response, swapSpec, {
3307
+ contextElement: details.historyElt,
3308
+ historyRequest: true
3309
+ })
3310
+ setCurrentPathForHistory(details.path)
3311
+ triggerEvent(getDocument().body, 'htmx:historyRestore', { path, cacheMiss: true, serverResponse: details.response })
3200
3312
  } else {
3201
3313
  triggerErrorEvent(getDocument().body, 'htmx:historyCacheMissLoadError', details)
3202
3314
  }
3203
3315
  }
3204
- request.send()
3316
+ if (triggerEvent(getDocument().body, 'htmx:historyCacheMiss', details)) {
3317
+ request.send() // only send request if event not prevented
3318
+ }
3205
3319
  }
3206
3320
 
3207
3321
  /**
@@ -3212,24 +3326,21 @@ var htmx = (function() {
3212
3326
  path = path || location.pathname + location.search
3213
3327
  const cached = getCachedHistory(path)
3214
3328
  if (cached) {
3215
- const fragment = makeFragment(cached.content)
3216
- const historyElement = getHistoryElement()
3217
- const settleInfo = makeSettleInfo(historyElement)
3218
- handleTitle(cached.title)
3219
- handlePreservedElements(fragment)
3220
- swapInnerHTML(historyElement, fragment, settleInfo)
3221
- restorePreservedElements()
3222
- settleImmediately(settleInfo.tasks)
3223
- getWindow().setTimeout(function() {
3224
- window.scrollTo(0, cached.scroll)
3225
- }, 0) // next 'tick', so browser has time to render layout
3226
- currentPathForHistory = path
3227
- triggerEvent(getDocument().body, 'htmx:historyRestore', { path, item: cached })
3329
+ const swapSpec = { swapStyle: 'innerHTML', swapDelay: 0, settleDelay: 0, scroll: cached.scroll }
3330
+ const details = { path, item: cached, historyElt: getHistoryElement(), swapSpec }
3331
+ if (triggerEvent(getDocument().body, 'htmx:historyCacheHit', details)) {
3332
+ swap(details.historyElt, cached.content, swapSpec, {
3333
+ contextElement: details.historyElt,
3334
+ title: cached.title
3335
+ })
3336
+ setCurrentPathForHistory(details.path)
3337
+ triggerEvent(getDocument().body, 'htmx:historyRestore', details)
3338
+ }
3228
3339
  } else {
3229
3340
  if (htmx.config.refreshOnHistoryMiss) {
3230
3341
  // @ts-ignore: optional parameter in reload() function throws error
3231
3342
  // noinspection JSUnresolvedReference
3232
- window.location.reload(true)
3343
+ htmx.location.reload(true)
3233
3344
  } else {
3234
3345
  loadHistoryFromServer(path)
3235
3346
  }
@@ -3334,7 +3445,8 @@ var htmx = (function() {
3334
3445
  return true
3335
3446
  }
3336
3447
 
3337
- /** @param {string} name
3448
+ /**
3449
+ * @param {string} name
3338
3450
  * @param {string|Array|FormDataEntryValue} value
3339
3451
  * @param {FormData} formData */
3340
3452
  function addValueToFormData(name, value, formData) {
@@ -3347,7 +3459,8 @@ var htmx = (function() {
3347
3459
  }
3348
3460
  }
3349
3461
 
3350
- /** @param {string} name
3462
+ /**
3463
+ * @param {string} name
3351
3464
  * @param {string|Array} value
3352
3465
  * @param {FormData} formData */
3353
3466
  function removeValueFromFormData(name, value, formData) {
@@ -3363,6 +3476,22 @@ var htmx = (function() {
3363
3476
  }
3364
3477
  }
3365
3478
 
3479
+ /**
3480
+ * @param {Element} elt
3481
+ * @returns {string|Array}
3482
+ */
3483
+ function getValueFromInput(elt) {
3484
+ if (elt instanceof HTMLSelectElement && elt.multiple) {
3485
+ return toArray(elt.querySelectorAll('option:checked')).map(function(e) { return (/** @type HTMLOptionElement */(e)).value })
3486
+ }
3487
+ // include file inputs
3488
+ if (elt instanceof HTMLInputElement && elt.files) {
3489
+ return toArray(elt.files)
3490
+ }
3491
+ // @ts-ignore value will be undefined for non-input elements, which is fine
3492
+ return elt.value
3493
+ }
3494
+
3366
3495
  /**
3367
3496
  * @param {Element[]} processed
3368
3497
  * @param {FormData} formData
@@ -3378,16 +3507,7 @@ var htmx = (function() {
3378
3507
  }
3379
3508
  if (shouldInclude(elt)) {
3380
3509
  const name = getRawAttribute(elt, 'name')
3381
- // @ts-ignore value will be undefined for non-input elements, which is fine
3382
- let value = elt.value
3383
- if (elt instanceof HTMLSelectElement && elt.multiple) {
3384
- value = toArray(elt.querySelectorAll('option:checked')).map(function(e) { return (/** @type HTMLOptionElement */(e)).value })
3385
- }
3386
- // include file inputs
3387
- if (elt instanceof HTMLInputElement && elt.files) {
3388
- value = toArray(elt.files)
3389
- }
3390
- addValueToFormData(name, value, formData)
3510
+ addValueToFormData(name, getValueFromInput(elt), formData)
3391
3511
  if (validate) {
3392
3512
  validateElement(elt, errors)
3393
3513
  }
@@ -3398,7 +3518,7 @@ var htmx = (function() {
3398
3518
  // The input has already been processed and added to the values, but the FormData that will be
3399
3519
  // constructed right after on the form, will include it once again. So remove that input's value
3400
3520
  // now to avoid duplicates
3401
- removeValueFromFormData(input.name, input.value, formData)
3521
+ removeValueFromFormData(input.name, getValueFromInput(input), formData)
3402
3522
  } else {
3403
3523
  processed.push(input)
3404
3524
  }
@@ -3416,7 +3536,6 @@ var htmx = (function() {
3416
3536
  }
3417
3537
 
3418
3538
  /**
3419
- *
3420
3539
  * @param {Element} elt
3421
3540
  * @param {HtmxElementValidationError[]} errors
3422
3541
  */
@@ -3471,9 +3590,9 @@ var htmx = (function() {
3471
3590
  validate = validate && internalData.lastButtonClicked.formNoValidate !== true
3472
3591
  }
3473
3592
 
3474
- // for a non-GET include the closest form
3593
+ // for a non-GET include the related form, which may or may not be a parent element of elt
3475
3594
  if (verb !== 'get') {
3476
- processInputValue(processed, priorityFormData, errors, closest(elt, 'form'), validate)
3595
+ processInputValue(processed, priorityFormData, errors, getRelatedForm(elt), validate)
3477
3596
  }
3478
3597
 
3479
3598
  // include the element itself
@@ -3553,7 +3672,7 @@ var htmx = (function() {
3553
3672
  'HX-Trigger': getRawAttribute(elt, 'id'),
3554
3673
  'HX-Trigger-Name': getRawAttribute(elt, 'name'),
3555
3674
  'HX-Target': getAttributeValue(target, 'id'),
3556
- 'HX-Current-URL': getDocument().location.href
3675
+ 'HX-Current-URL': location.href
3557
3676
  }
3558
3677
  getValuesForElement(elt, 'hx-headers', false, headers)
3559
3678
  if (prompt !== undefined) {
@@ -3581,7 +3700,7 @@ var htmx = (function() {
3581
3700
  } else if (paramsValue === '*') {
3582
3701
  return inputValues
3583
3702
  } else if (paramsValue.indexOf('not ') === 0) {
3584
- forEach(paramsValue.substr(4).split(','), function(name) {
3703
+ forEach(paramsValue.slice(4).split(','), function(name) {
3585
3704
  name = name.trim()
3586
3705
  inputValues.delete(name)
3587
3706
  })
@@ -3631,15 +3750,15 @@ var htmx = (function() {
3631
3750
  for (let i = 0; i < split.length; i++) {
3632
3751
  const value = split[i]
3633
3752
  if (value.indexOf('swap:') === 0) {
3634
- swapSpec.swapDelay = parseInterval(value.substr(5))
3753
+ swapSpec.swapDelay = parseInterval(value.slice(5))
3635
3754
  } else if (value.indexOf('settle:') === 0) {
3636
- swapSpec.settleDelay = parseInterval(value.substr(7))
3755
+ swapSpec.settleDelay = parseInterval(value.slice(7))
3637
3756
  } else if (value.indexOf('transition:') === 0) {
3638
- swapSpec.transition = value.substr(11) === 'true'
3757
+ swapSpec.transition = value.slice(11) === 'true'
3639
3758
  } else if (value.indexOf('ignoreTitle:') === 0) {
3640
- swapSpec.ignoreTitle = value.substr(12) === 'true'
3759
+ swapSpec.ignoreTitle = value.slice(12) === 'true'
3641
3760
  } else if (value.indexOf('scroll:') === 0) {
3642
- const scrollSpec = value.substr(7)
3761
+ const scrollSpec = value.slice(7)
3643
3762
  var splitSpec = scrollSpec.split(':')
3644
3763
  const scrollVal = splitSpec.pop()
3645
3764
  var selectorVal = splitSpec.length > 0 ? splitSpec.join(':') : null
@@ -3647,14 +3766,14 @@ var htmx = (function() {
3647
3766
  swapSpec.scroll = scrollVal
3648
3767
  swapSpec.scrollTarget = selectorVal
3649
3768
  } else if (value.indexOf('show:') === 0) {
3650
- const showSpec = value.substr(5)
3769
+ const showSpec = value.slice(5)
3651
3770
  var splitSpec = showSpec.split(':')
3652
3771
  const showVal = splitSpec.pop()
3653
3772
  var selectorVal = splitSpec.length > 0 ? splitSpec.join(':') : null
3654
3773
  swapSpec.show = showVal
3655
3774
  swapSpec.showTarget = selectorVal
3656
3775
  } else if (value.indexOf('focus-scroll:') === 0) {
3657
- const focusScrollVal = value.substr('focus-scroll:'.length)
3776
+ const focusScrollVal = value.slice('focus-scroll:'.length)
3658
3777
  swapSpec.focusScroll = focusScrollVal == 'true'
3659
3778
  } else if (i == 0) {
3660
3779
  swapSpec.swapStyle = value
@@ -3731,6 +3850,11 @@ var htmx = (function() {
3731
3850
  target = target || last
3732
3851
  target.scrollTop = target.scrollHeight
3733
3852
  }
3853
+ if (typeof swapSpec.scroll === 'number') {
3854
+ getWindow().setTimeout(function() {
3855
+ window.scrollTo(0, /** @type number */ (swapSpec.scroll))
3856
+ }, 0) // next 'tick', so browser has time to render layout
3857
+ }
3734
3858
  }
3735
3859
  if (swapSpec.show) {
3736
3860
  var target = null
@@ -3759,9 +3883,10 @@ var htmx = (function() {
3759
3883
  * @param {string} attr
3760
3884
  * @param {boolean=} evalAsDefault
3761
3885
  * @param {Object=} values
3886
+ * @param {Event=} event
3762
3887
  * @returns {Object}
3763
3888
  */
3764
- function getValuesForElement(elt, attr, evalAsDefault, values) {
3889
+ function getValuesForElement(elt, attr, evalAsDefault, values, event) {
3765
3890
  if (values == null) {
3766
3891
  values = {}
3767
3892
  }
@@ -3776,10 +3901,10 @@ var htmx = (function() {
3776
3901
  return null
3777
3902
  }
3778
3903
  if (str.indexOf('javascript:') === 0) {
3779
- str = str.substr(11)
3904
+ str = str.slice(11)
3780
3905
  evaluateValue = true
3781
3906
  } else if (str.indexOf('js:') === 0) {
3782
- str = str.substr(3)
3907
+ str = str.slice(3)
3783
3908
  evaluateValue = true
3784
3909
  }
3785
3910
  if (str.indexOf('{') !== 0) {
@@ -3787,7 +3912,13 @@ var htmx = (function() {
3787
3912
  }
3788
3913
  let varsValues
3789
3914
  if (evaluateValue) {
3790
- varsValues = maybeEval(elt, function() { return Function('return (' + str + ')')() }, {})
3915
+ varsValues = maybeEval(elt, function() {
3916
+ if (event) {
3917
+ return Function('event', 'return (' + str + ')').call(elt, event)
3918
+ } else { // allow window.event to be accessible
3919
+ return Function('return (' + str + ')').call(elt)
3920
+ }
3921
+ }, {})
3791
3922
  } else {
3792
3923
  varsValues = parseJSON(str)
3793
3924
  }
@@ -3799,7 +3930,7 @@ var htmx = (function() {
3799
3930
  }
3800
3931
  }
3801
3932
  }
3802
- return getValuesForElement(asElement(parentElt(elt)), attr, evalAsDefault, values)
3933
+ return getValuesForElement(asElement(parentElt(elt)), attr, evalAsDefault, values, event)
3803
3934
  }
3804
3935
 
3805
3936
  /**
@@ -3819,28 +3950,31 @@ var htmx = (function() {
3819
3950
 
3820
3951
  /**
3821
3952
  * @param {Element} elt
3822
- * @param {*?} expressionVars
3953
+ * @param {Event=} event
3954
+ * @param {*?=} expressionVars
3823
3955
  * @returns
3824
3956
  */
3825
- function getHXVarsForElement(elt, expressionVars) {
3826
- return getValuesForElement(elt, 'hx-vars', true, expressionVars)
3957
+ function getHXVarsForElement(elt, event, expressionVars) {
3958
+ return getValuesForElement(elt, 'hx-vars', true, expressionVars, event)
3827
3959
  }
3828
3960
 
3829
3961
  /**
3830
3962
  * @param {Element} elt
3831
- * @param {*?} expressionVars
3963
+ * @param {Event=} event
3964
+ * @param {*?=} expressionVars
3832
3965
  * @returns
3833
3966
  */
3834
- function getHXValsForElement(elt, expressionVars) {
3835
- return getValuesForElement(elt, 'hx-vals', false, expressionVars)
3967
+ function getHXValsForElement(elt, event, expressionVars) {
3968
+ return getValuesForElement(elt, 'hx-vals', false, expressionVars, event)
3836
3969
  }
3837
3970
 
3838
3971
  /**
3839
3972
  * @param {Element} elt
3973
+ * @param {Event=} event
3840
3974
  * @returns {FormData}
3841
3975
  */
3842
- function getExpressionVars(elt) {
3843
- return mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt))
3976
+ function getExpressionVars(elt, event) {
3977
+ return mergeObjects(getHXVarsForElement(elt, event), getHXValsForElement(elt, event))
3844
3978
  }
3845
3979
 
3846
3980
  /**
@@ -3865,8 +3999,7 @@ var htmx = (function() {
3865
3999
  * @return {string}
3866
4000
  */
3867
4001
  function getPathFromResponse(xhr) {
3868
- // NB: IE11 does not support this stuff
3869
- if (xhr.responseURL && typeof (URL) !== 'undefined') {
4002
+ if (xhr.responseURL) {
3870
4003
  try {
3871
4004
  const url = new URL(xhr.responseURL)
3872
4005
  return url.pathname + url.search
@@ -3905,9 +4038,9 @@ var htmx = (function() {
3905
4038
  })
3906
4039
  } else {
3907
4040
  let resolvedTarget = resolveTarget(context.target)
3908
- // If target is supplied but can't resolve OR both target and source can't be resolved
4041
+ // If target is supplied but can't resolve OR source is supplied but both target and source can't be resolved
3909
4042
  // then use DUMMY_ELT to abort the request with htmx:targetError to avoid it replacing body by mistake
3910
- if ((context.target && !resolvedTarget) || (!resolvedTarget && !resolveTarget(context.source))) {
4043
+ if ((context.target && !resolvedTarget) || (context.source && !resolvedTarget && !resolveTarget(context.source))) {
3911
4044
  resolvedTarget = DUMMY_ELT
3912
4045
  }
3913
4046
  return issueAjaxRequest(verb, path, resolveTarget(context.source), context.event,
@@ -3948,17 +4081,9 @@ var htmx = (function() {
3948
4081
  * @return {boolean}
3949
4082
  */
3950
4083
  function verifyPath(elt, path, requestConfig) {
3951
- let sameHost
3952
- let url
3953
- if (typeof URL === 'function') {
3954
- url = new URL(path, document.location.href)
3955
- const origin = document.location.origin
3956
- sameHost = origin === url.origin
3957
- } else {
3958
- // IE11 doesn't support URL
3959
- url = path
3960
- sameHost = startsWith(path, document.location.origin)
3961
- }
4084
+ const url = new URL(path, location.protocol !== 'about:' ? location.href : window.origin)
4085
+ const origin = location.protocol !== 'about:' ? location.origin : window.origin
4086
+ const sameHost = origin === url.origin
3962
4087
 
3963
4088
  if (htmx.config.selfRequestsOnly) {
3964
4089
  if (!sameHost) {
@@ -4039,7 +4164,15 @@ var htmx = (function() {
4039
4164
  get: function(target, name) {
4040
4165
  if (typeof name === 'symbol') {
4041
4166
  // Forward symbol calls to the FormData itself directly
4042
- return Reflect.get(target, name)
4167
+ const result = Reflect.get(target, name)
4168
+ // Wrap in function with apply to correctly bind the FormData context, as a direct call would result in an illegal invocation error
4169
+ if (typeof result === 'function') {
4170
+ return function() {
4171
+ return result.apply(formData, arguments)
4172
+ }
4173
+ } else {
4174
+ return result
4175
+ }
4043
4176
  }
4044
4177
  if (name === 'toJSON') {
4045
4178
  // Support JSON.stringify call on proxy
@@ -4051,8 +4184,6 @@ var htmx = (function() {
4051
4184
  return function() {
4052
4185
  return formData[name].apply(formData, arguments)
4053
4186
  }
4054
- } else {
4055
- return target[name]
4056
4187
  }
4057
4188
  }
4058
4189
  const array = formData.getAll(name)
@@ -4127,7 +4258,7 @@ var htmx = (function() {
4127
4258
  }
4128
4259
  const target = etc.targetOverride || asElement(getTarget(elt))
4129
4260
  if (target == null || target == DUMMY_ELT) {
4130
- triggerErrorEvent(elt, 'htmx:targetError', { target: getAttributeValue(elt, 'hx-target') })
4261
+ triggerErrorEvent(elt, 'htmx:targetError', { target: getClosestAttributeValue(elt, 'hx-target') })
4131
4262
  maybeCall(reject)
4132
4263
  return promise
4133
4264
  }
@@ -4143,9 +4274,11 @@ var htmx = (function() {
4143
4274
 
4144
4275
  const buttonVerb = getRawAttribute(submitter, 'formmethod')
4145
4276
  if (buttonVerb != null) {
4146
- // ignore buttons with formmethod="dialog"
4147
- if (buttonVerb.toLowerCase() !== 'dialog') {
4277
+ if (VERBS.includes(buttonVerb.toLowerCase())) {
4148
4278
  verb = (/** @type HttpVerb */(buttonVerb))
4279
+ } else {
4280
+ maybeCall(resolve)
4281
+ return promise
4149
4282
  }
4150
4283
  }
4151
4284
  }
@@ -4280,7 +4413,7 @@ var htmx = (function() {
4280
4413
  if (etc.values) {
4281
4414
  overrideFormData(rawFormData, formDataFromObject(etc.values))
4282
4415
  }
4283
- const expressionVars = formDataFromObject(getExpressionVars(elt))
4416
+ const expressionVars = formDataFromObject(getExpressionVars(elt, event))
4284
4417
  const allFormData = overrideFormData(rawFormData, expressionVars)
4285
4418
  let filteredFormData = filterValues(allFormData, elt)
4286
4419
 
@@ -4290,7 +4423,7 @@ var htmx = (function() {
4290
4423
 
4291
4424
  // behavior of anchors w/ empty href is to use the current URL
4292
4425
  if (path == null || path === '') {
4293
- path = getDocument().location.href
4426
+ path = location.href
4294
4427
  }
4295
4428
 
4296
4429
  /**
@@ -4314,6 +4447,7 @@ var htmx = (function() {
4314
4447
  unfilteredFormData: allFormData,
4315
4448
  unfilteredParameters: formDataProxy(allFormData),
4316
4449
  headers,
4450
+ elt,
4317
4451
  target,
4318
4452
  verb,
4319
4453
  errors,
@@ -4368,6 +4502,7 @@ var htmx = (function() {
4368
4502
  if (!verifyPath(elt, finalPath, requestConfig)) {
4369
4503
  triggerErrorEvent(elt, 'htmx:invalidPath', requestConfig)
4370
4504
  maybeCall(reject)
4505
+ endRequestLock()
4371
4506
  return promise
4372
4507
  }
4373
4508
 
@@ -4430,10 +4565,11 @@ var htmx = (function() {
4430
4565
  }
4431
4566
  }
4432
4567
  maybeCall(resolve)
4433
- endRequestLock()
4434
4568
  } catch (e) {
4435
4569
  triggerErrorEvent(elt, 'htmx:onLoadError', mergeObjects({ error: e }, responseInfo))
4436
4570
  throw e
4571
+ } finally {
4572
+ endRequestLock()
4437
4573
  }
4438
4574
  }
4439
4575
  xhr.onerror = function() {
@@ -4608,13 +4744,31 @@ var htmx = (function() {
4608
4744
  if (title) {
4609
4745
  const titleElt = find('title')
4610
4746
  if (titleElt) {
4611
- titleElt.innerHTML = title
4747
+ titleElt.textContent = title
4612
4748
  } else {
4613
4749
  window.document.title = title
4614
4750
  }
4615
4751
  }
4616
4752
  }
4617
4753
 
4754
+ /**
4755
+ * Resove the Retarget selector and throw if not found
4756
+ * @param {Element} elt
4757
+ * @param {String} target
4758
+ * @returns {Element}
4759
+ */
4760
+ function resolveRetarget(elt, target) {
4761
+ if (target === 'this') {
4762
+ return elt
4763
+ }
4764
+ const resolvedTarget = asElement(querySelectorExt(elt, target))
4765
+ if (resolvedTarget == null) {
4766
+ triggerErrorEvent(elt, 'htmx:targetError', { target })
4767
+ throw new Error(`Invalid re-target ${target}`)
4768
+ }
4769
+ return resolvedTarget
4770
+ }
4771
+
4618
4772
  /**
4619
4773
  * @param {Element} elt
4620
4774
  * @param {HtmxResponseInfo} responseInfo
@@ -4652,25 +4806,17 @@ var htmx = (function() {
4652
4806
 
4653
4807
  if (hasHeader(xhr, /HX-Redirect:/i)) {
4654
4808
  responseInfo.keepIndicators = true
4655
- location.href = xhr.getResponseHeader('HX-Redirect')
4656
- shouldRefresh && location.reload()
4809
+ htmx.location.href = xhr.getResponseHeader('HX-Redirect')
4810
+ shouldRefresh && htmx.location.reload()
4657
4811
  return
4658
4812
  }
4659
4813
 
4660
4814
  if (shouldRefresh) {
4661
4815
  responseInfo.keepIndicators = true
4662
- location.reload()
4816
+ htmx.location.reload()
4663
4817
  return
4664
4818
  }
4665
4819
 
4666
- if (hasHeader(xhr, /HX-Retarget:/i)) {
4667
- if (xhr.getResponseHeader('HX-Retarget') === 'this') {
4668
- responseInfo.target = elt
4669
- } else {
4670
- responseInfo.target = asElement(querySelectorExt(elt, xhr.getResponseHeader('HX-Retarget')))
4671
- }
4672
- }
4673
-
4674
4820
  const historyUpdate = determineHistoryUpdates(elt, responseInfo)
4675
4821
 
4676
4822
  const responseHandling = resolveResponseHandling(xhr)
@@ -4679,7 +4825,7 @@ var htmx = (function() {
4679
4825
  let ignoreTitle = htmx.config.ignoreTitle || responseHandling.ignoreTitle
4680
4826
  let selectOverride = responseHandling.select
4681
4827
  if (responseHandling.target) {
4682
- responseInfo.target = asElement(querySelectorExt(elt, responseHandling.target))
4828
+ responseInfo.target = resolveRetarget(elt, responseHandling.target)
4683
4829
  }
4684
4830
  var swapOverride = etc.swapOverride
4685
4831
  if (swapOverride == null && responseHandling.swapOverride) {
@@ -4688,12 +4834,9 @@ var htmx = (function() {
4688
4834
 
4689
4835
  // response headers override response handling config
4690
4836
  if (hasHeader(xhr, /HX-Retarget:/i)) {
4691
- if (xhr.getResponseHeader('HX-Retarget') === 'this') {
4692
- responseInfo.target = elt
4693
- } else {
4694
- responseInfo.target = asElement(querySelectorExt(elt, xhr.getResponseHeader('HX-Retarget')))
4695
- }
4837
+ responseInfo.target = resolveRetarget(elt, xhr.getResponseHeader('HX-Retarget'))
4696
4838
  }
4839
+
4697
4840
  if (hasHeader(xhr, /HX-Reswap:/i)) {
4698
4841
  swapOverride = xhr.getResponseHeader('HX-Reswap')
4699
4842
  }
@@ -4746,10 +4889,6 @@ var htmx = (function() {
4746
4889
 
4747
4890
  target.classList.add(htmx.config.swappingClass)
4748
4891
 
4749
- // optional transition API promise callbacks
4750
- let settleResolve = null
4751
- let settleReject = null
4752
-
4753
4892
  if (responseInfoSelect) {
4754
4893
  selectOverride = responseInfoSelect
4755
4894
  }
@@ -4761,8 +4900,31 @@ var htmx = (function() {
4761
4900
  const selectOOB = getClosestAttributeValue(elt, 'hx-select-oob')
4762
4901
  const select = getClosestAttributeValue(elt, 'hx-select')
4763
4902
 
4764
- let doSwap = function() {
4765
- try {
4903
+ swap(target, serverResponse, swapSpec, {
4904
+ select: selectOverride === 'unset' ? null : selectOverride || select,
4905
+ selectOOB,
4906
+ eventInfo: responseInfo,
4907
+ anchor: responseInfo.pathInfo.anchor,
4908
+ contextElement: elt,
4909
+ afterSwapCallback: function() {
4910
+ if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
4911
+ let finalElt = elt
4912
+ if (!bodyContains(elt)) {
4913
+ finalElt = getDocument().body
4914
+ }
4915
+ handleTriggerHeader(xhr, 'HX-Trigger-After-Swap', finalElt)
4916
+ }
4917
+ },
4918
+ afterSettleCallback: function() {
4919
+ if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
4920
+ let finalElt = elt
4921
+ if (!bodyContains(elt)) {
4922
+ finalElt = getDocument().body
4923
+ }
4924
+ handleTriggerHeader(xhr, 'HX-Trigger-After-Settle', finalElt)
4925
+ }
4926
+ },
4927
+ beforeSwapCallback: function() {
4766
4928
  // if we need to save history, do so, before swapping so that relative resources have the correct base URL
4767
4929
  if (historyUpdate.type) {
4768
4930
  triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo))
@@ -4774,70 +4936,8 @@ var htmx = (function() {
4774
4936
  triggerEvent(getDocument().body, 'htmx:replacedInHistory', { path: historyUpdate.path })
4775
4937
  }
4776
4938
  }
4777
-
4778
- swap(target, serverResponse, swapSpec, {
4779
- select: selectOverride || select,
4780
- selectOOB,
4781
- eventInfo: responseInfo,
4782
- anchor: responseInfo.pathInfo.anchor,
4783
- contextElement: elt,
4784
- afterSwapCallback: function() {
4785
- if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
4786
- let finalElt = elt
4787
- if (!bodyContains(elt)) {
4788
- finalElt = getDocument().body
4789
- }
4790
- handleTriggerHeader(xhr, 'HX-Trigger-After-Swap', finalElt)
4791
- }
4792
- },
4793
- afterSettleCallback: function() {
4794
- if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
4795
- let finalElt = elt
4796
- if (!bodyContains(elt)) {
4797
- finalElt = getDocument().body
4798
- }
4799
- handleTriggerHeader(xhr, 'HX-Trigger-After-Settle', finalElt)
4800
- }
4801
- maybeCall(settleResolve)
4802
- }
4803
- })
4804
- } catch (e) {
4805
- triggerErrorEvent(elt, 'htmx:swapError', responseInfo)
4806
- maybeCall(settleReject)
4807
- throw e
4808
4939
  }
4809
- }
4810
-
4811
- let shouldTransition = htmx.config.globalViewTransitions
4812
- if (swapSpec.hasOwnProperty('transition')) {
4813
- shouldTransition = swapSpec.transition
4814
- }
4815
-
4816
- if (shouldTransition &&
4817
- triggerEvent(elt, 'htmx:beforeTransition', responseInfo) &&
4818
- typeof Promise !== 'undefined' &&
4819
- // @ts-ignore experimental feature atm
4820
- document.startViewTransition) {
4821
- const settlePromise = new Promise(function(_resolve, _reject) {
4822
- settleResolve = _resolve
4823
- settleReject = _reject
4824
- })
4825
- // wrap the original doSwap() in a call to startViewTransition()
4826
- const innerDoSwap = doSwap
4827
- doSwap = function() {
4828
- // @ts-ignore experimental feature atm
4829
- document.startViewTransition(function() {
4830
- innerDoSwap()
4831
- return settlePromise
4832
- })
4833
- }
4834
- }
4835
-
4836
- if (swapSpec.swapDelay > 0) {
4837
- getWindow().setTimeout(doSwap, swapSpec.swapDelay)
4838
- } else {
4839
- doSwap()
4840
- }
4940
+ })
4841
4941
  }
4842
4942
  if (isError) {
4843
4943
  triggerErrorEvent(elt, 'htmx:responseError', mergeObjects({ error: 'Response Status Error Code ' + xhr.status + ' from ' + responseInfo.pathInfo.requestPath }, responseInfo))
@@ -4873,7 +4973,7 @@ var htmx = (function() {
4873
4973
  * @see https://htmx.org/api/#defineExtension
4874
4974
  *
4875
4975
  * @param {string} name the extension name
4876
- * @param {HtmxExtension} extension the extension definition
4976
+ * @param {Partial<HtmxExtension>} extension the extension definition
4877
4977
  */
4878
4978
  function defineExtension(name, extension) {
4879
4979
  if (extension.init) {
@@ -5038,6 +5138,9 @@ var htmx = (function() {
5038
5138
  * @property {Element} [contextElement]
5039
5139
  * @property {swapCallback} [afterSwapCallback]
5040
5140
  * @property {swapCallback} [afterSettleCallback]
5141
+ * @property {swapCallback} [beforeSwapCallback]
5142
+ * @property {string} [title]
5143
+ * @property {boolean} [historyRequest]
5041
5144
  */
5042
5145
 
5043
5146
  /**
@@ -5056,7 +5159,7 @@ var htmx = (function() {
5056
5159
  * @property {boolean} [transition]
5057
5160
  * @property {boolean} [ignoreTitle]
5058
5161
  * @property {string} [head]
5059
- * @property {'top' | 'bottom'} [scroll]
5162
+ * @property {'top' | 'bottom' | number } [scroll]
5060
5163
  * @property {string} [scrollTarget]
5061
5164
  * @property {string} [show]
5062
5165
  * @property {string} [showTarget]
@@ -5101,7 +5204,8 @@ var htmx = (function() {
5101
5204
  * @property {'true'} [HX-History-Restore-Request]
5102
5205
  */
5103
5206
 
5104
- /** @typedef HtmxAjaxHelperContext
5207
+ /**
5208
+ * @typedef HtmxAjaxHelperContext
5105
5209
  * @property {Element|string} [source]
5106
5210
  * @property {Event} [event]
5107
5211
  * @property {HtmxAjaxHandler} [handler]
@@ -5121,6 +5225,7 @@ var htmx = (function() {
5121
5225
  * @property {FormData} unfilteredFormData
5122
5226
  * @property {Object} unfilteredParameters unfilteredFormData proxy
5123
5227
  * @property {HtmxHeaderSpecification} headers
5228
+ * @property {Element} elt
5124
5229
  * @property {Element} target
5125
5230
  * @property {HttpVerb} verb
5126
5231
  * @property {HtmxElementValidationError[]} errors
@@ -5194,7 +5299,7 @@ var htmx = (function() {
5194
5299
  * @see https://github.com/bigskysoftware/htmx-extensions/blob/main/README.md
5195
5300
  * @typedef {Object} HtmxExtension
5196
5301
  * @property {(api: any) => void} init
5197
- * @property {(name: string, event: Event|CustomEvent) => boolean} onEvent
5302
+ * @property {(name: string, event: CustomEvent) => boolean} onEvent
5198
5303
  * @property {(text: string, xhr: XMLHttpRequest, elt: Element) => string} transformResponse
5199
5304
  * @property {(swapStyle: HtmxSwapStyle) => boolean} isInlineSwap
5200
5305
  * @property {(swapStyle: HtmxSwapStyle, target: Node, fragment: Node, settleInfo: HtmxSettleInfo) => boolean|Node[]} handleSwap