// UMD insanity // This code sets up support for (in order) AMD, ES6 modules, and globals. (function (root, factory) { //@ts-ignore if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. //@ts-ignore define([], factory); } else if (typeof module === 'object' && module.exports) { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(); } else { // Browser globals root.htmx = root.htmx || factory(); } }(typeof self !== 'undefined' ? self : this, function () { return (function () { 'use strict'; // Public API //** @type {import("./htmx").HtmxApi} */ // TODO: list all methods in public API var htmx = { onLoad: onLoadHelper, process: processNode, on: addEventListenerImpl, off: removeEventListenerImpl, trigger : triggerEvent, ajax : ajaxHelper, find : find, findAll : findAll, closest : closest, values : function(elt, type){ var inputValues = getInputValues(elt, type || "post"); return inputValues.values; }, remove : removeElement, addClass : addClassToElement, removeClass : removeClassFromElement, toggleClass : toggleClassOnElement, takeClass : takeClassForElement, defineExtension : defineExtension, removeExtension : removeExtension, logAll : logAll, logNone : logNone, logger : null, config : { historyEnabled:true, historyCacheSize:10, refreshOnHistoryMiss:false, defaultSwapStyle:'innerHTML', defaultSwapDelay:0, defaultSettleDelay:20, includeIndicatorStyles:true, indicatorClass:'htmx-indicator', requestClass:'htmx-request', addedClass:'htmx-added', settlingClass:'htmx-settling', swappingClass:'htmx-swapping', allowEval:true, inlineScriptNonce:'', attributesToSettle:["class", "style", "width", "height"], withCredentials:false, timeout:0, wsReconnectDelay: 'full-jitter', wsBinaryType: 'blob', disableSelector: "[hx-disable], [data-hx-disable]", useTemplateFragments: false, scrollBehavior: 'smooth', defaultFocusScroll: false, getCacheBusterParam: false, globalViewTransitions: false, methodsThatUseUrlParams: ["get"], }, parseInterval:parseInterval, _:internalEval, createEventSource: function(url){ return new EventSource(url, {withCredentials:true}) }, createWebSocket: function(url){ var sock = new WebSocket(url, []); sock.binaryType = htmx.config.wsBinaryType; return sock; }, version: "1.9.3" }; /** @type {import("./htmx").HtmxInternalApi} */ var internalAPI = { addTriggerHandler: addTriggerHandler, bodyContains: bodyContains, canAccessLocalStorage: canAccessLocalStorage, findThisElement: findThisElement, filterValues: filterValues, hasAttribute: hasAttribute, getAttributeValue: getAttributeValue, getClosestAttributeValue: getClosestAttributeValue, getClosestMatch: getClosestMatch, getExpressionVars: getExpressionVars, getHeaders: getHeaders, getInputValues: getInputValues, getInternalData: getInternalData, getSwapSpecification: getSwapSpecification, getTriggerSpecs: getTriggerSpecs, getTarget: getTarget, makeFragment: makeFragment, mergeObjects: mergeObjects, makeSettleInfo: makeSettleInfo, oobSwap: oobSwap, querySelectorExt: querySelectorExt, selectAndSwap: selectAndSwap, settleImmediately: settleImmediately, shouldCancel: shouldCancel, triggerEvent: triggerEvent, triggerErrorEvent: triggerErrorEvent, withExtensions: withExtensions, } var VERBS = ['get', 'post', 'put', 'delete', 'patch']; var VERB_SELECTOR = VERBS.map(function(verb){ return "[hx-" + verb + "], [data-hx-" + verb + "]" }).join(", "); //==================================================================== // Utilities //==================================================================== function parseInterval(str) { if (str == undefined) { return undefined } if (str.slice(-2) == "ms") { return parseFloat(str.slice(0,-2)) || undefined } if (str.slice(-1) == "s") { return (parseFloat(str.slice(0,-1)) * 1000) || undefined } if (str.slice(-1) == "m") { return (parseFloat(str.slice(0,-1)) * 1000 * 60) || undefined } return parseFloat(str) || undefined } /** * @param {HTMLElement} elt * @param {string} name * @returns {(string | null)} */ function getRawAttribute(elt, name) { return elt.getAttribute && elt.getAttribute(name); } // resolve with both hx and data-hx prefixes function hasAttribute(elt, qualifiedName) { return elt.hasAttribute && (elt.hasAttribute(qualifiedName) || elt.hasAttribute("data-" + qualifiedName)); } /** * * @param {HTMLElement} elt * @param {string} qualifiedName * @returns {(string | null)} */ function getAttributeValue(elt, qualifiedName) { return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName); } /** * @param {HTMLElement} elt * @returns {HTMLElement | null} */ function parentElt(elt) { return elt.parentElement; } /** * @returns {Document} */ function getDocument() { return document; } /** * @param {HTMLElement} elt * @param {(e:HTMLElement) => boolean} condition * @returns {HTMLElement | null} */ function getClosestMatch(elt, condition) { while (elt && !condition(elt)) { elt = parentElt(elt); } return elt ? elt : null; } function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName){ var attributeValue = getAttributeValue(ancestor, attributeName); var disinherit = getAttributeValue(ancestor, "hx-disinherit"); if (initialElement !== ancestor && disinherit && (disinherit === "*" || disinherit.split(" ").indexOf(attributeName) >= 0)) { return "unset"; } else { return attributeValue } } /** * @param {HTMLElement} elt * @param {string} attributeName * @returns {string | null} */ function getClosestAttributeValue(elt, attributeName) { var closestAttr = null; getClosestMatch(elt, function (e) { return closestAttr = getAttributeValueWithDisinheritance(elt, e, attributeName); }); if (closestAttr !== "unset") { return closestAttr; } } /** * @param {HTMLElement} elt * @param {string} selector * @returns {boolean} */ function matches(elt, selector) { // @ts-ignore: non-standard properties for browser compatability // noinspection JSUnresolvedVariable var matchesFunction = elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector; return matchesFunction && matchesFunction.call(elt, selector); } /** * @param {string} str * @returns {string} */ function getStartTag(str) { var tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i var match = tagMatcher.exec( str ); if (match) { return match[1].toLowerCase(); } else { return ""; } } /** * * @param {string} resp * @param {number} depth * @returns {Element} */ function parseHTML(resp, depth) { var parser = new DOMParser(); var responseDoc = parser.parseFromString(resp, "text/html"); /** @type {Element} */ var responseNode = responseDoc.body; while (depth > 0) { depth--; // @ts-ignore responseNode = responseNode.firstChild; } if (responseNode == null) { // @ts-ignore responseNode = getDocument().createDocumentFragment(); } return responseNode; } function aFullPageResponse(resp) { return resp.match(/", 0); // @ts-ignore type mismatch between DocumentFragment and Element. // TODO: Are these close enough for htmx to use interchangably? return documentFragment.querySelector('template').content; } else { var startTag = getStartTag(resp); switch (startTag) { case "thead": case "tbody": case "tfoot": case "colgroup": case "caption": return parseHTML("" + resp + "
", 1); case "col": return parseHTML("" + resp + "
", 2); case "tr": return parseHTML("" + resp + "
", 2); case "td": case "th": return parseHTML("" + resp + "
", 3); case "script": return parseHTML("
" + resp + "
", 1); default: return parseHTML(resp, 0); } } } /** * @param {Function} func */ function maybeCall(func){ if(func) { func(); } } /** * @param {any} o * @param {string} type * @returns */ function isType(o, type) { return Object.prototype.toString.call(o) === "[object " + type + "]"; } /** * @param {*} o * @returns {o is Function} */ function isFunction(o) { return isType(o, "Function"); } /** * @param {*} o * @returns {o is Object} */ function isRawObject(o) { return isType(o, "Object"); } /** * getInternalData retrieves "private" data stored by htmx within an element * @param {HTMLElement} elt * @returns {*} */ function getInternalData(elt) { var dataProp = 'htmx-internal-data'; var data = elt[dataProp]; if (!data) { data = elt[dataProp] = {}; } return data; } /** * toArray converts an ArrayLike object into a real array. * @param {ArrayLike} arr * @returns {any[]} */ function toArray(arr) { var returnArr = []; if (arr) { for (var i = 0; i < arr.length; i++) { returnArr.push(arr[i]); } } return returnArr } function forEach(arr, func) { if (arr) { for (var i = 0; i < arr.length; i++) { func(arr[i]); } } } function isScrolledIntoView(el) { var rect = el.getBoundingClientRect(); var elemTop = rect.top; var elemBottom = rect.bottom; return elemTop < window.innerHeight && elemBottom >= 0; } function bodyContains(elt) { // IE Fix if (elt.getRootNode && elt.getRootNode() instanceof ShadowRoot) { return getDocument().body.contains(elt.getRootNode().host); } else { return getDocument().body.contains(elt); } } function splitOnWhitespace(trigger) { return trigger.trim().split(/\s+/); } /** * mergeObjects takes all of the keys from * obj2 and duplicates them into obj1 * @param {Object} obj1 * @param {Object} obj2 * @returns {Object} */ function mergeObjects(obj1, obj2) { for (var key in obj2) { if (obj2.hasOwnProperty(key)) { obj1[key] = obj2[key]; } } return obj1; } function parseJSON(jString) { try { return JSON.parse(jString); } catch(error) { logError(error); return null; } } function canAccessLocalStorage() { var test = 'htmx:localStorageTest'; try { localStorage.setItem(test, test); localStorage.removeItem(test); return true; } catch(e) { return false; } } function normalizePath(path) { try { var url = new URL(path); if (url) { path = url.pathname + url.search; } // remove trailing slash, unless index page if (!path.match('^/$')) { path = path.replace(/\/+$/, ''); } return path; } catch (e) { // be kind to IE11, which doesn't support URL() return path; } } //========================================================================================== // public API //========================================================================================== function internalEval(str){ return maybeEval(getDocument().body, function () { return eval(str); }); } function onLoadHelper(callback) { var value = htmx.on("htmx:load", function(evt) { callback(evt.detail.elt); }); return value; } function logAll(){ htmx.logger = function(elt, event, data) { if(console) { console.log(event, elt, data); } } } function logNone() { htmx.logger = null } function find(eltOrSelector, selector) { if (selector) { return eltOrSelector.querySelector(selector); } else { return find(getDocument(), eltOrSelector); } } function findAll(eltOrSelector, selector) { if (selector) { return eltOrSelector.querySelectorAll(selector); } else { return findAll(getDocument(), eltOrSelector); } } function removeElement(elt, delay) { elt = resolveTarget(elt); if (delay) { setTimeout(function(){ removeElement(elt); elt = null; }, delay); } else { elt.parentElement.removeChild(elt); } } function addClassToElement(elt, clazz, delay) { elt = resolveTarget(elt); if (delay) { setTimeout(function(){ addClassToElement(elt, clazz); elt = null; }, delay); } else { elt.classList && elt.classList.add(clazz); } } function removeClassFromElement(elt, clazz, delay) { elt = resolveTarget(elt); if (delay) { setTimeout(function(){ removeClassFromElement(elt, clazz); elt = null; }, delay); } else { if (elt.classList) { elt.classList.remove(clazz); // if there are no classes left, remove the class attribute if (elt.classList.length === 0) { elt.removeAttribute("class"); } } } } function toggleClassOnElement(elt, clazz) { elt = resolveTarget(elt); elt.classList.toggle(clazz); } function takeClassForElement(elt, clazz) { elt = resolveTarget(elt); forEach(elt.parentElement.children, function(child){ removeClassFromElement(child, clazz); }) addClassToElement(elt, clazz); } function closest(elt, selector) { elt = resolveTarget(elt); if (elt.closest) { return elt.closest(selector); } else { // TODO remove when IE goes away do{ if (elt == null || matches(elt, selector)){ return elt; } } while (elt = elt && parentElt(elt)); return null; } } function normalizeSelector(selector) { var trimmedSelector = selector.trim(); if (trimmedSelector.startsWith("<") && trimmedSelector.endsWith("/>")) { return trimmedSelector.substring(1, trimmedSelector.length - 2); } else { return trimmedSelector; } } function querySelectorAllExt(elt, selector) { if (selector.indexOf("closest ") === 0) { return [closest(elt, normalizeSelector(selector.substr(8)))]; } else if (selector.indexOf("find ") === 0) { return [find(elt, normalizeSelector(selector.substr(5)))]; } else if (selector.indexOf("next ") === 0) { return [scanForwardQuery(elt, normalizeSelector(selector.substr(5)))]; } else if (selector.indexOf("previous ") === 0) { return [scanBackwardsQuery(elt, normalizeSelector(selector.substr(9)))]; } else if (selector === 'document') { return [document]; } else if (selector === 'window') { return [window]; } else { return getDocument().querySelectorAll(normalizeSelector(selector)); } } var scanForwardQuery = function(start, match) { var results = getDocument().querySelectorAll(match); for (var i = 0; i < results.length; i++) { var elt = results[i]; if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_PRECEDING) { return elt; } } } var scanBackwardsQuery = function(start, match) { var results = getDocument().querySelectorAll(match); for (var i = results.length - 1; i >= 0; i--) { var elt = results[i]; if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_FOLLOWING) { return elt; } } } function querySelectorExt(eltOrSelector, selector) { if (selector) { return querySelectorAllExt(eltOrSelector, selector)[0]; } else { return querySelectorAllExt(getDocument().body, eltOrSelector)[0]; } } function resolveTarget(arg2) { if (isType(arg2, 'String')) { return find(arg2); } else { return arg2; } } function processEventArgs(arg1, arg2, arg3) { if (isFunction(arg2)) { return { target: getDocument().body, event: arg1, listener: arg2 } } else { return { target: resolveTarget(arg1), event: arg2, listener: arg3 } } } function addEventListenerImpl(arg1, arg2, arg3) { ready(function(){ var eventArgs = processEventArgs(arg1, arg2, arg3); eventArgs.target.addEventListener(eventArgs.event, eventArgs.listener); }) var b = isFunction(arg2); return b ? arg2 : arg3; } function removeEventListenerImpl(arg1, arg2, arg3) { ready(function(){ var eventArgs = processEventArgs(arg1, arg2, arg3); eventArgs.target.removeEventListener(eventArgs.event, eventArgs.listener); }) return isFunction(arg2) ? arg2 : arg3; } //==================================================================== // Node processing //==================================================================== var DUMMY_ELT = getDocument().createElement("output"); // dummy element for bad selectors function findAttributeTargets(elt, attrName) { var attrTarget = getClosestAttributeValue(elt, attrName); if (attrTarget) { if (attrTarget === "this") { return [findThisElement(elt, attrName)]; } else { var result = querySelectorAllExt(elt, attrTarget); if (result.length === 0) { logError('The selector "' + attrTarget + '" on ' + attrName + " returned no matches!"); return [DUMMY_ELT] } else { return result; } } } } function findThisElement(elt, attribute){ return getClosestMatch(elt, function (elt) { return getAttributeValue(elt, attribute) != null; }) } function getTarget(elt) { var targetStr = getClosestAttributeValue(elt, "hx-target"); if (targetStr) { if (targetStr === "this") { return findThisElement(elt,'hx-target'); } else { return querySelectorExt(elt, targetStr) } } else { var data = getInternalData(elt); if (data.boosted) { return getDocument().body; } else { return elt; } } } function shouldSettleAttribute(name) { var attributesToSettle = htmx.config.attributesToSettle; for (var i = 0; i < attributesToSettle.length; i++) { if (name === attributesToSettle[i]) { return true; } } return false; } function cloneAttributes(mergeTo, mergeFrom) { forEach(mergeTo.attributes, function (attr) { if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) { mergeTo.removeAttribute(attr.name) } }); forEach(mergeFrom.attributes, function (attr) { if (shouldSettleAttribute(attr.name)) { mergeTo.setAttribute(attr.name, attr.value); } }); } function isInlineSwap(swapStyle, target) { var extensions = getExtensions(target); for (var i = 0; i < extensions.length; i++) { var extension = extensions[i]; try { if (extension.isInlineSwap(swapStyle)) { return true; } } catch(e) { logError(e); } } return swapStyle === "outerHTML"; } /** * * @param {string} oobValue * @param {HTMLElement} oobElement * @param {*} settleInfo * @returns */ function oobSwap(oobValue, oobElement, settleInfo) { var selector = "#" + oobElement.id; var swapStyle = "outerHTML"; if (oobValue === "true") { // do nothing } else if (oobValue.indexOf(":") > 0) { swapStyle = oobValue.substr(0, oobValue.indexOf(":")); selector = oobValue.substr(oobValue.indexOf(":") + 1, oobValue.length); } else { swapStyle = oobValue; } var targets = getDocument().querySelectorAll(selector); if (targets) { forEach( targets, function (target) { var fragment; var oobElementClone = oobElement.cloneNode(true); fragment = getDocument().createDocumentFragment(); fragment.appendChild(oobElementClone); if (!isInlineSwap(swapStyle, target)) { fragment = oobElementClone; // if this is not an inline swap, we use the content of the node, not the node itself } var beforeSwapDetails = {shouldSwap: true, target: target, fragment:fragment }; if (!triggerEvent(target, 'htmx:oobBeforeSwap', beforeSwapDetails)) return; target = beforeSwapDetails.target; // allow re-targeting if (beforeSwapDetails['shouldSwap']){ swap(swapStyle, target, target, fragment, settleInfo); } forEach(settleInfo.elts, function (elt) { triggerEvent(elt, 'htmx:oobAfterSwap', beforeSwapDetails); }); } ); oobElement.parentNode.removeChild(oobElement); } else { oobElement.parentNode.removeChild(oobElement); triggerErrorEvent(getDocument().body, "htmx:oobErrorNoTarget", {content: oobElement}); } return oobValue; } function handleOutOfBandSwaps(elt, fragment, settleInfo) { var oobSelects = getClosestAttributeValue(elt, "hx-select-oob"); if (oobSelects) { var oobSelectValues = oobSelects.split(","); for (let i = 0; i < oobSelectValues.length; i++) { var oobSelectValue = oobSelectValues[i].split(":", 2); var id = oobSelectValue[0].trim(); if (id.indexOf("#") === 0) { id = id.substring(1); } var oobValue = oobSelectValue[1] || "true"; var oobElement = fragment.querySelector("#" + id); if (oobElement) { oobSwap(oobValue, oobElement, settleInfo); } } } forEach(findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]'), function (oobElement) { var oobValue = getAttributeValue(oobElement, "hx-swap-oob"); if (oobValue != null) { oobSwap(oobValue, oobElement, settleInfo); } }); } function handlePreservedElements(fragment) { forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function (preservedElt) { var id = getAttributeValue(preservedElt, "id"); var oldElt = getDocument().getElementById(id); if (oldElt != null) { preservedElt.parentNode.replaceChild(oldElt, preservedElt); } }); } function handleAttributes(parentNode, fragment, settleInfo) { forEach(fragment.querySelectorAll("[id]"), function (newNode) { if (newNode.id && newNode.id.length > 0) { var normalizedId = newNode.id.replace("'", "\\'"); var normalizedTag = newNode.tagName.replace(':', '\\:'); var oldNode = parentNode.querySelector(normalizedTag + "[id='" + normalizedId + "']"); if (oldNode && oldNode !== parentNode) { var newAttributes = newNode.cloneNode(); cloneAttributes(newNode, oldNode); settleInfo.tasks.push(function () { cloneAttributes(newNode, newAttributes); }); } } }); } function makeAjaxLoadTask(child) { return function () { removeClassFromElement(child, htmx.config.addedClass); processNode(child); processScripts(child); processFocus(child) triggerEvent(child, 'htmx:load'); }; } function processFocus(child) { var autofocus = "[autofocus]"; var autoFocusedElt = matches(child, autofocus) ? child : child.querySelector(autofocus) if (autoFocusedElt != null) { autoFocusedElt.focus(); } } function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) { handleAttributes(parentNode, fragment, settleInfo); while(fragment.childNodes.length > 0){ var child = fragment.firstChild; addClassToElement(child, htmx.config.addedClass); parentNode.insertBefore(child, insertBefore); if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) { settleInfo.tasks.push(makeAjaxLoadTask(child)); } } } // based on https://gist.github.com/hyamamoto/fd435505d29ebfa3d9716fd2be8d42f0, // derived from Java's string hashcode implementation function stringHash(string, hash) { var char = 0; while (char < string.length){ hash = (hash << 5) - hash + string.charCodeAt(char++) | 0; // bitwise or ensures we have a 32-bit int } return hash; } function attributeHash(elt) { var hash = 0; // IE fix if (elt.attributes) { for (var i = 0; i < elt.attributes.length; i++) { var attribute = elt.attributes[i]; if(attribute.value){ // only include attributes w/ actual values (empty is same as non-existent) hash = stringHash(attribute.name, hash); hash = stringHash(attribute.value, hash); } } } return hash; } function deInitOnHandlers(elt) { var internalData = getInternalData(elt); if (internalData.onHandlers) { for (let i = 0; i < internalData.onHandlers.length; i++) { const handlerInfo = internalData.onHandlers[i]; elt.removeEventListener(handlerInfo.name, handlerInfo.handler); } delete internalData.onHandlers } } function deInitNode(element) { var internalData = getInternalData(element); if (internalData.timeout) { clearTimeout(internalData.timeout); } if (internalData.webSocket) { internalData.webSocket.close(); } if (internalData.sseEventSource) { internalData.sseEventSource.close(); } if (internalData.listenerInfos) { forEach(internalData.listenerInfos, function (info) { if (info.on) { info.on.removeEventListener(info.trigger, info.listener); } }); } deInitOnHandlers(element); } function cleanUpElement(element) { triggerEvent(element, "htmx:beforeCleanupElement") deInitNode(element); if (element.children) { // IE forEach(element.children, function(child) { cleanUpElement(child) }); } } function swapOuterHTML(target, fragment, settleInfo) { if (target.tagName === "BODY") { return swapInnerHTML(target, fragment, settleInfo); } else { // @type {HTMLElement} var newElt var eltBeforeNewContent = target.previousSibling; insertNodesBefore(parentElt(target), target, fragment, settleInfo); if (eltBeforeNewContent == null) { newElt = parentElt(target).firstChild; } else { newElt = eltBeforeNewContent.nextSibling; } getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later settleInfo.elts = settleInfo.elts.filter(e => e != target); while(newElt && newElt !== target) { if (newElt.nodeType === Node.ELEMENT_NODE) { settleInfo.elts.push(newElt); } newElt = newElt.nextElementSibling; } cleanUpElement(target); parentElt(target).removeChild(target); } } function swapAfterBegin(target, fragment, settleInfo) { return insertNodesBefore(target, target.firstChild, fragment, settleInfo); } function swapBeforeBegin(target, fragment, settleInfo) { return insertNodesBefore(parentElt(target), target, fragment, settleInfo); } function swapBeforeEnd(target, fragment, settleInfo) { return insertNodesBefore(target, null, fragment, settleInfo); } function swapAfterEnd(target, fragment, settleInfo) { return insertNodesBefore(parentElt(target), target.nextSibling, fragment, settleInfo); } function swapDelete(target, fragment, settleInfo) { cleanUpElement(target); return parentElt(target).removeChild(target); } function swapInnerHTML(target, fragment, settleInfo) { var firstChild = target.firstChild; insertNodesBefore(target, firstChild, fragment, settleInfo); if (firstChild) { while (firstChild.nextSibling) { cleanUpElement(firstChild.nextSibling) target.removeChild(firstChild.nextSibling); } cleanUpElement(firstChild) target.removeChild(firstChild); } } function maybeSelectFromResponse(elt, fragment, selectOverride) { var selector = selectOverride || getClosestAttributeValue(elt, "hx-select"); if (selector) { var newFragment = getDocument().createDocumentFragment(); forEach(fragment.querySelectorAll(selector), function (node) { newFragment.appendChild(node); }); fragment = newFragment; } return fragment; } function swap(swapStyle, elt, target, fragment, settleInfo) { switch (swapStyle) { case "none": return; case "outerHTML": swapOuterHTML(target, fragment, settleInfo); return; case "afterbegin": swapAfterBegin(target, fragment, settleInfo); return; case "beforebegin": swapBeforeBegin(target, fragment, settleInfo); return; case "beforeend": swapBeforeEnd(target, fragment, settleInfo); return; case "afterend": swapAfterEnd(target, fragment, settleInfo); return; case "delete": swapDelete(target, fragment, settleInfo); return; default: var extensions = getExtensions(elt); for (var i = 0; i < extensions.length; i++) { var ext = extensions[i]; try { var newElements = ext.handleSwap(swapStyle, target, fragment, settleInfo); if (newElements) { if (typeof newElements.length !== 'undefined') { // if handleSwap returns an array (like) of elements, we handle them for (var j = 0; j < newElements.length; j++) { var child = newElements[j]; if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) { settleInfo.tasks.push(makeAjaxLoadTask(child)); } } } return; } } catch (e) { logError(e); } } if (swapStyle === "innerHTML") { swapInnerHTML(target, fragment, settleInfo); } else { swap(htmx.config.defaultSwapStyle, elt, target, fragment, settleInfo); } } } function findTitle(content) { if (content.indexOf(' -1) { var contentWithSvgsRemoved = content.replace(/]*>|>)([\s\S]*?)<\/svg>/gim, ''); var result = contentWithSvgsRemoved.match(/]*>|>)([\s\S]*?)<\/title>/im); if (result) { return result[2]; } } } function selectAndSwap(swapStyle, target, elt, responseText, settleInfo, selectOverride) { settleInfo.title = findTitle(responseText); var fragment = makeFragment(responseText); if (fragment) { handleOutOfBandSwaps(elt, fragment, settleInfo); fragment = maybeSelectFromResponse(elt, fragment, selectOverride); handlePreservedElements(fragment); return swap(swapStyle, elt, target, fragment, settleInfo); } } function handleTrigger(xhr, header, elt) { var triggerBody = xhr.getResponseHeader(header); if (triggerBody.indexOf("{") === 0) { var triggers = parseJSON(triggerBody); for (var eventName in triggers) { if (triggers.hasOwnProperty(eventName)) { var detail = triggers[eventName]; if (!isRawObject(detail)) { detail = {"value": detail} } triggerEvent(elt, eventName, detail); } } } else { triggerEvent(elt, triggerBody, []); } } var WHITESPACE = /\s/; var WHITESPACE_OR_COMMA = /[\s,]/; var SYMBOL_START = /[_$a-zA-Z]/; var SYMBOL_CONT = /[_$a-zA-Z0-9]/; var STRINGISH_START = ['"', "'", "/"]; var NOT_WHITESPACE = /[^\s]/; function tokenizeString(str) { var tokens = []; var position = 0; while (position < str.length) { if(SYMBOL_START.exec(str.charAt(position))) { var startPosition = position; while (SYMBOL_CONT.exec(str.charAt(position + 1))) { position++; } tokens.push(str.substr(startPosition, position - startPosition + 1)); } else if (STRINGISH_START.indexOf(str.charAt(position)) !== -1) { var startChar = str.charAt(position); var startPosition = position; position++; while (position < str.length && str.charAt(position) !== startChar ) { if (str.charAt(position) === "\\") { position++; } position++; } tokens.push(str.substr(startPosition, position - startPosition + 1)); } else { var symbol = str.charAt(position); tokens.push(symbol); } position++; } return tokens; } function isPossibleRelativeReference(token, last, paramName) { return SYMBOL_START.exec(token.charAt(0)) && token !== "true" && token !== "false" && token !== "this" && token !== paramName && last !== "."; } function maybeGenerateConditional(elt, tokens, paramName) { if (tokens[0] === '[') { tokens.shift(); var bracketCount = 1; var conditionalSource = " return (function(" + paramName + "){ return ("; var last = null; while (tokens.length > 0) { var token = tokens[0]; if (token === "]") { bracketCount--; if (bracketCount === 0) { if (last === null) { conditionalSource = conditionalSource + "true"; } tokens.shift(); conditionalSource += ")})"; try { var conditionFunction = maybeEval(elt,function () { return Function(conditionalSource)(); }, function(){return true}) conditionFunction.source = conditionalSource; return conditionFunction; } catch (e) { triggerErrorEvent(getDocument().body, "htmx:syntax:error", {error:e, source:conditionalSource}) return null; } } } else if (token === "[") { bracketCount++; } if (isPossibleRelativeReference(token, last, paramName)) { conditionalSource += "((" + paramName + "." + token + ") ? (" + paramName + "." + token + ") : (window." + token + "))"; } else { conditionalSource = conditionalSource + token; } last = tokens.shift(); } } } function consumeUntil(tokens, match) { var result = ""; while (tokens.length > 0 && !tokens[0].match(match)) { result += tokens.shift(); } return result; } var INPUT_SELECTOR = 'input, textarea, select'; /** * @param {HTMLElement} elt * @returns {import("./htmx").HtmxTriggerSpecification[]} */ function getTriggerSpecs(elt) { var explicitTrigger = getAttributeValue(elt, 'hx-trigger'); var triggerSpecs = []; if (explicitTrigger) { var tokens = tokenizeString(explicitTrigger); do { consumeUntil(tokens, NOT_WHITESPACE); var initialLength = tokens.length; var trigger = consumeUntil(tokens, /[,\[\s]/); if (trigger !== "") { if (trigger === "every") { var every = {trigger: 'every'}; consumeUntil(tokens, NOT_WHITESPACE); every.pollInterval = parseInterval(consumeUntil(tokens, /[,\[\s]/)); consumeUntil(tokens, NOT_WHITESPACE); var eventFilter = maybeGenerateConditional(elt, tokens, "event"); if (eventFilter) { every.eventFilter = eventFilter; } triggerSpecs.push(every); } else if (trigger.indexOf("sse:") === 0) { triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)}); } else { var triggerSpec = {trigger: trigger}; var eventFilter = maybeGenerateConditional(elt, tokens, "event"); if (eventFilter) { triggerSpec.eventFilter = eventFilter; } while (tokens.length > 0 && tokens[0] !== ",") { consumeUntil(tokens, NOT_WHITESPACE) var token = tokens.shift(); if (token === "changed") { triggerSpec.changed = true; } else if (token === "once") { triggerSpec.once = true; } else if (token === "consume") { triggerSpec.consume = true; } else if (token === "delay" && tokens[0] === ":") { tokens.shift(); 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(); from_arg += " " + consumeUntil( tokens, WHITESPACE_OR_COMMA ); } triggerSpec.from = from_arg; } else if (token === "target" && tokens[0] === ":") { tokens.shift(); triggerSpec.target = consumeUntil(tokens, WHITESPACE_OR_COMMA); } 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] === ":") { tokens.shift(); triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA); } else { triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()}); } } triggerSpecs.push(triggerSpec); } } if (tokens.length === initialLength) { triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()}); } consumeUntil(tokens, NOT_WHITESPACE); } while (tokens[0] === "," && tokens.shift()) } if (triggerSpecs.length > 0) { return triggerSpecs; } else if (matches(elt, 'form')) { return [{trigger: 'submit'}]; } else if (matches(elt, 'input[type="button"]')){ return [{trigger: 'click'}]; } else if (matches(elt, INPUT_SELECTOR)) { return [{trigger: 'change'}]; } else { return [{trigger: 'click'}]; } } function cancelPolling(elt) { getInternalData(elt).cancelled = true; } function processPolling(elt, handler, spec) { var nodeData = getInternalData(elt); nodeData.timeout = setTimeout(function () { if (bodyContains(elt) && nodeData.cancelled !== true) { if (!maybeFilterEvent(spec, elt, makeEvent('hx:poll:trigger', { triggerSpec: spec, target: elt }))) { handler(elt); } processPolling(elt, handler, spec); } }, spec.pollInterval); } function isLocalLink(elt) { return location.hostname === elt.hostname && getRawAttribute(elt,'href') && getRawAttribute(elt,'href').indexOf("#") !== 0; } function boostElement(elt, nodeData, triggerSpecs) { if ((elt.tagName === "A" && isLocalLink(elt) && (elt.target === "" || elt.target === "_self")) || elt.tagName === "FORM") { nodeData.boosted = true; var verb, path; if (elt.tagName === "A") { verb = "get"; path = elt.href; // DOM property gives the fully resolved href of a relative link } else { var rawAttribute = getRawAttribute(elt, "method"); verb = rawAttribute ? rawAttribute.toLowerCase() : "get"; if (verb === "get") { } path = getRawAttribute(elt, 'action'); } triggerSpecs.forEach(function(triggerSpec) { addEventListener(elt, function(elt, evt) { issueAjaxRequest(verb, path, elt, evt) }, nodeData, triggerSpec, true); }); } } /** * * @param {Event} evt * @param {HTMLElement} elt * @returns */ function shouldCancel(evt, elt) { if (evt.type === "submit" || evt.type === "click") { if (elt.tagName === "FORM") { return true; } if (matches(elt, 'input[type="submit"], button') && closest(elt, 'form') !== null) { return true; } if (elt.tagName === "A" && elt.href && (elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf("#") !== 0)) { return true; } } return false; } function ignoreBoostedAnchorCtrlClick(elt, evt) { return getInternalData(elt).boosted && elt.tagName === "A" && evt.type === "click" && (evt.ctrlKey || evt.metaKey); } function maybeFilterEvent(triggerSpec, elt, evt) { var eventFilter = triggerSpec.eventFilter; if(eventFilter){ try { return eventFilter.call(elt, evt) !== true; } catch(e) { triggerErrorEvent(getDocument().body, "htmx:eventFilter:error", {error: e, source:eventFilter.source}); return true; } } return false; } function addEventListener(elt, handler, nodeData, triggerSpec, explicitCancel) { var elementData = getInternalData(elt); var eltsToListenOn; if (triggerSpec.from) { eltsToListenOn = querySelectorAllExt(elt, triggerSpec.from); } else { eltsToListenOn = [elt]; } // store the initial value of the element so we can tell if it changes if (triggerSpec.changed) { elementData.lastValue = elt.value; } forEach(eltsToListenOn, function (eltToListenOn) { var eventListener = function (evt) { if (!bodyContains(elt)) { eltToListenOn.removeEventListener(triggerSpec.trigger, eventListener); return; } if (ignoreBoostedAnchorCtrlClick(elt, evt)) { return; } if (explicitCancel || shouldCancel(evt, elt)) { evt.preventDefault(); } if (maybeFilterEvent(triggerSpec, elt, evt)) { return; } var eventData = getInternalData(evt); eventData.triggerSpec = triggerSpec; if (eventData.handledFor == null) { eventData.handledFor = []; } if (eventData.handledFor.indexOf(elt) < 0) { eventData.handledFor.push(elt); if (triggerSpec.consume) { evt.stopPropagation(); } if (triggerSpec.target && evt.target) { if (!matches(evt.target, triggerSpec.target)) { return; } } if (triggerSpec.once) { if (elementData.triggeredOnce) { return; } else { elementData.triggeredOnce = true; } } if (triggerSpec.changed) { if (elementData.lastValue === elt.value) { return; } else { elementData.lastValue = elt.value; } } if (elementData.delayed) { clearTimeout(elementData.delayed); } if (elementData.throttle) { return; } if (triggerSpec.throttle) { if (!elementData.throttle) { handler(elt, evt); elementData.throttle = setTimeout(function () { elementData.throttle = null; }, triggerSpec.throttle); } } else if (triggerSpec.delay) { elementData.delayed = setTimeout(function() { handler(elt, evt) }, triggerSpec.delay); } else { triggerEvent(elt, 'htmx:trigger') handler(elt, evt); } } }; if (nodeData.listenerInfos == null) { nodeData.listenerInfos = []; } nodeData.listenerInfos.push({ trigger: triggerSpec.trigger, listener: eventListener, on: eltToListenOn }) eltToListenOn.addEventListener(triggerSpec.trigger, eventListener); }); } var windowIsScrolling = false // used by initScrollHandler var scrollHandler = null; function initScrollHandler() { if (!scrollHandler) { scrollHandler = function() { windowIsScrolling = true }; window.addEventListener("scroll", scrollHandler) setInterval(function() { if (windowIsScrolling) { windowIsScrolling = false; forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) { maybeReveal(elt); }) } }, 200); } } function maybeReveal(elt) { if (!hasAttribute(elt,'data-hx-revealed') && isScrolledIntoView(elt)) { elt.setAttribute('data-hx-revealed', 'true'); var nodeData = getInternalData(elt); if (nodeData.initHash) { triggerEvent(elt, 'revealed'); } else { // if the node isn't initialized, wait for it before triggering the request elt.addEventListener("htmx:afterProcessNode", function(evt) { triggerEvent(elt, 'revealed') }, {once: true}); } } } //==================================================================== // Web Sockets //==================================================================== function processWebSocketInfo(elt, nodeData, info) { var values = splitOnWhitespace(info); for (var i = 0; i < values.length; i++) { var value = values[i].split(/:(.+)/); if (value[0] === "connect") { ensureWebSocket(elt, value[1], 0); } if (value[0] === "send") { processWebSocketSend(elt); } } } function ensureWebSocket(elt, wssSource, retryCount) { if (!bodyContains(elt)) { return; // stop ensuring websocket connection when socket bearing element ceases to exist } if (wssSource.indexOf("/") == 0) { // complete absolute paths only var base_part = location.hostname + (location.port ? ':'+location.port: ''); if (location.protocol == 'https:') { wssSource = "wss://" + base_part + wssSource; } else if (location.protocol == 'http:') { wssSource = "ws://" + base_part + wssSource; } } var socket = htmx.createWebSocket(wssSource); socket.onerror = function (e) { triggerErrorEvent(elt, "htmx:wsError", {error:e, socket:socket}); maybeCloseWebSocketSource(elt); }; socket.onclose = function (e) { if ([1006, 1012, 1013].indexOf(e.code) >= 0) { // Abnormal Closure/Service Restart/Try Again Later var delay = getWebSocketReconnectDelay(retryCount); setTimeout(function() { ensureWebSocket(elt, wssSource, retryCount+1); // creates a websocket with a new timeout }, delay); } }; socket.onopen = function (e) { retryCount = 0; } getInternalData(elt).webSocket = socket; socket.addEventListener('message', function (event) { if (maybeCloseWebSocketSource(elt)) { return; } var response = event.data; withExtensions(elt, function(extension){ response = extension.transformResponse(response, null, elt); }); var settleInfo = makeSettleInfo(elt); var fragment = makeFragment(response); var children = toArray(fragment.children); for (var i = 0; i < children.length; i++) { var child = children[i]; oobSwap(getAttributeValue(child, "hx-swap-oob") || "true", child, settleInfo); } settleImmediately(settleInfo.tasks); }); } function maybeCloseWebSocketSource(elt) { if (!bodyContains(elt)) { getInternalData(elt).webSocket.close(); return true; } } function processWebSocketSend(elt) { var webSocketSourceElt = getClosestMatch(elt, function (parent) { return getInternalData(parent).webSocket != null; }); if (webSocketSourceElt) { elt.addEventListener(getTriggerSpecs(elt)[0].trigger, function (evt) { var webSocket = getInternalData(webSocketSourceElt).webSocket; var headers = getHeaders(elt, webSocketSourceElt); var results = getInputValues(elt, 'post'); var errors = results.errors; var rawParameters = results.values; var expressionVars = getExpressionVars(elt); var allParameters = mergeObjects(rawParameters, expressionVars); var filteredParameters = filterValues(allParameters, elt); filteredParameters['HEADERS'] = headers; if (errors && errors.length > 0) { triggerEvent(elt, 'htmx:validation:halted', errors); return; } webSocket.send(JSON.stringify(filteredParameters)); if(shouldCancel(evt, elt)){ evt.preventDefault(); } }); } else { triggerErrorEvent(elt, "htmx:noWebSocketSourceError"); } } function getWebSocketReconnectDelay(retryCount) { var delay = htmx.config.wsReconnectDelay; if (typeof delay === 'function') { // @ts-ignore return delay(retryCount); } if (delay === 'full-jitter') { var exp = Math.min(retryCount, 6); var maxDelay = 1000 * Math.pow(2, exp); return maxDelay * Math.random(); } logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"'); } //==================================================================== // Server Sent Events //==================================================================== function processSSEInfo(elt, nodeData, info) { var values = splitOnWhitespace(info); for (var i = 0; i < values.length; i++) { var value = values[i].split(/:(.+)/); if (value[0] === "connect") { processSSESource(elt, value[1]); } if ((value[0] === "swap")) { processSSESwap(elt, value[1]) } } } function processSSESource(elt, sseSrc) { var source = htmx.createEventSource(sseSrc); source.onerror = function (e) { triggerErrorEvent(elt, "htmx:sseError", {error:e, source:source}); maybeCloseSSESource(elt); }; getInternalData(elt).sseEventSource = source; } function processSSESwap(elt, sseEventName) { var sseSourceElt = getClosestMatch(elt, hasEventSource); if (sseSourceElt) { var sseEventSource = getInternalData(sseSourceElt).sseEventSource; var sseListener = function (event) { if (maybeCloseSSESource(sseSourceElt)) { return; } if (!bodyContains(elt)) { sseEventSource.removeEventListener(sseEventName, sseListener); return; } /////////////////////////// // TODO: merge this code with AJAX and WebSockets code in the future. var response = event.data; withExtensions(elt, function(extension){ response = extension.transformResponse(response, null, elt); }); var swapSpec = getSwapSpecification(elt) var target = getTarget(elt) var settleInfo = makeSettleInfo(elt); selectAndSwap(swapSpec.swapStyle, target, elt, response, settleInfo) settleImmediately(settleInfo.tasks) triggerEvent(elt, "htmx:sseMessage", event) }; getInternalData(elt).sseListener = sseListener; sseEventSource.addEventListener(sseEventName, sseListener); } else { triggerErrorEvent(elt, "htmx:noSSESourceError"); } } function processSSETrigger(elt, handler, sseEventName) { var sseSourceElt = getClosestMatch(elt, hasEventSource); if (sseSourceElt) { var sseEventSource = getInternalData(sseSourceElt).sseEventSource; var sseListener = function () { if (!maybeCloseSSESource(sseSourceElt)) { if (bodyContains(elt)) { handler(elt); } else { sseEventSource.removeEventListener(sseEventName, sseListener); } } }; getInternalData(elt).sseListener = sseListener; sseEventSource.addEventListener(sseEventName, sseListener); } else { triggerErrorEvent(elt, "htmx:noSSESourceError"); } } function maybeCloseSSESource(elt) { if (!bodyContains(elt)) { getInternalData(elt).sseEventSource.close(); return true; } } function hasEventSource(node) { return getInternalData(node).sseEventSource != null; } //==================================================================== function loadImmediately(elt, handler, nodeData, delay) { var load = function(){ if (!nodeData.loaded) { nodeData.loaded = true; handler(elt); } } if (delay) { setTimeout(load, delay); } else { load(); } } function processVerbs(elt, nodeData, triggerSpecs) { var explicitAction = false; forEach(VERBS, function (verb) { if (hasAttribute(elt,'hx-' + verb)) { var path = getAttributeValue(elt, 'hx-' + verb); explicitAction = true; nodeData.path = path; nodeData.verb = verb; triggerSpecs.forEach(function(triggerSpec) { addTriggerHandler(elt, triggerSpec, nodeData, function (elt, evt) { issueAjaxRequest(verb, path, elt, evt) }) }); } }); return explicitAction; } function addTriggerHandler(elt, triggerSpec, nodeData, handler) { if (triggerSpec.sseEvent) { processSSETrigger(elt, handler, triggerSpec.sseEvent); } else if (triggerSpec.trigger === "revealed") { initScrollHandler(); addEventListener(elt, handler, nodeData, triggerSpec); maybeReveal(elt); } else if (triggerSpec.trigger === "intersect") { var observerOptions = {}; if (triggerSpec.root) { observerOptions.root = querySelectorExt(elt, triggerSpec.root) } if (triggerSpec.threshold) { observerOptions.threshold = parseFloat(triggerSpec.threshold); } var observer = new IntersectionObserver(function (entries) { for (var i = 0; i < entries.length; i++) { var entry = entries[i]; if (entry.isIntersecting) { triggerEvent(elt, "intersect"); break; } } }, observerOptions); observer.observe(elt); addEventListener(elt, handler, nodeData, triggerSpec); } else if (triggerSpec.trigger === "load") { if (!maybeFilterEvent(triggerSpec, elt, makeEvent("load", {elt: elt}))) { loadImmediately(elt, handler, nodeData, triggerSpec.delay); } } else if (triggerSpec.pollInterval) { nodeData.polling = true; processPolling(elt, handler, triggerSpec); } else { addEventListener(elt, handler, nodeData, triggerSpec); } } function evalScript(script) { if (script.type === "text/javascript" || script.type === "module" || script.type === "") { var newScript = getDocument().createElement("script"); forEach(script.attributes, function (attr) { newScript.setAttribute(attr.name, attr.value); }); newScript.textContent = script.textContent; newScript.async = false; if (htmx.config.inlineScriptNonce) { newScript.nonce = htmx.config.inlineScriptNonce; } var parent = script.parentElement; try { parent.insertBefore(newScript, script); } catch (e) { logError(e); } finally { // remove old script element, but only if it is still in DOM if (script.parentElement) { script.parentElement.removeChild(script); } } } } function processScripts(elt) { if (matches(elt, "script")) { evalScript(elt); } forEach(findAll(elt, "script"), function (script) { evalScript(script); }); } function hasChanceOfBeingBoosted() { return document.querySelector("[hx-boost], [data-hx-boost]"); } function findHxOnWildcardElements(elt) { if (!document.evaluate) return [] let node = null const elements = [] const iter = document.evaluate('//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") ]]', elt) while (node = iter.iterateNext()) elements.push(node) return elements } function findElementsToProcess(elt) { if (elt.querySelectorAll) { var boostedElts = hasChanceOfBeingBoosted() ? ", a, form" : ""; var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", [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]"); return results; } else { return []; } } function initButtonTracking(form){ var maybeSetLastButtonClicked = function(evt){ var elt = closest(evt.target, "button, input[type='submit']"); if (elt !== null) { var internalData = getInternalData(form); internalData.lastButtonClicked = elt; } }; // need to handle both click and focus in: // focusin - in case someone tabs in to a button and hits the space bar // click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724 form.addEventListener('click', maybeSetLastButtonClicked) form.addEventListener('focusin', maybeSetLastButtonClicked) form.addEventListener('focusout', function(evt){ var internalData = getInternalData(form); internalData.lastButtonClicked = null; }) } function countCurlies(line) { var tokens = tokenizeString(line); var netCurlies = 0; for (let i = 0; i < tokens.length; i++) { const token = tokens[i]; if (token === "{") { netCurlies++; } else if (token === "}") { netCurlies--; } } return netCurlies; } function addHxOnEventHandler(elt, eventName, code) { var nodeData = getInternalData(elt); nodeData.onHandlers = []; var func = new Function("event", code + "; return;"); var listener = elt.addEventListener(eventName, function (e) { return func.call(elt, e); }); nodeData.onHandlers.push({event:eventName, listener:listener}); return {nodeData, code, func, listener}; } function processHxOn(elt) { var hxOnValue = getAttributeValue(elt, 'hx-on'); if (hxOnValue && htmx.config.allowEval) { 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) for (const attr of elt.attributes) { const { name, value } = attr if (name.startsWith("hx-on:") || name.startsWith("data-hx-on:")) { let eventName = name.slice(name.indexOf(":") + 1) // if the eventName starts with a colon, prepend "htmx" for shorthand support if (eventName.startsWith(":")) eventName = "htmx" + eventName addHxOnEventHandler(elt, eventName, value) } } } function initNode(elt) { if (elt.closest && elt.closest(htmx.config.disableSelector)) { return; } var nodeData = getInternalData(elt); if (nodeData.initHash !== attributeHash(elt)) { nodeData.initHash = attributeHash(elt); // clean up any previously processed info deInitNode(elt); processHxOn(elt); triggerEvent(elt, "htmx:beforeProcessNode") if (elt.value) { nodeData.lastValue = elt.value; } var triggerSpecs = getTriggerSpecs(elt); var hasExplicitHttpAction = processVerbs(elt, nodeData, triggerSpecs); if (!hasExplicitHttpAction) { if (getClosestAttributeValue(elt, "hx-boost") === "true") { boostElement(elt, nodeData, triggerSpecs); } else if (hasAttribute(elt, 'hx-trigger')) { triggerSpecs.forEach(function (triggerSpec) { // For "naked" triggers, don't do anything at all addTriggerHandler(elt, triggerSpec, nodeData, function () { }) }) } } if (elt.tagName === "FORM") { initButtonTracking(elt); } var sseInfo = getAttributeValue(elt, 'hx-sse'); if (sseInfo) { processSSEInfo(elt, nodeData, sseInfo); } var wsInfo = getAttributeValue(elt, 'hx-ws'); if (wsInfo) { processWebSocketInfo(elt, nodeData, wsInfo); } triggerEvent(elt, "htmx:afterProcessNode"); } } function processNode(elt) { elt = resolveTarget(elt); 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); } //==================================================================== // Event/Log Support //==================================================================== function kebabEventName(str) { return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); } function makeEvent(eventName, detail) { var evt; if (window.CustomEvent && typeof window.CustomEvent === 'function') { evt = new CustomEvent(eventName, {bubbles: true, cancelable: true, detail: detail}); } else { evt = getDocument().createEvent('CustomEvent'); evt.initCustomEvent(eventName, true, true, detail); } return evt; } function triggerErrorEvent(elt, eventName, detail) { triggerEvent(elt, eventName, mergeObjects({error:eventName}, detail)); } function ignoreEventForLogging(eventName) { return eventName === "htmx:afterProcessNode" } /** * `withExtensions` locates all active extensions for a provided element, then * executes the provided function using each of the active extensions. It should * be called internally at every extendable execution point in htmx. * * @param {HTMLElement} elt * @param {(extension:import("./htmx").HtmxExtension) => void} toDo * @returns void */ function withExtensions(elt, toDo) { forEach(getExtensions(elt), function(extension){ try { toDo(extension); } catch (e) { logError(e); } }); } function logError(msg) { if(console.error) { console.error(msg); } else if (console.log) { console.log("ERROR: ", msg); } } function triggerEvent(elt, eventName, detail) { elt = resolveTarget(elt); if (detail == null) { detail = {}; } detail["elt"] = elt; var event = makeEvent(eventName, detail); if (htmx.logger && !ignoreEventForLogging(eventName)) { htmx.logger(elt, eventName, detail); } if (detail.error) { logError(detail.error); triggerEvent(elt, "htmx:error", {errorInfo:detail}) } var eventResult = elt.dispatchEvent(event); var kebabName = kebabEventName(eventName); if (eventResult && kebabName !== eventName) { var kebabedEvent = makeEvent(kebabName, event.detail); eventResult = eventResult && elt.dispatchEvent(kebabedEvent) } withExtensions(elt, function (extension) { eventResult = eventResult && (extension.onEvent(eventName, event) !== false) }); return eventResult; } //==================================================================== // History Support //==================================================================== var currentPathForHistory = location.pathname+location.search; function getHistoryElement() { var historyElt = getDocument().querySelector('[hx-history-elt],[data-hx-history-elt]'); return historyElt || getDocument().body; } function saveToHistoryCache(url, content, title, scroll) { if (!canAccessLocalStorage()) { return; } url = normalizePath(url); var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || []; for (var i = 0; i < historyCache.length; i++) { if (historyCache[i].url === url) { historyCache.splice(i, 1); break; } } var newHistoryItem = {url:url, content: content, title:title, scroll:scroll}; triggerEvent(getDocument().body, "htmx:historyItemCreated", {item:newHistoryItem, cache: historyCache}) historyCache.push(newHistoryItem) while (historyCache.length > htmx.config.historyCacheSize) { historyCache.shift(); } while(historyCache.length > 0){ try { localStorage.setItem("htmx-history-cache", JSON.stringify(historyCache)); break; } catch (e) { triggerErrorEvent(getDocument().body, "htmx:historyCacheError", {cause:e, cache: historyCache}) historyCache.shift(); // shrink the cache and retry } } } function getCachedHistory(url) { if (!canAccessLocalStorage()) { return null; } url = normalizePath(url); var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || []; for (var i = 0; i < historyCache.length; i++) { if (historyCache[i].url === url) { return historyCache[i]; } } return null; } function cleanInnerHtmlForHistory(elt) { var className = htmx.config.requestClass; var clone = elt.cloneNode(true); forEach(findAll(clone, "." + className), function(child){ removeClassFromElement(child, className); }); return clone.innerHTML; } function saveCurrentPageToHistory() { var elt = getHistoryElement(); var path = currentPathForHistory || location.pathname+location.search; // Allow history snapshot feature to be disabled where hx-history="false" // is present *anywhere* in the current document we're about to save, // so we can prevent privileged data entering the cache. // The page will still be reachable as a history entry, but htmx will fetch it // live from the server onpopstate rather than look in the localStorage cache var disableHistoryCache = getDocument().querySelector('[hx-history="false" i],[data-hx-history="false" i]'); if (!disableHistoryCache) { triggerEvent(getDocument().body, "htmx:beforeHistorySave", {path: path, historyElt: elt}); saveToHistoryCache(path, cleanInnerHtmlForHistory(elt), getDocument().title, window.scrollY); } if (htmx.config.historyEnabled) history.replaceState({htmx: true}, getDocument().title, window.location.href); } function pushUrlIntoHistory(path) { // remove the cache buster parameter, if any if (htmx.config.getCacheBusterParam) { path = path.replace(/org\.htmx\.cache-buster=[^&]*&?/, '') if (path.endsWith('&') || path.endsWith("?")) { path = path.slice(0, -1); } } if(htmx.config.historyEnabled) { history.pushState({htmx:true}, "", path); } currentPathForHistory = path; } function replaceUrlInHistory(path) { if(htmx.config.historyEnabled) history.replaceState({htmx:true}, "", path); currentPathForHistory = path; } function settleImmediately(tasks) { forEach(tasks, function (task) { task.call(); }); } function loadHistoryFromServer(path) { var request = new XMLHttpRequest(); var details = {path: path, xhr:request}; triggerEvent(getDocument().body, "htmx:historyCacheMiss", details); request.open('GET', path, true); request.setRequestHeader("HX-History-Restore-Request", "true"); request.onload = function () { if (this.status >= 200 && this.status < 400) { triggerEvent(getDocument().body, "htmx:historyCacheMissLoad", details); var fragment = makeFragment(this.response); // @ts-ignore fragment = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment; var historyElement = getHistoryElement(); var settleInfo = makeSettleInfo(historyElement); var title = findTitle(this.response); if (title) { var titleElt = find("title"); if (titleElt) { titleElt.innerHTML = title; } else { window.document.title = title; } } // @ts-ignore swapInnerHTML(historyElement, fragment, settleInfo) settleImmediately(settleInfo.tasks); currentPathForHistory = path; triggerEvent(getDocument().body, "htmx:historyRestore", {path: path, cacheMiss:true, serverResponse:this.response}); } else { triggerErrorEvent(getDocument().body, "htmx:historyCacheMissLoadError", details); } }; request.send(); } function restoreHistory(path) { saveCurrentPageToHistory(); path = path || location.pathname+location.search; var cached = getCachedHistory(path); if (cached) { var fragment = makeFragment(cached.content); var historyElement = getHistoryElement(); var settleInfo = makeSettleInfo(historyElement); swapInnerHTML(historyElement, fragment, settleInfo) settleImmediately(settleInfo.tasks); document.title = cached.title; setTimeout(function () { window.scrollTo(0, cached.scroll); }, 0); // next 'tick', so browser has time to render layout currentPathForHistory = path; triggerEvent(getDocument().body, "htmx:historyRestore", {path:path, item:cached}); } else { if (htmx.config.refreshOnHistoryMiss) { // @ts-ignore: optional parameter in reload() function throws error window.location.reload(true); } else { loadHistoryFromServer(path); } } } function addRequestIndicatorClasses(elt) { var indicators = findAttributeTargets(elt, 'hx-indicator'); if (indicators == null) { indicators = [elt]; } forEach(indicators, function (ic) { var internalData = getInternalData(ic); internalData.requestCount = (internalData.requestCount || 0) + 1; ic.classList["add"].call(ic.classList, htmx.config.requestClass); }); return indicators; } function removeRequestIndicatorClasses(indicators) { forEach(indicators, function (ic) { var internalData = getInternalData(ic); internalData.requestCount = (internalData.requestCount || 0) - 1; if (internalData.requestCount === 0) { ic.classList["remove"].call(ic.classList, htmx.config.requestClass); } }); } //==================================================================== // Input Value Processing //==================================================================== function haveSeenNode(processed, elt) { for (var i = 0; i < processed.length; i++) { var node = processed[i]; if (node.isSameNode(elt)) { return true; } } return false; } function shouldInclude(elt) { if(elt.name === "" || elt.name == null || elt.disabled) { return false; } // ignore "submitter" types (see jQuery src/serialize.js) if (elt.type === "button" || elt.type === "submit" || elt.tagName === "image" || elt.tagName === "reset" || elt.tagName === "file" ) { return false; } if (elt.type === "checkbox" || elt.type === "radio" ) { return elt.checked; } return true; } function processInputValue(processed, values, errors, elt, validate) { if (elt == null || haveSeenNode(processed, elt)) { return; } else { processed.push(elt); } if (shouldInclude(elt)) { var name = getRawAttribute(elt,"name"); var value = elt.value; if (elt.multiple) { value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value }); } // include file inputs if (elt.files) { value = toArray(elt.files); } // This is a little ugly because both the current value of the named value in the form // and the new value could be arrays, so we have to handle all four cases :/ if (name != null && value != null) { var current = values[name]; if (current !== undefined) { if (Array.isArray(current)) { if (Array.isArray(value)) { values[name] = current.concat(value); } else { current.push(value); } } else { if (Array.isArray(value)) { values[name] = [current].concat(value); } else { values[name] = [current, value]; } } } else { values[name] = value; } } if (validate) { validateElement(elt, errors); } } if (matches(elt, 'form')) { var inputs = elt.elements; forEach(inputs, function(input) { processInputValue(processed, values, errors, input, validate); }); } } function validateElement(element, errors) { if (element.willValidate) { triggerEvent(element, "htmx:validation:validate") if (!element.checkValidity()) { errors.push({elt: element, message:element.validationMessage, validity:element.validity}); triggerEvent(element, "htmx:validation:failed", {message:element.validationMessage, validity:element.validity}) } } } /** * @param {HTMLElement} elt * @param {string} verb */ function getInputValues(elt, verb) { var processed = []; var values = {}; var formValues = {}; var errors = []; var internalData = getInternalData(elt); // only validate when form is directly submitted and novalidate or formnovalidate are not set // or if the element has an explicit hx-validate="true" on it var validate = (matches(elt, 'form') && elt.noValidate !== true) || getAttributeValue(elt, "hx-validate") === "true"; if (internalData.lastButtonClicked) { validate = validate && internalData.lastButtonClicked.formNoValidate !== true; } // for a non-GET include the closest form if (verb !== 'get') { processInputValue(processed, formValues, errors, closest(elt, 'form'), validate); } // include the element itself processInputValue(processed, values, errors, elt, validate); // if a button or submit was clicked last, include its value if (internalData.lastButtonClicked) { var name = getRawAttribute(internalData.lastButtonClicked,"name"); if (name) { values[name] = internalData.lastButtonClicked.value; } } // include any explicit includes var includes = findAttributeTargets(elt, "hx-include"); forEach(includes, function(node) { processInputValue(processed, values, errors, node, validate); // if a non-form is included, include any input values within it if (!matches(node, 'form')) { forEach(node.querySelectorAll(INPUT_SELECTOR), function (descendant) { processInputValue(processed, values, errors, descendant, validate); }) } }); // form values take precedence, overriding the regular values values = mergeObjects(values, formValues); return {errors:errors, values:values}; } function appendParam(returnStr, name, realValue) { if (returnStr !== "") { returnStr += "&"; } if (String(realValue) === "[object Object]") { realValue = JSON.stringify(realValue); } var s = encodeURIComponent(realValue); returnStr += encodeURIComponent(name) + "=" + s; return returnStr; } function urlEncode(values) { var returnStr = ""; for (var name in values) { if (values.hasOwnProperty(name)) { var value = values[name]; if (Array.isArray(value)) { forEach(value, function(v) { returnStr = appendParam(returnStr, name, v); }); } else { returnStr = appendParam(returnStr, name, value); } } } return returnStr; } function makeFormData(values) { var formData = new FormData(); for (var name in values) { if (values.hasOwnProperty(name)) { var value = values[name]; if (Array.isArray(value)) { forEach(value, function(v) { formData.append(name, v); }); } else { formData.append(name, value); } } } return formData; } //==================================================================== // Ajax //==================================================================== /** * @param {HTMLElement} elt * @param {HTMLElement} target * @param {string} prompt * @returns {Object} // TODO: Define/Improve HtmxHeaderSpecification */ function getHeaders(elt, target, prompt) { var headers = { "HX-Request" : "true", "HX-Trigger" : getRawAttribute(elt, "id"), "HX-Trigger-Name" : getRawAttribute(elt, "name"), "HX-Target" : getAttributeValue(target, "id"), "HX-Current-URL" : getDocument().location.href, } getValuesForElement(elt, "hx-headers", false, headers) if (prompt !== undefined) { headers["HX-Prompt"] = prompt; } if (getInternalData(elt).boosted) { headers["HX-Boosted"] = "true"; } return headers; } /** * filterValues takes an object containing form input values * and returns a new object that only contains keys that are * specified by the closest "hx-params" attribute * @param {Object} inputValues * @param {HTMLElement} elt * @returns {Object} */ function filterValues(inputValues, elt) { var paramsValue = getClosestAttributeValue(elt, "hx-params"); if (paramsValue) { if (paramsValue === "none") { return {}; } else if (paramsValue === "*") { return inputValues; } else if(paramsValue.indexOf("not ") === 0) { forEach(paramsValue.substr(4).split(","), function (name) { name = name.trim(); delete inputValues[name]; }); return inputValues; } else { var newValues = {} forEach(paramsValue.split(","), function (name) { name = name.trim(); newValues[name] = inputValues[name]; }); return newValues; } } else { return inputValues; } } function isAnchorLink(elt) { return getRawAttribute(elt, 'href') && getRawAttribute(elt, 'href').indexOf("#") >=0 } /** * * @param {HTMLElement} elt * @param {string} swapInfoOverride * @returns {import("./htmx").HtmxSwapSpecification} */ function getSwapSpecification(elt, swapInfoOverride) { var swapInfo = swapInfoOverride ? swapInfoOverride : getClosestAttributeValue(elt, "hx-swap"); var swapSpec = { "swapStyle" : getInternalData(elt).boosted ? 'innerHTML' : htmx.config.defaultSwapStyle, "swapDelay" : htmx.config.defaultSwapDelay, "settleDelay" : htmx.config.defaultSettleDelay } if (getInternalData(elt).boosted && !isAnchorLink(elt)) { swapSpec["show"] = "top" } if (swapInfo) { var split = splitOnWhitespace(swapInfo); if (split.length > 0) { swapSpec["swapStyle"] = split[0]; for (var i = 1; i < split.length; i++) { var modifier = split[i]; if (modifier.indexOf("swap:") === 0) { swapSpec["swapDelay"] = parseInterval(modifier.substr(5)); } if (modifier.indexOf("settle:") === 0) { swapSpec["settleDelay"] = parseInterval(modifier.substr(7)); } if (modifier.indexOf("transition:") === 0) { swapSpec["transition"] = modifier.substr(11) === "true"; } if (modifier.indexOf("scroll:") === 0) { var scrollSpec = modifier.substr(7); var splitSpec = scrollSpec.split(":"); var scrollVal = splitSpec.pop(); var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null; swapSpec["scroll"] = scrollVal; swapSpec["scrollTarget"] = selectorVal; } if (modifier.indexOf("show:") === 0) { var showSpec = modifier.substr(5); var splitSpec = showSpec.split(":"); var showVal = splitSpec.pop(); var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null; swapSpec["show"] = showVal; swapSpec["showTarget"] = selectorVal; } if (modifier.indexOf("focus-scroll:") === 0) { var focusScrollVal = modifier.substr("focus-scroll:".length); swapSpec["focusScroll"] = focusScrollVal == "true"; } } } } return swapSpec; } function usesFormData(elt) { return getClosestAttributeValue(elt, "hx-encoding") === "multipart/form-data" || (matches(elt, "form") && getRawAttribute(elt, 'enctype') === "multipart/form-data"); } function encodeParamsForBody(xhr, elt, filteredParameters) { var encodedParameters = null; withExtensions(elt, function (extension) { if (encodedParameters == null) { encodedParameters = extension.encodeParameters(xhr, filteredParameters, elt); } }); if (encodedParameters != null) { return encodedParameters; } else { if (usesFormData(elt)) { return makeFormData(filteredParameters); } else { return urlEncode(filteredParameters); } } } /** * * @param {Element} target * @returns {import("./htmx").HtmxSettleInfo} */ function makeSettleInfo(target) { return {tasks: [], elts: [target]}; } function updateScrollState(content, swapSpec) { var first = content[0]; var last = content[content.length - 1]; if (swapSpec.scroll) { var target = null; if (swapSpec.scrollTarget) { target = querySelectorExt(first, swapSpec.scrollTarget); } if (swapSpec.scroll === "top" && (first || target)) { target = target || first; target.scrollTop = 0; } if (swapSpec.scroll === "bottom" && (last || target)) { target = target || last; target.scrollTop = target.scrollHeight; } } if (swapSpec.show) { var target = null; if (swapSpec.showTarget) { var targetStr = swapSpec.showTarget; if (swapSpec.showTarget === "window") { targetStr = "body"; } target = querySelectorExt(first, targetStr); } if (swapSpec.show === "top" && (first || target)) { target = target || first; target.scrollIntoView({block:'start', behavior: htmx.config.scrollBehavior}); } if (swapSpec.show === "bottom" && (last || target)) { target = target || last; target.scrollIntoView({block:'end', behavior: htmx.config.scrollBehavior}); } } } /** * @param {HTMLElement} elt * @param {string} attr * @param {boolean=} evalAsDefault * @param {Object=} values * @returns {Object} */ function getValuesForElement(elt, attr, evalAsDefault, values) { if (values == null) { values = {}; } if (elt == null) { return values; } var attributeValue = getAttributeValue(elt, attr); if (attributeValue) { var str = attributeValue.trim(); var evaluateValue = evalAsDefault; if (str === "unset") { return null; } if (str.indexOf("javascript:") === 0) { str = str.substr(11); evaluateValue = true; } else if (str.indexOf("js:") === 0) { str = str.substr(3); evaluateValue = true; } if (str.indexOf('{') !== 0) { str = "{" + str + "}"; } var varsValues; if (evaluateValue) { varsValues = maybeEval(elt,function () {return Function("return (" + str + ")")();}, {}); } else { varsValues = parseJSON(str); } for (var key in varsValues) { if (varsValues.hasOwnProperty(key)) { if (values[key] == null) { values[key] = varsValues[key]; } } } } return getValuesForElement(parentElt(elt), attr, evalAsDefault, values); } function maybeEval(elt, toEval, defaultVal) { if (htmx.config.allowEval) { return toEval(); } else { triggerErrorEvent(elt, 'htmx:evalDisallowedError'); return defaultVal; } } /** * @param {HTMLElement} elt * @param {*} expressionVars * @returns */ function getHXVarsForElement(elt, expressionVars) { return getValuesForElement(elt, "hx-vars", true, expressionVars); } /** * @param {HTMLElement} elt * @param {*} expressionVars * @returns */ function getHXValsForElement(elt, expressionVars) { return getValuesForElement(elt, "hx-vals", false, expressionVars); } /** * @param {HTMLElement} elt * @returns {Object} */ function getExpressionVars(elt) { return mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt)); } function safelySetHeaderValue(xhr, header, headerValue) { if (headerValue !== null) { try { xhr.setRequestHeader(header, headerValue); } catch (e) { // On an exception, try to set the header URI encoded instead xhr.setRequestHeader(header, encodeURIComponent(headerValue)); xhr.setRequestHeader(header + "-URI-AutoEncoded", "true"); } } } function getPathFromResponse(xhr) { // NB: IE11 does not support this stuff if (xhr.responseURL && typeof(URL) !== "undefined") { try { var url = new URL(xhr.responseURL); return url.pathname + url.search; } catch (e) { triggerErrorEvent(getDocument().body, "htmx:badResponseUrl", {url: xhr.responseURL}); } } } function hasHeader(xhr, regexp) { return xhr.getAllResponseHeaders().match(regexp); } function ajaxHelper(verb, path, context) { verb = verb.toLowerCase(); if (context) { if (context instanceof Element || isType(context, 'String')) { return issueAjaxRequest(verb, path, null, null, { targetOverride: resolveTarget(context), returnPromise: true }); } else { return issueAjaxRequest(verb, path, resolveTarget(context.source), context.event, { handler : context.handler, headers : context.headers, values : context.values, targetOverride: resolveTarget(context.target), swapOverride: context.swap, returnPromise: true }); } } else { return issueAjaxRequest(verb, path, null, null, { returnPromise: true }); } } function hierarchyForElt(elt) { var arr = []; while (elt) { arr.push(elt); elt = elt.parentElement; } return arr; } function issueAjaxRequest(verb, path, elt, event, etc, confirmed) { var resolve = null; var reject = null; etc = etc != null ? etc : {}; if(etc.returnPromise && typeof Promise !== "undefined"){ var promise = new Promise(function (_resolve, _reject) { resolve = _resolve; reject = _reject; }); } if(elt == null) { elt = getDocument().body; } var responseHandler = etc.handler || handleAjaxResponse; if (!bodyContains(elt)) { return; // do not issue requests for elements removed from the DOM } var target = etc.targetOverride || getTarget(elt); if (target == null || target == DUMMY_ELT) { triggerErrorEvent(elt, 'htmx:targetError', {target: getAttributeValue(elt, "hx-target")}); return; } // allow event-based confirmation w/ a callback if (!confirmed) { var issueRequest = function() { return issueAjaxRequest(verb, path, elt, event, etc, true); } var confirmDetails = {target: target, elt: elt, path: path, verb: verb, triggeringEvent: event, etc: etc, issueRequest: issueRequest}; if (triggerEvent(elt, 'htmx:confirm', confirmDetails) === false) { return; } } var syncElt = elt; var eltData = getInternalData(elt); var syncStrategy = getClosestAttributeValue(elt, "hx-sync"); var queueStrategy = null; var abortable = false; if (syncStrategy) { var syncStrings = syncStrategy.split(":"); var selector = syncStrings[0].trim(); if (selector === "this") { syncElt = findThisElement(elt, 'hx-sync'); } else { syncElt = querySelectorExt(elt, selector); } // default to the drop strategy syncStrategy = (syncStrings[1] || 'drop').trim(); eltData = getInternalData(syncElt); if (syncStrategy === "drop" && eltData.xhr && eltData.abortable !== true) { return; } else if (syncStrategy === "abort") { if (eltData.xhr) { return; } else { abortable = true; } } else if (syncStrategy === "replace") { triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue } else if (syncStrategy.indexOf("queue") === 0) { var queueStrArray = syncStrategy.split(" "); queueStrategy = (queueStrArray[1] || "last").trim(); } } if (eltData.xhr) { if (eltData.abortable) { triggerEvent(syncElt, 'htmx:abort'); // abort the current request and continue } else { if(queueStrategy == null){ if (event) { var eventData = getInternalData(event); if (eventData && eventData.triggerSpec && eventData.triggerSpec.queue) { queueStrategy = eventData.triggerSpec.queue; } } if (queueStrategy == null) { queueStrategy = "last"; } } if (eltData.queuedRequests == null) { eltData.queuedRequests = []; } if (queueStrategy === "first" && eltData.queuedRequests.length === 0) { eltData.queuedRequests.push(function () { issueAjaxRequest(verb, path, elt, event, etc) }); } else if (queueStrategy === "all") { eltData.queuedRequests.push(function () { issueAjaxRequest(verb, path, elt, event, etc) }); } else if (queueStrategy === "last") { eltData.queuedRequests = []; // dump existing queue eltData.queuedRequests.push(function () { issueAjaxRequest(verb, path, elt, event, etc) }); } return; } } var xhr = new XMLHttpRequest(); eltData.xhr = xhr; eltData.abortable = abortable; var endRequestLock = function(){ eltData.xhr = null; eltData.abortable = false; if (eltData.queuedRequests != null && eltData.queuedRequests.length > 0) { var queuedRequest = eltData.queuedRequests.shift(); queuedRequest(); } } var promptQuestion = getClosestAttributeValue(elt, "hx-prompt"); if (promptQuestion) { var promptResponse = prompt(promptQuestion); // prompt returns null if cancelled and empty string if accepted with no entry if (promptResponse === null || !triggerEvent(elt, 'htmx:prompt', {prompt: promptResponse, target:target})) { maybeCall(resolve); endRequestLock(); return promise; } } var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm"); if (confirmQuestion) { if(!confirm(confirmQuestion)) { maybeCall(resolve); endRequestLock() return promise; } } var headers = getHeaders(elt, target, promptResponse); if (etc.headers) { headers = mergeObjects(headers, etc.headers); } var results = getInputValues(elt, verb); var errors = results.errors; var rawParameters = results.values; if (etc.values) { rawParameters = mergeObjects(rawParameters, etc.values); } var expressionVars = getExpressionVars(elt); 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"; } // behavior of anchors w/ empty href is to use the current URL if (path == null || path === "") { path = getDocument().location.href; } var requestAttrValues = getValuesForElement(elt, 'hx-request'); var eltIsBoosted = getInternalData(elt).boosted; var useUrlParams = htmx.config.methodsThatUseUrlParams.indexOf(verb) >= 0 var requestConfig = { boosted: eltIsBoosted, useUrlParams: useUrlParams, parameters: filteredParameters, unfilteredParameters: allParameters, headers:headers, target:target, verb:verb, errors:errors, withCredentials: etc.credentials || requestAttrValues.credentials || htmx.config.withCredentials, timeout: etc.timeout || requestAttrValues.timeout || htmx.config.timeout, path:path, triggeringEvent:event }; if(!triggerEvent(elt, 'htmx:configRequest', requestConfig)){ maybeCall(resolve); endRequestLock(); return promise; } // copy out in case the object was overwritten path = requestConfig.path; verb = requestConfig.verb; headers = requestConfig.headers; filteredParameters = requestConfig.parameters; errors = requestConfig.errors; useUrlParams = requestConfig.useUrlParams; if(errors && errors.length > 0){ triggerEvent(elt, 'htmx:validation:halted', requestConfig) maybeCall(resolve); endRequestLock(); return promise; } var splitPath = path.split("#"); var pathNoAnchor = splitPath[0]; var anchor = splitPath[1]; var finalPath = path if (useUrlParams) { finalPath = pathNoAnchor; var values = Object.keys(filteredParameters).length !== 0; if (values) { if (finalPath.indexOf("?") < 0) { finalPath += "?"; } else { finalPath += "&"; } finalPath += urlEncode(filteredParameters); if (anchor) { finalPath += "#" + anchor; } } } xhr.open(verb.toUpperCase(), finalPath, true); xhr.overrideMimeType("text/html"); xhr.withCredentials = requestConfig.withCredentials; xhr.timeout = requestConfig.timeout; // request headers if (requestAttrValues.noHeaders) { // ignore all headers } else { for (var header in headers) { if (headers.hasOwnProperty(header)) { var headerValue = headers[header]; safelySetHeaderValue(xhr, header, headerValue); } } } var responseInfo = { xhr: xhr, target: target, requestConfig: requestConfig, etc: etc, boosted: eltIsBoosted, pathInfo: { requestPath: path, finalRequestPath: finalPath, anchor: anchor } }; xhr.onload = function () { try { var hierarchy = hierarchyForElt(elt); responseInfo.pathInfo.responsePath = getPathFromResponse(xhr); responseHandler(elt, responseInfo); removeRequestIndicatorClasses(indicators); triggerEvent(elt, 'htmx:afterRequest', responseInfo); triggerEvent(elt, 'htmx:afterOnLoad', responseInfo); // if the body no longer contains the element, trigger the event on the closest parent // remaining in the DOM if (!bodyContains(elt)) { var secondaryTriggerElt = null; while (hierarchy.length > 0 && secondaryTriggerElt == null) { var parentEltInHierarchy = hierarchy.shift(); if (bodyContains(parentEltInHierarchy)) { secondaryTriggerElt = parentEltInHierarchy; } } if (secondaryTriggerElt) { triggerEvent(secondaryTriggerElt, 'htmx:afterRequest', responseInfo); triggerEvent(secondaryTriggerElt, 'htmx:afterOnLoad', responseInfo); } } maybeCall(resolve); endRequestLock(); } catch (e) { triggerErrorEvent(elt, 'htmx:onLoadError', mergeObjects({error:e}, responseInfo)); throw e; } } xhr.onerror = function () { removeRequestIndicatorClasses(indicators); triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo); triggerErrorEvent(elt, 'htmx:sendError', responseInfo); maybeCall(reject); endRequestLock(); } xhr.onabort = function() { removeRequestIndicatorClasses(indicators); triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo); triggerErrorEvent(elt, 'htmx:sendAbort', responseInfo); maybeCall(reject); endRequestLock(); } xhr.ontimeout = function() { removeRequestIndicatorClasses(indicators); triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo); triggerErrorEvent(elt, 'htmx:timeout', responseInfo); maybeCall(reject); endRequestLock(); } if(!triggerEvent(elt, 'htmx:beforeRequest', responseInfo)){ maybeCall(resolve); endRequestLock() return promise } var indicators = addRequestIndicatorClasses(elt); forEach(['loadstart', 'loadend', 'progress', 'abort'], function(eventName) { forEach([xhr, xhr.upload], function (target) { target.addEventListener(eventName, function(event){ triggerEvent(elt, "htmx:xhr:" + eventName, { lengthComputable:event.lengthComputable, loaded:event.loaded, total:event.total }); }) }); }); triggerEvent(elt, 'htmx:beforeSend', responseInfo); var params = useUrlParams ? null : encodeParamsForBody(xhr, elt, filteredParameters) xhr.send(params); return promise; } function determineHistoryUpdates(elt, responseInfo) { var xhr = responseInfo.xhr; //=========================================== // First consult response headers //=========================================== var pathFromHeaders = null; var typeFromHeaders = null; if (hasHeader(xhr,/HX-Push:/i)) { pathFromHeaders = xhr.getResponseHeader("HX-Push"); typeFromHeaders = "push"; } else if (hasHeader(xhr,/HX-Push-Url:/i)) { pathFromHeaders = xhr.getResponseHeader("HX-Push-Url"); typeFromHeaders = "push"; } else if (hasHeader(xhr,/HX-Replace-Url:/i)) { pathFromHeaders = xhr.getResponseHeader("HX-Replace-Url"); typeFromHeaders = "replace"; } // if there was a response header, that has priority if (pathFromHeaders) { if (pathFromHeaders === "false") { return {} } else { return { type: typeFromHeaders, path : pathFromHeaders } } } //=========================================== // Next resolve via DOM values //=========================================== var requestPath = responseInfo.pathInfo.finalRequestPath; var responsePath = responseInfo.pathInfo.responsePath; var pushUrl = getClosestAttributeValue(elt, "hx-push-url"); var replaceUrl = getClosestAttributeValue(elt, "hx-replace-url"); var elementIsBoosted = getInternalData(elt).boosted; var saveType = null; var path = null; if (pushUrl) { saveType = "push"; path = pushUrl; } else if (replaceUrl) { saveType = "replace"; path = replaceUrl; } else if (elementIsBoosted) { saveType = "push"; path = responsePath || requestPath; // if there is no response path, go with the original request path } if (path) { // false indicates no push, return empty object if (path === "false") { return {}; } // true indicates we want to follow wherever the server ended up sending us if (path === "true") { path = responsePath || requestPath; // if there is no response path, go with the original request path } // restore any anchor associated with the request if (responseInfo.pathInfo.anchor && path.indexOf("#") === -1) { path = path + "#" + responseInfo.pathInfo.anchor; } return { type:saveType, path: path } } else { return {}; } } function handleAjaxResponse(elt, responseInfo) { var xhr = responseInfo.xhr; var target = responseInfo.target; var etc = responseInfo.etc; if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return; if (hasHeader(xhr, /HX-Trigger:/i)) { handleTrigger(xhr, "HX-Trigger", elt); } if (hasHeader(xhr, /HX-Location:/i)) { saveCurrentPageToHistory(); var redirectPath = xhr.getResponseHeader("HX-Location"); var swapSpec; if (redirectPath.indexOf("{") === 0) { swapSpec = parseJSON(redirectPath); // what's the best way to throw an error if the user didn't include this redirectPath = swapSpec['path']; delete swapSpec['path']; } ajaxHelper('GET', redirectPath, swapSpec).then(function(){ pushUrlIntoHistory(redirectPath); }); return; } if (hasHeader(xhr, /HX-Redirect:/i)) { location.href = xhr.getResponseHeader("HX-Redirect"); return; } if (hasHeader(xhr,/HX-Refresh:/i)) { if ("true" === xhr.getResponseHeader("HX-Refresh")) { location.reload(); return; } } if (hasHeader(xhr,/HX-Retarget:/i)) { responseInfo.target = getDocument().querySelector(xhr.getResponseHeader("HX-Retarget")); } var historyUpdate = determineHistoryUpdates(elt, responseInfo); // by default htmx only swaps on 200 return codes and does not swap // on 204 'No Content' // this can be ovverriden by responding to the htmx:beforeSwap event and // overriding the detail.shouldSwap property var shouldSwap = xhr.status >= 200 && xhr.status < 400 && xhr.status !== 204; var serverResponse = xhr.response; var isError = xhr.status >= 400; var beforeSwapDetails = mergeObjects({shouldSwap: shouldSwap, serverResponse:serverResponse, isError:isError}, responseInfo); if (!triggerEvent(target, 'htmx:beforeSwap', beforeSwapDetails)) return; target = beforeSwapDetails.target; // allow re-targeting serverResponse = beforeSwapDetails.serverResponse; // allow updating content isError = beforeSwapDetails.isError; // allow updating error responseInfo.target = target; // Make updated target available to response events responseInfo.failed = isError; // Make failed property available to response events responseInfo.successful = !isError; // Make successful property available to response events if (beforeSwapDetails.shouldSwap) { if (xhr.status === 286) { cancelPolling(elt); } withExtensions(elt, function (extension) { serverResponse = extension.transformResponse(serverResponse, xhr, elt); }); // Save current page if there will be a history update if (historyUpdate.type) { saveCurrentPageToHistory(); } var swapOverride = etc.swapOverride; if (hasHeader(xhr,/HX-Reswap:/i)) { swapOverride = xhr.getResponseHeader("HX-Reswap"); } var swapSpec = getSwapSpecification(elt, swapOverride); target.classList.add(htmx.config.swappingClass); // optional transition API promise callbacks var settleResolve = null; var settleReject = null; var doSwap = function () { try { var activeElt = document.activeElement; var selectionInfo = {}; try { selectionInfo = { elt: activeElt, // @ts-ignore start: activeElt ? activeElt.selectionStart : null, // @ts-ignore end: activeElt ? activeElt.selectionEnd : null }; } catch (e) { // safari issue - see https://github.com/microsoft/playwright/issues/5894 } var selectOverride; if (hasHeader(xhr, /HX-Reselect:/i)) { selectOverride = xhr.getResponseHeader("HX-Reselect"); } var settleInfo = makeSettleInfo(target); selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo, selectOverride); if (selectionInfo.elt && !bodyContains(selectionInfo.elt) && selectionInfo.elt.id) { var newActiveElt = document.getElementById(selectionInfo.elt.id); var focusOptions = { preventScroll: swapSpec.focusScroll !== undefined ? !swapSpec.focusScroll : !htmx.config.defaultFocusScroll }; if (newActiveElt) { // @ts-ignore if (selectionInfo.start && newActiveElt.setSelectionRange) { // @ts-ignore try { newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end); } catch (e) { // the setSelectionRange method is present on fields that don't support it, so just let this fail } } newActiveElt.focus(focusOptions); } } target.classList.remove(htmx.config.swappingClass); forEach(settleInfo.elts, function (elt) { if (elt.classList) { elt.classList.add(htmx.config.settlingClass); } triggerEvent(elt, 'htmx:afterSwap', responseInfo); }); if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) { var finalElt = elt; if (!bodyContains(elt)) { finalElt = getDocument().body; } handleTrigger(xhr, "HX-Trigger-After-Swap", finalElt); } var doSettle = function () { forEach(settleInfo.tasks, function (task) { task.call(); }); forEach(settleInfo.elts, function (elt) { if (elt.classList) { elt.classList.remove(htmx.config.settlingClass); } triggerEvent(elt, 'htmx:afterSettle', responseInfo); }); // if we need to save history, do so if (historyUpdate.type) { 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 = find("#" + responseInfo.pathInfo.anchor); if(anchorTarget) { anchorTarget.scrollIntoView({block:'start', behavior: "auto"}); } } if(settleInfo.title) { var titleElt = find("title"); if(titleElt) { titleElt.innerHTML = settleInfo.title; } else { window.document.title = settleInfo.title; } } updateScrollState(settleInfo.elts, swapSpec); if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) { var finalElt = elt; if (!bodyContains(elt)) { finalElt = getDocument().body; } handleTrigger(xhr, "HX-Trigger-After-Settle", finalElt); } maybeCall(settleResolve); } if (swapSpec.settleDelay > 0) { setTimeout(doSettle, swapSpec.settleDelay) } else { doSettle(); } } catch (e) { triggerErrorEvent(elt, 'htmx:swapError', responseInfo); maybeCall(settleReject); throw e; } }; var shouldTransition = htmx.config.globalViewTransitions if(swapSpec.hasOwnProperty('transition')){ shouldTransition = swapSpec.transition; } if(shouldTransition && triggerEvent(elt, 'htmx:beforeTransition', responseInfo) && typeof Promise !== "undefined" && document.startViewTransition){ var settlePromise = new Promise(function (_resolve, _reject) { settleResolve = _resolve; settleReject = _reject; }); // wrap the original doSwap() in a call to startViewTransition() var innerDoSwap = doSwap; doSwap = function() { document.startViewTransition(function () { innerDoSwap(); return settlePromise; }); } } if (swapSpec.swapDelay > 0) { setTimeout(doSwap, swapSpec.swapDelay) } else { doSwap(); } } if (isError) { triggerErrorEvent(elt, 'htmx:responseError', mergeObjects({error: "Response Status Error Code " + xhr.status + " from " + responseInfo.pathInfo.requestPath}, responseInfo)); } } //==================================================================== // Extensions API //==================================================================== /** @type {Object} */ var extensions = {}; /** * extensionBase defines the default functions for all extensions. * @returns {import("./htmx").HtmxExtension} */ function extensionBase() { return { init: function(api) {return null;}, onEvent : function(name, evt) {return true;}, transformResponse : function(text, xhr, elt) {return text;}, isInlineSwap : function(swapStyle) {return false;}, handleSwap : function(swapStyle, target, fragment, settleInfo) {return false;}, encodeParameters : function(xhr, parameters, elt) {return null;} } } /** * defineExtension initializes the extension and adds it to the htmx registry * * @param {string} name * @param {import("./htmx").HtmxExtension} extension */ function defineExtension(name, extension) { if(extension.init) { extension.init(internalAPI) } extensions[name] = mergeObjects(extensionBase(), extension); } /** * removeExtension removes an extension from the htmx registry * * @param {string} name */ function removeExtension(name) { delete extensions[name]; } /** * getExtensions searches up the DOM tree to return all extensions that can be applied to a given element * * @param {HTMLElement} elt * @param {import("./htmx").HtmxExtension[]=} extensionsToReturn * @param {import("./htmx").HtmxExtension[]=} extensionsToIgnore */ function getExtensions(elt, extensionsToReturn, extensionsToIgnore) { if (elt == undefined) { return extensionsToReturn; } if (extensionsToReturn == undefined) { extensionsToReturn = []; } if (extensionsToIgnore == undefined) { extensionsToIgnore = []; } var extensionsForElement = getAttributeValue(elt, "hx-ext"); if (extensionsForElement) { forEach(extensionsForElement.split(","), function(extensionName){ extensionName = extensionName.replace(/ /g, ''); if (extensionName.slice(0, 7) == "ignore:") { extensionsToIgnore.push(extensionName.slice(7)); return; } if (extensionsToIgnore.indexOf(extensionName) < 0) { var extension = extensions[extensionName]; if (extension && extensionsToReturn.indexOf(extension) < 0) { extensionsToReturn.push(extension); } } }); } return getExtensions(parentElt(elt), extensionsToReturn, extensionsToIgnore); } //==================================================================== // Initialization //==================================================================== function ready(fn) { if (getDocument().readyState !== 'loading') { fn(); } else { getDocument().addEventListener('DOMContentLoaded', fn); } } function insertIndicatorStyles() { if (htmx.config.includeIndicatorStyles !== false) { getDocument().head.insertAdjacentHTML("beforeend", ""); } } function getMetaConfig() { var element = getDocument().querySelector('meta[name="htmx-config"]'); if (element) { // @ts-ignore return parseJSON(element.content); } else { return null; } } function mergeMetaConfig() { var metaConfig = getMetaConfig(); if (metaConfig) { htmx.config = mergeObjects(htmx.config , metaConfig) } } // initialize the document ready(function () { mergeMetaConfig(); insertIndicatorStyles(); var body = getDocument().body; processNode(body); var restoredElts = getDocument().querySelectorAll( "[hx-trigger='restored'],[data-hx-trigger='restored']" ); body.addEventListener("htmx:abort", function (evt) { var target = evt.target; var internalData = getInternalData(target); if (internalData && internalData.xhr) { internalData.xhr.abort(); } }); var originalPopstate = window.onpopstate; window.onpopstate = function (event) { if (event.state && event.state.htmx) { restoreHistory(); forEach(restoredElts, function(elt){ triggerEvent(elt, 'htmx:restored', { 'document': getDocument(), 'triggerEvent': triggerEvent }); }); } else { if (originalPopstate) { originalPopstate(event); } } }; setTimeout(function () { triggerEvent(body, 'htmx:load', {}); // give ready handlers a chance to load up before firing this event body = null; // kill reference for gc }, 0); }) return htmx; } )() }));