Nice little directory browser :D

upgrade HTMX to 2.0.6

helpimnotdrowning.net e2d879cb 9fae6eb0

verified
+483 -436
+483 -436
public/htmx.js
··· 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 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.4' 292 + version: '2.0.6' 281 293 } 282 294 // Tsc madness part 2 283 295 htmx.onLoad = onLoadHelper ··· 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 /** ··· 810 819 * @returns {boolean} 811 820 */ 812 821 function canAccessLocalStorage() { 813 - const test = 'htmx:localStorageTest' 822 + const test = 'htmx:sessionStorageTest' 814 823 try { 815 - localStorage.setItem(test, test) 816 - localStorage.removeItem(test) 824 + sessionStorage.setItem(test, test) 825 + sessionStorage.removeItem(test) 817 826 return true 818 827 } catch (e) { 819 828 return false ··· 825 834 * @returns {string} 826 835 */ 827 836 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 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(/\/+$/, '') 841 845 } 846 + return path 842 847 } 843 848 844 849 //= ========================================================================================= ··· 1074 1079 */ 1075 1080 function closest(elt, selector) { 1076 1081 elt = asElement(resolveTarget(elt)) 1077 - if (elt && elt.closest) { 1082 + if (elt) { 1078 1083 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 1084 } 1085 + return null 1089 1086 } 1090 1087 1091 1088 /** ··· 1160 1157 const selector = normalizeSelector(parts.shift()) 1161 1158 let item 1162 1159 if (selector.indexOf('closest ') === 0) { 1163 - item = closest(asElement(elt), normalizeSelector(selector.substr(8))) 1160 + item = closest(asElement(elt), normalizeSelector(selector.slice(8))) 1164 1161 } else if (selector.indexOf('find ') === 0) { 1165 - item = find(asParentNode(elt), normalizeSelector(selector.substr(5))) 1162 + item = find(asParentNode(elt), normalizeSelector(selector.slice(5))) 1166 1163 } else if (selector === 'next' || selector === 'nextElementSibling') { 1167 1164 item = asElement(elt).nextElementSibling 1168 1165 } else if (selector.indexOf('next ') === 0) { 1169 - item = scanForwardQuery(elt, normalizeSelector(selector.substr(5)), !!global) 1166 + item = scanForwardQuery(elt, normalizeSelector(selector.slice(5)), !!global) 1170 1167 } else if (selector === 'previous' || selector === 'previousElementSibling') { 1171 1168 item = asElement(elt).previousElementSibling 1172 1169 } else if (selector.indexOf('previous ') === 0) { 1173 - item = scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)), !!global) 1170 + item = scanBackwardsQuery(elt, normalizeSelector(selector.slice(9)), !!global) 1174 1171 } else if (selector === 'document') { 1175 1172 item = document 1176 1173 } else if (selector === 'window') { ··· 1350 1347 return [findThisElement(elt, attrName)] 1351 1348 } else { 1352 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 + } 1353 1360 if (result.length === 0) { 1354 1361 logError('The selector "' + attrTarget + '" on ' + attrName + ' returned no matches!') 1355 1362 return [DUMMY_ELT] ··· 1398 1405 * @returns {boolean} 1399 1406 */ 1400 1407 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 + return htmx.config.attributesToSettle.includes(name) 1408 1409 } 1409 1410 1410 1411 /** ··· 1453 1454 */ 1454 1455 function oobSwap(oobValue, oobElement, settleInfo, rootNode) { 1455 1456 rootNode = rootNode || getDocument() 1456 - let selector = '#' + getRawAttribute(oobElement, 'id') 1457 + let selector = '#' + CSS.escape(getRawAttribute(oobElement, 'id')) 1457 1458 /** @type HtmxSwapStyle */ 1458 1459 let swapStyle = 'outerHTML' 1459 1460 if (oobValue === 'true') { ··· 1468 1469 oobElement.removeAttribute('data-hx-swap-oob') 1469 1470 1470 1471 const targets = querySelectorAllExt(rootNode, selector, false) 1471 - console.log('targets: ') 1472 - console.log(targets) 1473 - if (targets) { 1472 + if (targets.length) { 1474 1473 forEach( 1475 1474 targets, 1476 1475 function(target) { ··· 1628 1627 */ 1629 1628 function attributeHash(elt) { 1630 1629 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 - } 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) 1639 1635 } 1640 1636 } 1641 1637 return hash ··· 1680 1676 function cleanUpElement(element) { 1681 1677 triggerEvent(element, 'htmx:beforeCleanupElement') 1682 1678 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 - } 1679 + // @ts-ignore 1680 + forEach(element.children, function(child) { cleanUpElement(child) }) 1689 1681 } 1690 1682 1691 1683 /** 1692 - * @param {Node} target 1684 + * @param {Element} target 1693 1685 * @param {ParentNode} fragment 1694 1686 * @param {HtmxSettleInfo} settleInfo 1695 1687 */ 1696 1688 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 1689 + if (target.tagName === 'BODY') { // special case the body to innerHTML because DocumentFragments can't contain a body elt unfortunately 1698 1690 return swapInnerHTML(target, fragment, settleInfo) 1699 1691 } 1700 1692 /** @type {Node} */ ··· 1720 1712 newElt = newElt.nextSibling 1721 1713 } 1722 1714 cleanUpElement(target) 1723 - if (target instanceof Element) { 1724 - target.remove() 1725 - } else { 1726 - target.parentNode.removeChild(target) 1727 - } 1715 + target.remove() 1728 1716 } 1729 1717 1730 1718 /** 1731 - * @param {Node} target 1719 + * @param {Element} target 1732 1720 * @param {ParentNode} fragment 1733 1721 * @param {HtmxSettleInfo} settleInfo 1734 1722 */ ··· 1737 1725 } 1738 1726 1739 1727 /** 1740 - * @param {Node} target 1728 + * @param {Element} target 1741 1729 * @param {ParentNode} fragment 1742 1730 * @param {HtmxSettleInfo} settleInfo 1743 1731 */ ··· 1746 1734 } 1747 1735 1748 1736 /** 1749 - * @param {Node} target 1737 + * @param {Element} target 1750 1738 * @param {ParentNode} fragment 1751 1739 * @param {HtmxSettleInfo} settleInfo 1752 1740 */ ··· 1755 1743 } 1756 1744 1757 1745 /** 1758 - * @param {Node} target 1746 + * @param {Element} target 1759 1747 * @param {ParentNode} fragment 1760 1748 * @param {HtmxSettleInfo} settleInfo 1761 1749 */ ··· 1764 1752 } 1765 1753 1766 1754 /** 1767 - * @param {Node} target 1755 + * @param {Element} target 1768 1756 */ 1769 1757 function swapDelete(target) { 1770 1758 cleanUpElement(target) ··· 1775 1763 } 1776 1764 1777 1765 /** 1778 - * @param {Node} target 1766 + * @param {Element} target 1779 1767 * @param {ParentNode} fragment 1780 1768 * @param {HtmxSettleInfo} settleInfo 1781 1769 */ ··· 1795 1783 /** 1796 1784 * @param {HtmxSwapStyle} swapStyle 1797 1785 * @param {Element} elt 1798 - * @param {Node} target 1786 + * @param {Element} target 1799 1787 * @param {ParentNode} fragment 1800 1788 * @param {HtmxSettleInfo} settleInfo 1801 1789 */ ··· 1873 1861 } 1874 1862 1875 1863 /** 1876 - * Implements complete swapping pipeline, including: focus and selection preservation, 1864 + * Implements complete swapping pipeline, including: delay, view transitions, focus and selection preservation, 1877 1865 * title updates, scroll, OOB swapping, normal swapping and settling 1878 1866 * @param {string|Element} target 1879 1867 * @param {string} content ··· 1884 1872 if (!swapOptions) { 1885 1873 swapOptions = {} 1886 1874 } 1875 + // optional transition API promise callbacks 1876 + let settleResolve = null 1877 + let settleReject = null 1887 1878 1888 - target = resolveTarget(target) 1889 - const rootNode = swapOptions.contextElement ? getRootNode(swapOptions.contextElement, false) : getDocument() 1879 + let doSwap = function() { 1880 + maybeCall(swapOptions.beforeSwapCallback) 1881 + 1882 + target = resolveTarget(target) 1883 + const rootNode = swapOptions.contextElement ? getRootNode(swapOptions.contextElement, false) : getDocument() 1890 1884 1891 - // preserve focus and selection 1892 - const activeElt = document.activeElement 1893 - let selectionInfo = {} 1894 - try { 1885 + // preserve focus and selection 1886 + const activeElt = document.activeElement 1887 + let selectionInfo = {} 1895 1888 selectionInfo = { 1896 1889 elt: activeElt, 1897 1890 // @ts-ignore ··· 1899 1892 // @ts-ignore 1900 1893 end: activeElt ? activeElt.selectionEnd : null 1901 1894 } 1902 - } catch (e) { 1903 - // safari issue - see https://github.com/microsoft/playwright/issues/5894 1904 - } 1905 - const settleInfo = makeSettleInfo(target) 1895 + const settleInfo = makeSettleInfo(target) 1906 1896 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) 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) 1913 1903 1914 - settleInfo.title = fragment.title 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 + } 1915 1909 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) 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 1924 } 1925 - const oobValue = oobSelectValue[1] || 'true' 1926 - const oobElement = fragment.querySelector('#' + id) 1927 - if (oobElement) { 1928 - 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() 1929 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 1930 1942 } 1943 + handlePreservedElements(fragment) 1944 + swapWithStyle(swapSpec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo) 1945 + restorePreservedElements() 1931 1946 } 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() 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) 1938 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) 1939 1974 }) 1975 + maybeCall(swapOptions.afterSwapCallback) 1940 1976 1941 - // normal swap 1942 - if (swapOptions.select) { 1943 - const newFragment = getDocument().createDocumentFragment() 1944 - forEach(fragment.querySelectorAll(swapOptions.select), function(node) { 1945 - newFragment.appendChild(node) 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) 1946 1992 }) 1947 - fragment = newFragment 1948 - } 1949 - handlePreservedElements(fragment) 1950 - swapWithStyle(swapSpec.swapStyle, swapOptions.contextElement, target, fragment, settleInfo) 1951 - restorePreservedElements() 1952 - } 1953 1993 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 1994 + if (swapOptions.anchor) { 1995 + const anchorTarget = asElement(resolveTarget('#' + swapOptions.anchor)) 1996 + if (anchorTarget) { 1997 + anchorTarget.scrollIntoView({ block: 'start', behavior: 'auto' }) 1968 1998 } 1969 1999 } 1970 - newActiveElt.focus(focusOptions) 2000 + 2001 + updateScrollState(settleInfo.elts, swapSpec) 2002 + maybeCall(swapOptions.afterSettleCallback) 2003 + maybeCall(settleResolve) 1971 2004 } 1972 - } 1973 2005 1974 - target.classList.remove(htmx.config.swappingClass) 1975 - forEach(settleInfo.elts, function(elt) { 1976 - if (elt.classList) { 1977 - elt.classList.add(htmx.config.settlingClass) 2006 + if (swapSpec.settleDelay > 0) { 2007 + getWindow().setTimeout(doSettle, swapSpec.settleDelay) 2008 + } else { 2009 + doSettle() 1978 2010 } 1979 - triggerEvent(elt, 'htmx:afterSwap', swapOptions.eventInfo) 1980 - }) 1981 - if (swapOptions.afterSwapCallback) { 1982 - swapOptions.afterSwapCallback() 1983 2011 } 1984 - 1985 - // merge in new title after swap but before settle 1986 - if (!swapSpec.ignoreTitle) { 1987 - handleTitle(settleInfo.title) 2012 + let shouldTransition = htmx.config.globalViewTransitions 2013 + if (swapSpec.hasOwnProperty('transition')) { 2014 + shouldTransition = swapSpec.transition 1988 2015 } 1989 2016 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 - } 2017 + const elt = swapOptions.contextElement || getDocument() 2008 2018 2009 - updateScrollState(settleInfo.elts, swapSpec) 2010 - if (swapOptions.afterSettleCallback) { 2011 - 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 + }) 2012 2036 } 2013 2037 } 2014 2038 2015 - if (swapSpec.settleDelay > 0) { 2016 - getWindow().setTimeout(doSettle, swapSpec.settleDelay) 2017 - } else { 2018 - 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 2019 2049 } 2020 2050 } 2021 2051 ··· 2369 2399 if (path == null || path === '') { 2370 2400 // if there is no action attribute on the form set path to current href before the 2371 2401 // following logic to properly clear parameters on a GET (not on a POST!) 2372 - path = getDocument().location.href 2402 + path = location.href 2373 2403 } 2374 2404 if (verb === 'get' && path.includes('?')) { 2375 2405 path = path.replace(/\?[^#]+/, '') ··· 2390 2420 2391 2421 /** 2392 2422 * @param {Event} evt 2393 - * @param {Node} node 2423 + * @param {Element} elt 2394 2424 * @returns {boolean} 2395 2425 */ 2396 - function shouldCancel(evt, node) { 2397 - const elt = asElement(node) 2398 - if (!elt) { 2399 - return false 2400 - } 2426 + function shouldCancel(evt, elt) { 2401 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 2402 2430 if (elt.tagName === 'FORM') { 2403 2431 return true 2404 2432 } 2405 - if (matches(elt, 'input[type="submit"], button') && 2406 - (matches(elt, '[form]') || 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') { 2407 2436 return true 2408 2437 } 2409 - if (elt instanceof HTMLAnchorElement && elt.href && 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 && 2410 2441 (elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf('#') !== 0)) { 2411 2442 return true 2412 2443 } ··· 2446 2477 } 2447 2478 2448 2479 /** 2449 - * @param {Node} elt 2480 + * @param {Element} elt 2450 2481 * @param {TriggerHandler} handler 2451 2482 * @param {HtmxNodeInternalData} nodeData 2452 2483 * @param {HtmxTriggerSpecification} triggerSpec ··· 2513 2544 } 2514 2545 } 2515 2546 if (triggerSpec.changed) { 2516 - const node = event.target 2547 + const node = evt.target 2517 2548 // @ts-ignore value will be undefined for non-input elements, which is fine 2518 2549 const value = node.value 2519 2550 const lastValue = elementData.lastValue.get(triggerSpec) ··· 2636 2667 triggerSpecs.forEach(function(triggerSpec) { 2637 2668 addTriggerHandler(elt, triggerSpec, nodeData, function(node, evt) { 2638 2669 const elt = asElement(node) 2639 - if (closest(elt, htmx.config.disableSelector)) { 2670 + if (eltIsDisabled(elt)) { 2640 2671 cleanUpElement(elt) 2641 2672 return 2642 2673 } ··· 2650 2681 2651 2682 /** 2652 2683 * @callback TriggerHandler 2653 - * @param {Node} elt 2684 + * @param {Element} elt 2654 2685 * @param {Event} [evt] 2655 2686 */ 2656 2687 2657 2688 /** 2658 - * @param {Node} elt 2689 + * @param {Element} elt 2659 2690 * @param {HtmxTriggerSpecification} triggerSpec 2660 2691 * @param {HtmxNodeInternalData} nodeData 2661 2692 * @param {TriggerHandler} handler ··· 2780 2811 * @param {Event} evt 2781 2812 */ 2782 2813 function maybeSetLastButtonClicked(evt) { 2783 - const elt = /** @type {HTMLButtonElement|HTMLInputElement} */ (closest(asElement(evt.target), "button, input[type='submit']")) 2814 + const elt = getTargetButton(evt.target) 2784 2815 const internalData = getRelatedFormData(evt) 2785 2816 if (internalData) { 2786 2817 internalData.lastButtonClicked = elt ··· 2795 2826 if (internalData) { 2796 2827 internalData.lastButtonClicked = null 2797 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') 2798 2846 } 2799 2847 2800 2848 /** ··· 2802 2850 * @returns {HtmxNodeInternalData|undefined} 2803 2851 */ 2804 2852 function getRelatedFormData(evt) { 2805 - const elt = closest(asElement(evt.target), "button, input[type='submit']") 2853 + const elt = getTargetButton(evt.target) 2806 2854 if (!elt) { 2807 2855 return 2808 2856 } 2809 - const form = resolveTarget('#' + getRawAttribute(elt, 'form'), elt.getRootNode()) || closest(elt, 'form') 2810 - if (!form) { 2811 - return 2812 - } 2857 + const form = getRelatedForm(elt) 2813 2858 return getInternalData(form) 2814 2859 } 2815 2860 ··· 2886 2931 * @param {Element|HTMLInputElement} elt 2887 2932 */ 2888 2933 function initNode(elt) { 2889 - if (closest(elt, htmx.config.disableSelector)) { 2890 - cleanUpElement(elt) 2891 - return 2892 - } 2934 + triggerEvent(elt, 'htmx:beforeProcessNode') 2935 + 2893 2936 const nodeData = getInternalData(elt) 2894 - const attrHash = attributeHash(elt) 2895 - if (nodeData.initHash !== attrHash) { 2896 - // clean up any previously processed info 2897 - deInitNode(elt) 2937 + const triggerSpecs = getTriggerSpecs(elt) 2938 + const hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs) 2898 2939 2899 - nodeData.initHash = attrHash 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 + } 2900 2951 2901 - triggerEvent(elt, 'htmx:beforeProcessNode') 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 + } 2902 2957 2903 - const triggerSpecs = getTriggerSpecs(elt) 2904 - const hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs) 2958 + nodeData.firstInitCompleted = true 2959 + triggerEvent(elt, 'htmx:afterProcessNode') 2960 + } 2905 2961 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 - } 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 + } 2923 2971 2924 - nodeData.firstInitCompleted = true 2925 - triggerEvent(elt, 'htmx:afterProcessNode') 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 2926 2978 } 2979 + return false 2927 2980 } 2928 2981 2929 2982 /** ··· 2935 2988 */ 2936 2989 function processNode(elt) { 2937 2990 elt = resolveTarget(elt) 2938 - if (closest(elt, htmx.config.disableSelector)) { 2991 + if (eltIsDisabled(elt)) { 2939 2992 cleanUpElement(elt) 2940 2993 return 2941 2994 } 2942 - initNode(elt) 2943 - forEach(findElementsToProcess(elt), function(child) { initNode(child) }) 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 + 2944 3010 forEach(findHxOnWildcardElements(elt), processHxOnWildcard) 3011 + forEach(elementsToInit, initNode) 2945 3012 } 2946 3013 2947 3014 //= =================================================================== ··· 2962 3029 * @returns {CustomEvent} 2963 3030 */ 2964 3031 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 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 }) 2975 3035 } 2976 3036 2977 3037 /** ··· 2993 3053 2994 3054 /** 2995 3055 * `withExtensions` locates all active extensions for a provided element, then 2996 - * executes the provided function using each of the active extensions. It should 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 2997 3058 * be called internally at every extendable execution point in htmx. 2998 3059 * 2999 3060 * @param {Element} elt 3000 3061 * @param {(extension:HtmxExtension) => void} toDo 3062 + * @param {string[]=} extensionsToIgnore 3001 3063 * @returns void 3002 3064 */ 3003 - function withExtensions(elt, toDo) { 3004 - forEach(getExtensions(elt), function(extension) { 3065 + function withExtensions(elt, toDo, extensionsToIgnore) { 3066 + forEach(getExtensions(elt, [], extensionsToIgnore), function(extension) { 3005 3067 try { 3006 3068 toDo(extension) 3007 3069 } catch (e) { ··· 3011 3073 } 3012 3074 3013 3075 function logError(msg) { 3014 - if (console.error) { 3015 - console.error(msg) 3016 - } else if (console.log) { 3017 - console.log('ERROR: ', msg) 3018 - } 3076 + console.error(msg) 3019 3077 } 3020 3078 3021 3079 /** ··· 3060 3118 let currentPathForHistory = location.pathname + location.search 3061 3119 3062 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 + /** 3063 3131 * @returns {Element} 3064 3132 */ 3065 3133 function getHistoryElement() { ··· 3083 3151 3084 3152 if (htmx.config.historyCacheSize <= 0) { 3085 3153 // make sure that an eventually already existing cache is purged 3086 - localStorage.removeItem('htmx-history-cache') 3154 + sessionStorage.removeItem('htmx-history-cache') 3087 3155 return 3088 3156 } 3089 3157 3090 3158 url = normalizePath(url) 3091 3159 3092 - const historyCache = parseJSON(localStorage.getItem('htmx-history-cache')) || [] 3160 + const historyCache = parseJSON(sessionStorage.getItem('htmx-history-cache')) || [] 3093 3161 for (let i = 0; i < historyCache.length; i++) { 3094 3162 if (historyCache[i].url === url) { 3095 3163 historyCache.splice(i, 1) ··· 3110 3178 // keep trying to save the cache until it succeeds or is empty 3111 3179 while (historyCache.length > 0) { 3112 3180 try { 3113 - localStorage.setItem('htmx-history-cache', JSON.stringify(historyCache)) 3181 + sessionStorage.setItem('htmx-history-cache', JSON.stringify(historyCache)) 3114 3182 break 3115 3183 } catch (e) { 3116 3184 triggerErrorEvent(getDocument().body, 'htmx:historyCacheError', { cause: e, cache: historyCache }) ··· 3138 3206 3139 3207 url = normalizePath(url) 3140 3208 3141 - const historyCache = parseJSON(localStorage.getItem('htmx-history-cache')) || [] 3209 + const historyCache = parseJSON(sessionStorage.getItem('htmx-history-cache')) || [] 3142 3210 for (let i = 0; i < historyCache.length; i++) { 3143 3211 if (historyCache[i].url === url) { 3144 3212 return historyCache[i] ··· 3166 3234 3167 3235 function saveCurrentPageToHistory() { 3168 3236 const elt = getHistoryElement() 3169 - const path = currentPathForHistory || location.pathname + location.search 3237 + let path = currentPathForHistory 3238 + if (canAccessLocalStorage()) { 3239 + path = sessionStorage.getItem('htmx-current-path-for-history') 3240 + } 3241 + path = path || location.pathname + location.search 3170 3242 3171 3243 // Allow history snapshot feature to be disabled where hx-history="false" 3172 3244 // is present *anywhere* in the current document we're about to save, 3173 3245 // so we can prevent privileged data entering the cache. 3174 3246 // 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 - } 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]') 3183 3249 if (!disableHistoryCache) { 3184 3250 triggerEvent(getDocument().body, 'htmx:beforeHistorySave', { path, historyElt: elt }) 3185 3251 saveToHistoryCache(path, elt) 3186 3252 } 3187 3253 3188 - if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, getDocument().title, window.location.href) 3254 + if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, getDocument().title, location.href) 3189 3255 } 3190 3256 3191 3257 /** ··· 3202 3268 if (htmx.config.historyEnabled) { 3203 3269 history.pushState({ htmx: true }, '', path) 3204 3270 } 3205 - currentPathForHistory = path 3271 + setCurrentPathForHistory(path) 3206 3272 } 3207 3273 3208 3274 /** ··· 3210 3276 */ 3211 3277 function replaceUrlInHistory(path) { 3212 3278 if (htmx.config.historyEnabled) history.replaceState({ htmx: true }, '', path) 3213 - currentPathForHistory = path 3279 + setCurrentPathForHistory(path) 3214 3280 } 3215 3281 3216 3282 /** ··· 3227 3293 */ 3228 3294 function loadHistoryFromServer(path) { 3229 3295 const request = new XMLHttpRequest() 3230 - const details = { path, xhr: request } 3231 - triggerEvent(getDocument().body, 'htmx:historyCacheMiss', details) 3296 + const swapSpec = { swapStyle: 'innerHTML', swapDelay: 0, settleDelay: 0 } 3297 + const details = { path, xhr: request, historyElt: getHistoryElement(), swapSpec } 3232 3298 request.open('GET', path, true) 3233 - request.setRequestHeader('HX-Request', 'true') 3299 + if (htmx.config.historyRestoreAsHxRequest) { 3300 + request.setRequestHeader('HX-Request', 'true') 3301 + } 3234 3302 request.setRequestHeader('HX-History-Restore-Request', 'true') 3235 - request.setRequestHeader('HX-Current-URL', getDocument().location.href) 3303 + request.setRequestHeader('HX-Current-URL', location.href) 3236 3304 request.onload = function() { 3237 3305 if (this.status >= 200 && this.status < 400) { 3306 + details.response = this.response 3238 3307 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 }) 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 }) 3252 3314 } else { 3253 3315 triggerErrorEvent(getDocument().body, 'htmx:historyCacheMissLoadError', details) 3254 3316 } 3255 3317 } 3256 - request.send() 3318 + if (triggerEvent(getDocument().body, 'htmx:historyCacheMiss', details)) { 3319 + request.send() // only send request if event not prevented 3320 + } 3257 3321 } 3258 3322 3259 3323 /** ··· 3264 3328 path = path || location.pathname + location.search 3265 3329 const cached = getCachedHistory(path) 3266 3330 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 }) 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 + } 3280 3341 } else { 3281 3342 if (htmx.config.refreshOnHistoryMiss) { 3282 3343 // @ts-ignore: optional parameter in reload() function throws error 3283 3344 // noinspection JSUnresolvedReference 3284 - window.location.reload(true) 3345 + htmx.location.reload(true) 3285 3346 } else { 3286 3347 loadHistoryFromServer(path) 3287 3348 } ··· 3386 3447 return true 3387 3448 } 3388 3449 3389 - /** @param {string} name 3450 + /** 3451 + * @param {string} name 3390 3452 * @param {string|Array|FormDataEntryValue} value 3391 3453 * @param {FormData} formData */ 3392 3454 function addValueToFormData(name, value, formData) { ··· 3399 3461 } 3400 3462 } 3401 3463 3402 - /** @param {string} name 3464 + /** 3465 + * @param {string} name 3403 3466 * @param {string|Array} value 3404 3467 * @param {FormData} formData */ 3405 3468 function removeValueFromFormData(name, value, formData) { ··· 3416 3479 } 3417 3480 3418 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 + /** 3419 3498 * @param {Element[]} processed 3420 3499 * @param {FormData} formData 3421 3500 * @param {HtmxElementValidationError[]} errors ··· 3430 3509 } 3431 3510 if (shouldInclude(elt)) { 3432 3511 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) 3512 + addValueToFormData(name, getValueFromInput(elt), formData) 3443 3513 if (validate) { 3444 3514 validateElement(elt, errors) 3445 3515 } ··· 3450 3520 // The input has already been processed and added to the values, but the FormData that will be 3451 3521 // constructed right after on the form, will include it once again. So remove that input's value 3452 3522 // now to avoid duplicates 3453 - removeValueFromFormData(input.name, input.value, formData) 3523 + removeValueFromFormData(input.name, getValueFromInput(input), formData) 3454 3524 } else { 3455 3525 processed.push(input) 3456 3526 } ··· 3468 3538 } 3469 3539 3470 3540 /** 3471 - * 3472 3541 * @param {Element} elt 3473 3542 * @param {HtmxElementValidationError[]} errors 3474 3543 */ ··· 3523 3592 validate = validate && internalData.lastButtonClicked.formNoValidate !== true 3524 3593 } 3525 3594 3526 - // for a non-GET include the closest form 3595 + // for a non-GET include the related form, which may or may not be a parent element of elt 3527 3596 if (verb !== 'get') { 3528 - processInputValue(processed, priorityFormData, errors, closest(elt, 'form'), validate) 3597 + processInputValue(processed, priorityFormData, errors, getRelatedForm(elt), validate) 3529 3598 } 3530 3599 3531 3600 // include the element itself ··· 3605 3674 'HX-Trigger': getRawAttribute(elt, 'id'), 3606 3675 'HX-Trigger-Name': getRawAttribute(elt, 'name'), 3607 3676 'HX-Target': getAttributeValue(target, 'id'), 3608 - 'HX-Current-URL': getDocument().location.href 3677 + 'HX-Current-URL': location.href 3609 3678 } 3610 3679 getValuesForElement(elt, 'hx-headers', false, headers) 3611 3680 if (prompt !== undefined) { ··· 3783 3852 target = target || last 3784 3853 target.scrollTop = target.scrollHeight 3785 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 + } 3786 3860 } 3787 3861 if (swapSpec.show) { 3788 3862 var target = null ··· 3811 3885 * @param {string} attr 3812 3886 * @param {boolean=} evalAsDefault 3813 3887 * @param {Object=} values 3888 + * @param {Event=} event 3814 3889 * @returns {Object} 3815 3890 */ 3816 - function getValuesForElement(elt, attr, evalAsDefault, values) { 3891 + function getValuesForElement(elt, attr, evalAsDefault, values, event) { 3817 3892 if (values == null) { 3818 3893 values = {} 3819 3894 } ··· 3839 3914 } 3840 3915 let varsValues 3841 3916 if (evaluateValue) { 3842 - varsValues = maybeEval(elt, function() { return Function('return (' + str + ')')() }, {}) 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 + }, {}) 3843 3924 } else { 3844 3925 varsValues = parseJSON(str) 3845 3926 } ··· 3851 3932 } 3852 3933 } 3853 3934 } 3854 - return getValuesForElement(asElement(parentElt(elt)), attr, evalAsDefault, values) 3935 + return getValuesForElement(asElement(parentElt(elt)), attr, evalAsDefault, values, event) 3855 3936 } 3856 3937 3857 3938 /** ··· 3871 3952 3872 3953 /** 3873 3954 * @param {Element} elt 3874 - * @param {*?} expressionVars 3955 + * @param {Event=} event 3956 + * @param {*?=} expressionVars 3875 3957 * @returns 3876 3958 */ 3877 - function getHXVarsForElement(elt, expressionVars) { 3878 - return getValuesForElement(elt, 'hx-vars', true, expressionVars) 3959 + function getHXVarsForElement(elt, event, expressionVars) { 3960 + return getValuesForElement(elt, 'hx-vars', true, expressionVars, event) 3879 3961 } 3880 3962 3881 3963 /** 3882 3964 * @param {Element} elt 3883 - * @param {*?} expressionVars 3965 + * @param {Event=} event 3966 + * @param {*?=} expressionVars 3884 3967 * @returns 3885 3968 */ 3886 - function getHXValsForElement(elt, expressionVars) { 3887 - return getValuesForElement(elt, 'hx-vals', false, expressionVars) 3969 + function getHXValsForElement(elt, event, expressionVars) { 3970 + return getValuesForElement(elt, 'hx-vals', false, expressionVars, event) 3888 3971 } 3889 3972 3890 3973 /** 3891 3974 * @param {Element} elt 3975 + * @param {Event=} event 3892 3976 * @returns {FormData} 3893 3977 */ 3894 - function getExpressionVars(elt) { 3895 - return mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt)) 3978 + function getExpressionVars(elt, event) { 3979 + return mergeObjects(getHXVarsForElement(elt, event), getHXValsForElement(elt, event)) 3896 3980 } 3897 3981 3898 3982 /** ··· 3917 4001 * @return {string} 3918 4002 */ 3919 4003 function getPathFromResponse(xhr) { 3920 - // NB: IE11 does not support this stuff 3921 - if (xhr.responseURL && typeof (URL) !== 'undefined') { 4004 + if (xhr.responseURL) { 3922 4005 try { 3923 4006 const url = new URL(xhr.responseURL) 3924 4007 return url.pathname + url.search ··· 4000 4083 * @return {boolean} 4001 4084 */ 4002 4085 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 - } 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 4014 4089 4015 4090 if (htmx.config.selfRequestsOnly) { 4016 4091 if (!sameHost) { ··· 4111 4186 return function() { 4112 4187 return formData[name].apply(formData, arguments) 4113 4188 } 4114 - } else { 4115 - return target[name] 4116 4189 } 4117 4190 } 4118 4191 const array = formData.getAll(name) ··· 4187 4260 } 4188 4261 const target = etc.targetOverride || asElement(getTarget(elt)) 4189 4262 if (target == null || target == DUMMY_ELT) { 4190 - triggerErrorEvent(elt, 'htmx:targetError', { target: getAttributeValue(elt, 'hx-target') }) 4263 + triggerErrorEvent(elt, 'htmx:targetError', { target: getClosestAttributeValue(elt, 'hx-target') }) 4191 4264 maybeCall(reject) 4192 4265 return promise 4193 4266 } ··· 4203 4276 4204 4277 const buttonVerb = getRawAttribute(submitter, 'formmethod') 4205 4278 if (buttonVerb != null) { 4206 - // ignore buttons with formmethod="dialog" 4207 - if (buttonVerb.toLowerCase() !== 'dialog') { 4279 + if (VERBS.includes(buttonVerb.toLowerCase())) { 4208 4280 verb = (/** @type HttpVerb */(buttonVerb)) 4281 + } else { 4282 + maybeCall(resolve) 4283 + return promise 4209 4284 } 4210 4285 } 4211 4286 } ··· 4340 4415 if (etc.values) { 4341 4416 overrideFormData(rawFormData, formDataFromObject(etc.values)) 4342 4417 } 4343 - const expressionVars = formDataFromObject(getExpressionVars(elt)) 4418 + const expressionVars = formDataFromObject(getExpressionVars(elt, event)) 4344 4419 const allFormData = overrideFormData(rawFormData, expressionVars) 4345 4420 let filteredFormData = filterValues(allFormData, elt) 4346 4421 ··· 4350 4425 4351 4426 // behavior of anchors w/ empty href is to use the current URL 4352 4427 if (path == null || path === '') { 4353 - path = getDocument().location.href 4428 + path = location.href 4354 4429 } 4355 4430 4356 4431 /** ··· 4374 4449 unfilteredFormData: allFormData, 4375 4450 unfilteredParameters: formDataProxy(allFormData), 4376 4451 headers, 4452 + elt, 4377 4453 target, 4378 4454 verb, 4379 4455 errors, ··· 4428 4504 if (!verifyPath(elt, finalPath, requestConfig)) { 4429 4505 triggerErrorEvent(elt, 'htmx:invalidPath', requestConfig) 4430 4506 maybeCall(reject) 4507 + endRequestLock() 4431 4508 return promise 4432 4509 } 4433 4510 ··· 4490 4567 } 4491 4568 } 4492 4569 maybeCall(resolve) 4493 - endRequestLock() 4494 4570 } catch (e) { 4495 4571 triggerErrorEvent(elt, 'htmx:onLoadError', mergeObjects({ error: e }, responseInfo)) 4496 4572 throw e 4573 + } finally { 4574 + endRequestLock() 4497 4575 } 4498 4576 } 4499 4577 xhr.onerror = function() { ··· 4668 4746 if (title) { 4669 4747 const titleElt = find('title') 4670 4748 if (titleElt) { 4671 - titleElt.innerHTML = title 4749 + titleElt.textContent = title 4672 4750 } else { 4673 4751 window.document.title = title 4674 4752 } ··· 4676 4754 } 4677 4755 4678 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 + /** 4679 4775 * @param {Element} elt 4680 4776 * @param {HtmxResponseInfo} responseInfo 4681 4777 */ ··· 4712 4808 4713 4809 if (hasHeader(xhr, /HX-Redirect:/i)) { 4714 4810 responseInfo.keepIndicators = true 4715 - location.href = xhr.getResponseHeader('HX-Redirect') 4716 - shouldRefresh && location.reload() 4811 + htmx.location.href = xhr.getResponseHeader('HX-Redirect') 4812 + shouldRefresh && htmx.location.reload() 4717 4813 return 4718 4814 } 4719 4815 4720 4816 if (shouldRefresh) { 4721 4817 responseInfo.keepIndicators = true 4722 - location.reload() 4818 + htmx.location.reload() 4723 4819 return 4724 4820 } 4725 4821 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 4822 const historyUpdate = determineHistoryUpdates(elt, responseInfo) 4735 4823 4736 4824 const responseHandling = resolveResponseHandling(xhr) ··· 4739 4827 let ignoreTitle = htmx.config.ignoreTitle || responseHandling.ignoreTitle 4740 4828 let selectOverride = responseHandling.select 4741 4829 if (responseHandling.target) { 4742 - responseInfo.target = asElement(querySelectorExt(elt, responseHandling.target)) 4830 + responseInfo.target = resolveRetarget(elt, responseHandling.target) 4743 4831 } 4744 4832 var swapOverride = etc.swapOverride 4745 4833 if (swapOverride == null && responseHandling.swapOverride) { ··· 4748 4836 4749 4837 // response headers override response handling config 4750 4838 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 - } 4839 + responseInfo.target = resolveRetarget(elt, xhr.getResponseHeader('HX-Retarget')) 4756 4840 } 4841 + 4757 4842 if (hasHeader(xhr, /HX-Reswap:/i)) { 4758 4843 swapOverride = xhr.getResponseHeader('HX-Reswap') 4759 4844 } ··· 4806 4891 4807 4892 target.classList.add(htmx.config.swappingClass) 4808 4893 4809 - // optional transition API promise callbacks 4810 - let settleResolve = null 4811 - let settleReject = null 4812 - 4813 4894 if (responseInfoSelect) { 4814 4895 selectOverride = responseInfoSelect 4815 4896 } ··· 4821 4902 const selectOOB = getClosestAttributeValue(elt, 'hx-select-oob') 4822 4903 const select = getClosestAttributeValue(elt, 'hx-select') 4823 4904 4824 - let doSwap = function() { 4825 - try { 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() { 4826 4930 // if we need to save history, do so, before swapping so that relative resources have the correct base URL 4827 4931 if (historyUpdate.type) { 4828 4932 triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo)) ··· 4834 4938 triggerEvent(getDocument().body, 'htmx:replacedInHistory', { path: historyUpdate.path }) 4835 4939 } 4836 4940 } 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 4941 } 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 - } 4942 + }) 4901 4943 } 4902 4944 if (isError) { 4903 4945 triggerErrorEvent(elt, 'htmx:responseError', mergeObjects({ error: 'Response Status Error Code ' + xhr.status + ' from ' + responseInfo.pathInfo.requestPath }, responseInfo)) ··· 5098 5140 * @property {Element} [contextElement] 5099 5141 * @property {swapCallback} [afterSwapCallback] 5100 5142 * @property {swapCallback} [afterSettleCallback] 5143 + * @property {swapCallback} [beforeSwapCallback] 5144 + * @property {string} [title] 5145 + * @property {boolean} [historyRequest] 5101 5146 */ 5102 5147 5103 5148 /** ··· 5116 5161 * @property {boolean} [transition] 5117 5162 * @property {boolean} [ignoreTitle] 5118 5163 * @property {string} [head] 5119 - * @property {'top' | 'bottom'} [scroll] 5164 + * @property {'top' | 'bottom' | number } [scroll] 5120 5165 * @property {string} [scrollTarget] 5121 5166 * @property {string} [show] 5122 5167 * @property {string} [showTarget] ··· 5161 5206 * @property {'true'} [HX-History-Restore-Request] 5162 5207 */ 5163 5208 5164 - /** @typedef HtmxAjaxHelperContext 5209 + /** 5210 + * @typedef HtmxAjaxHelperContext 5165 5211 * @property {Element|string} [source] 5166 5212 * @property {Event} [event] 5167 5213 * @property {HtmxAjaxHandler} [handler] ··· 5181 5227 * @property {FormData} unfilteredFormData 5182 5228 * @property {Object} unfilteredParameters unfilteredFormData proxy 5183 5229 * @property {HtmxHeaderSpecification} headers 5230 + * @property {Element} elt 5184 5231 * @property {Element} target 5185 5232 * @property {HttpVerb} verb 5186 5233 * @property {HtmxElementValidationError[]} errors ··· 5254 5301 * @see https://github.com/bigskysoftware/htmx-extensions/blob/main/README.md 5255 5302 * @typedef {Object} HtmxExtension 5256 5303 * @property {(api: any) => void} init 5257 - * @property {(name: string, event: Event|CustomEvent) => boolean} onEvent 5304 + * @property {(name: string, event: CustomEvent) => boolean} onEvent 5258 5305 * @property {(text: string, xhr: XMLHttpRequest, elt: Element) => string} transformResponse 5259 5306 * @property {(swapStyle: HtmxSwapStyle) => boolean} isInlineSwap 5260 5307 * @property {(swapStyle: HtmxSwapStyle, target: Node, fragment: Node, settleInfo: HtmxSettleInfo) => boolean|Node[]} handleSwap