diff options
Diffstat (limited to 'src/htmx.js')
-rw-r--r-- | src/htmx.js | 246 |
1 files changed, 138 insertions, 108 deletions
diff --git a/src/htmx.js b/src/htmx.js index f11437ca..66dcd038 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -69,12 +69,13 @@ return (function () { wsBinaryType: 'blob', disableSelector: "[hx-disable], [data-hx-disable]", useTemplateFragments: false, - scrollBehavior: 'smooth', + scrollBehavior: 'instant', defaultFocusScroll: false, getCacheBusterParam: false, globalViewTransitions: false, - methodsThatUseUrlParams: ["get"], - selfRequestsOnly: false, + methodsThatUseUrlParams: ["get", "delete"], + selfRequestsOnly: true, + ignoreTitle: false, scrollIntoViewOnBoost: true, disableInheritance: false }, @@ -88,7 +89,7 @@ return (function () { sock.binaryType = htmx.config.wsBinaryType; return sock; }, - version: "1.9.8" + version: "1.9.10" }; /** @type {import("./htmx").HtmxInternalApi} */ @@ -285,7 +286,7 @@ return (function () { } function aFullPageResponse(resp) { - return resp.match(/<body/); + return /<body/.test(resp) } /** @@ -459,7 +460,7 @@ return (function () { path = url.pathname + url.search; } // remove trailing slash, unless index page - if (!path.match('^/$')) { + if (!(/^\/$/.test(path))) { path = path.replace(/\/+$/, ''); } return path; @@ -836,7 +837,7 @@ return (function () { var oobSelects = getClosestAttributeValue(elt, "hx-select-oob"); if (oobSelects) { var oobSelectValues = oobSelects.split(","); - for (let i = 0; i < oobSelectValues.length; i++) { + for (var i = 0; i < oobSelectValues.length; i++) { var oobSelectValue = oobSelectValues[i].split(":", 2); var id = oobSelectValue[0].trim(); if (id.indexOf("#") === 0) { @@ -943,7 +944,7 @@ return (function () { function deInitOnHandlers(elt) { var internalData = getInternalData(elt); if (internalData.onHandlers) { - for (let i = 0; i < internalData.onHandlers.length; i++) { + for (var i = 0; i < internalData.onHandlers.length; i++) { const handlerInfo = internalData.onHandlers[i]; elt.removeEventListener(handlerInfo.event, handlerInfo.listener); } @@ -1155,6 +1156,8 @@ return (function () { var SYMBOL_CONT = /[_$a-zA-Z0-9]/; var STRINGISH_START = ['"', "'", "/"]; var NOT_WHITESPACE = /[^\s]/; + var COMBINED_SELECTOR_START = /[{(]/; + var COMBINED_SELECTOR_END = /[})]/; function tokenizeString(str) { var tokens = []; var position = 0; @@ -1237,12 +1240,24 @@ return (function () { function consumeUntil(tokens, match) { var result = ""; - while (tokens.length > 0 && !tokens[0].match(match)) { + while (tokens.length > 0 && !match.test(tokens[0])) { result += tokens.shift(); } return result; } + function consumeCSSSelector(tokens) { + var result; + if (tokens.length > 0 && COMBINED_SELECTOR_START.test(tokens[0])) { + tokens.shift(); + result = consumeUntil(tokens, COMBINED_SELECTOR_END).trim(); + tokens.shift(); + } else { + result = consumeUntil(tokens, WHITESPACE_OR_COMMA); + } + return result; + } + var INPUT_SELECTOR = 'input, textarea, select'; /** @@ -1291,29 +1306,33 @@ return (function () { triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA)); } else if (token === "from" && tokens[0] === ":") { tokens.shift(); - var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA); - if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") { - tokens.shift(); - var selector = consumeUntil( - tokens, - WHITESPACE_OR_COMMA - ) - // `next` and `previous` allow a selector-less syntax - if (selector.length > 0) { - from_arg += " " + selector; + if (COMBINED_SELECTOR_START.test(tokens[0])) { + var from_arg = consumeCSSSelector(tokens); + } else { + var from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA); + if (from_arg === "closest" || from_arg === "find" || from_arg === "next" || from_arg === "previous") { + tokens.shift(); + var selector = consumeCSSSelector(tokens); + // `next` and `previous` allow a selector-less syntax + if (selector.length > 0) { + from_arg += " " + selector; + } } } triggerSpec.from = from_arg; } else if (token === "target" && tokens[0] === ":") { tokens.shift(); - triggerSpec.target = consumeUntil(tokens, WHITESPACE_OR_COMMA); + triggerSpec.target = consumeCSSSelector(tokens); } else if (token === "throttle" && tokens[0] === ":") { tokens.shift(); triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA)); } else if (token === "queue" && tokens[0] === ":") { tokens.shift(); triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA); - } else if ((token === "root" || token === "threshold") && tokens[0] === ":") { + } else if (token === "root" && tokens[0] === ":") { + tokens.shift(); + triggerSpec[token] = consumeCSSSelector(tokens); + } else if (token === "threshold" && tokens[0] === ":") { tokens.shift(); triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA); } else { @@ -1885,26 +1904,35 @@ return (function () { }); } - function hasChanceOfBeingBoosted() { - return document.querySelector("[hx-boost], [data-hx-boost]"); + function shouldProcessHxOn(elt) { + var attributes = elt.attributes + for (var j = 0; j < attributes.length; j++) { + var attrName = attributes[j].name + if (startsWith(attrName, "hx-on:") || startsWith(attrName, "data-hx-on:") || + startsWith(attrName, "hx-on-") || startsWith(attrName, "data-hx-on-")) { + return true + } + } + return false } function findHxOnWildcardElements(elt) { var node = null var elements = [] + if (shouldProcessHxOn(elt)) { + elements.push(elt) + } + if (document.evaluate) { - var iter = document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]', elt) + var iter = document.evaluate('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' + + ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', elt) while (node = iter.iterateNext()) elements.push(node) } else { - var allElements = document.getElementsByTagName("*") + var allElements = elt.getElementsByTagName("*") for (var i = 0; i < allElements.length; i++) { - var attributes = allElements[i].attributes - for (var j = 0; j < attributes.length; j++) { - var attrName = attributes[j].name - if (startsWith(attrName, "hx-on:") || startsWith(attrName, "data-hx-on:")) { - elements.push(allElements[i]) - } + if (shouldProcessHxOn(allElements[i])) { + elements.push(allElements[i]) } } } @@ -1914,9 +1942,9 @@ return (function () { function findElementsToProcess(elt) { if (elt.querySelectorAll) { - var boostedElts = hasChanceOfBeingBoosted() ? ", a" : ""; - var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws]," + - " [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger], [hx-on], [data-hx-on]"); + var boostedSelector = ", [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]"; + var results = elt.querySelectorAll(VERB_SELECTOR + boostedSelector + ", form, [type='submit'], [hx-sse], [data-hx-sse], [hx-ws]," + + " [data-hx-ws], [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]"); return results; } else { return []; @@ -1961,7 +1989,7 @@ return (function () { function countCurlies(line) { var tokens = tokenizeString(line); var netCurlies = 0; - for (let i = 0; i < tokens.length; i++) { + for (var i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token === "{") { netCurlies++; @@ -1990,32 +2018,6 @@ return (function () { nodeData.onHandlers.push({event:eventName, listener:listener}); } - function processHxOn(elt) { - var hxOnValue = getAttributeValue(elt, 'hx-on'); - if (hxOnValue) { - var handlers = {} - var lines = hxOnValue.split("\n"); - var currentEvent = null; - var curlyCount = 0; - while (lines.length > 0) { - var line = lines.shift(); - var match = line.match(/^\s*([a-zA-Z:\-\.]+:)(.*)/); - if (curlyCount === 0 && match) { - line.split(":") - currentEvent = match[1].slice(0, -1); // strip last colon - handlers[currentEvent] = match[2]; - } else { - handlers[currentEvent] += line; - } - curlyCount += countCurlies(line); - } - - for (var eventName in handlers) { - addHxOnEventHandler(elt, eventName, handlers[eventName]); - } - } - } - function processHxOnWildcard(elt) { // wipe any previous on handlers so that this function takes precedence deInitOnHandlers(elt) @@ -2023,12 +2025,22 @@ return (function () { for (var i = 0; i < elt.attributes.length; i++) { var name = elt.attributes[i].name var value = elt.attributes[i].value - if (startsWith(name, "hx-on:") || startsWith(name, "data-hx-on:")) { - let eventName = name.slice(name.indexOf(":") + 1) - // if the eventName starts with a colon, prepend "htmx" for shorthand support - if (startsWith(eventName, ":")) eventName = "htmx" + eventName + if (startsWith(name, "hx-on") || startsWith(name, "data-hx-on")) { + var afterOnPosition = name.indexOf("-on") + 3; + var nextChar = name.slice(afterOnPosition, afterOnPosition + 1); + if (nextChar === "-" || nextChar === ":") { + var eventName = name.slice(afterOnPosition + 1); + // if the eventName starts with a colon or dash, prepend "htmx" for shorthand support + if (startsWith(eventName, ":")) { + eventName = "htmx" + eventName + } else if (startsWith(eventName, "-")) { + eventName = "htmx:" + eventName.slice(1); + } else if (startsWith(eventName, "htmx-")) { + eventName = "htmx:" + eventName.slice(5); + } - addHxOnEventHandler(elt, eventName, value) + addHxOnEventHandler(elt, eventName, value) + } } } } @@ -2045,8 +2057,6 @@ return (function () { nodeData.initHash = attributeHash(elt); - processHxOn(elt); - triggerEvent(elt, "htmx:beforeProcessNode") if (elt.value) { @@ -2095,8 +2105,6 @@ return (function () { } initNode(elt); forEach(findElementsToProcess(elt), function(child) { initNode(child) }); - // Because it happens second, the new way of adding onHandlers superseeds the old one - // i.e. if there are any hx-on:eventName attributes, the hx-on attribute will be ignored forEach(findHxOnWildcardElements(elt), processHxOnWildcard); } @@ -2306,7 +2314,9 @@ return (function () { var details = {path: path, xhr:request}; triggerEvent(getDocument().body, "htmx:historyCacheMiss", details); request.open('GET', path, true); + request.setRequestHeader("HX-Request", "true"); request.setRequestHeader("HX-History-Restore-Request", "true"); + request.setRequestHeader("HX-Current-URL", getDocument().location.href); request.onload = function () { if (this.status >= 200 && this.status < 400) { triggerEvent(getDocument().body, "htmx:historyCacheMissLoad", details); @@ -2897,7 +2907,7 @@ return (function () { } function hasHeader(xhr, regexp) { - return xhr.getAllResponseHeaders().match(regexp); + return regexp.test(xhr.getAllResponseHeaders()) } function ajaxHelper(verb, path, context) { @@ -2916,6 +2926,7 @@ return (function () { values : context.values, targetOverride: resolveTarget(context.target), swapOverride: context.swap, + select: context.select, returnPromise: true }); } @@ -2970,6 +2981,7 @@ return (function () { elt = getDocument().body; } var responseHandler = etc.handler || handleAjaxResponse; + var select = etc.select || null; if (!bodyContains(elt)) { // do not issue requests for elements removed from the DOM @@ -3118,6 +3130,11 @@ return (function () { var headers = getHeaders(elt, target, promptResponse); + + if (verb !== 'get' && !usesFormData(elt)) { + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + if (etc.headers) { headers = mergeObjects(headers, etc.headers); } @@ -3131,10 +3148,6 @@ return (function () { var allParameters = mergeObjects(rawParameters, expressionVars); var filteredParameters = filterValues(allParameters, elt); - if (verb !== 'get' && !usesFormData(elt)) { - headers['Content-Type'] = 'application/x-www-form-urlencoded'; - } - if (htmx.config.getCacheBusterParam && verb === 'get') { filteredParameters['org.htmx.cache-buster'] = getRawAttribute(target, "id") || "true"; } @@ -3232,7 +3245,7 @@ return (function () { } var responseInfo = { - xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted, + xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted, select: select, pathInfo: { requestPath: path, finalRequestPath: finalPath, @@ -3403,6 +3416,7 @@ return (function () { var target = responseInfo.target; var etc = responseInfo.etc; var requestConfig = responseInfo.requestConfig; + var select = responseInfo.select; if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return; @@ -3512,10 +3526,26 @@ return (function () { } var selectOverride; + if (select) { + selectOverride = select; + } + if (hasHeader(xhr, /HX-Reselect:/i)) { selectOverride = xhr.getResponseHeader("HX-Reselect"); } + // if we need to save history, do so, before swapping so that relative resources have the correct base URL + if (historyUpdate.type) { + triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo)); + if (historyUpdate.type === "push") { + pushUrlIntoHistory(historyUpdate.path); + triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path}); + } else { + replaceUrlInHistory(historyUpdate.path); + triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path}); + } + } + var settleInfo = makeSettleInfo(target); selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, selectOverride); @@ -3565,17 +3595,6 @@ return (function () { triggerEvent(elt, 'htmx:afterSettle', responseInfo); }); - // if we need to save history, do so - if (historyUpdate.type) { - triggerEvent(getDocument().body, 'htmx:beforeHistoryUpdate', mergeObjects({ history: historyUpdate }, responseInfo)); - if (historyUpdate.type === "push") { - pushUrlIntoHistory(historyUpdate.path); - triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: historyUpdate.path}); - } else { - replaceUrlInHistory(historyUpdate.path); - triggerEvent(getDocument().body, 'htmx:replacedInHistory', {path: historyUpdate.path}); - } - } if (responseInfo.pathInfo.anchor) { var anchorTarget = getDocument().getElementById(responseInfo.pathInfo.anchor); if(anchorTarget) { @@ -3734,25 +3753,34 @@ return (function () { //==================================================================== // Initialization //==================================================================== - var isReady = false - getDocument().addEventListener('DOMContentLoaded', function() { - isReady = true - }) - /** - * Execute a function now if DOMContentLoaded has fired, otherwise listen for it. - * - * This function uses isReady because there is no realiable way to ask the browswer whether - * the DOMContentLoaded event has already been fired; there's a gap between DOMContentLoaded - * firing and readystate=complete. + * We want to initialize the page elements after DOMContentLoaded + * fires, but there isn't always a good way to tell whether + * it has already fired when we get here or not. */ - function ready(fn) { - // Checking readyState here is a failsafe in case the htmx script tag entered the DOM by - // some means other than the initial page load. - if (isReady || getDocument().readyState === 'complete') { - fn(); - } else { - getDocument().addEventListener('DOMContentLoaded', fn); + function ready(functionToCall) { + // call the function exactly once no matter how many times this is called + var callReadyFunction = function() { + if (!functionToCall) return; + functionToCall(); + functionToCall = null; + }; + + if (getDocument().readyState === "complete") { + // DOMContentLoaded definitely fired, we can initialize the page + callReadyFunction(); + } + else { + /* DOMContentLoaded *maybe* already fired, wait for + * the next DOMContentLoaded or readystatechange event + */ + getDocument().addEventListener("DOMContentLoaded", function() { + callReadyFunction(); + }); + getDocument().addEventListener("readystatechange", function() { + if (getDocument().readyState !== "complete") return; + callReadyFunction(); + }); } } @@ -3760,9 +3788,9 @@ return (function () { if (htmx.config.includeIndicatorStyles !== false) { getDocument().head.insertAdjacentHTML("beforeend", "<style>\ - ." + htmx.config.indicatorClass + "{opacity:0;transition: opacity 200ms ease-in;}\ - ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1}\ - ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1}\ + ." + htmx.config.indicatorClass + "{opacity:0}\ + ." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\ + ." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1; transition: opacity 200ms ease-in;}\ </style>"); } } @@ -3800,7 +3828,9 @@ return (function () { internalData.xhr.abort(); } }); - var originalPopstate = window.onpopstate; + /** @type {(ev: PopStateEvent) => any} */ + const originalPopstate = window.onpopstate ? window.onpopstate.bind(window) : null; + /** @type {(ev: PopStateEvent) => any} */ window.onpopstate = function (event) { if (event.state && event.state.htmx) { restoreHistory(); |