UNPKG

htmx.org

Version:

high power tools for html

1,288 lines (1,151 loc) 78.5 kB
//AMD insanity (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define([], factory); } else { // Browser globals root.htmx = factory(); } }(typeof self !== 'undefined' ? self : this, function () { return (function () { 'use strict'; // Public API var htmx = { onLoad: onLoadHelper, process: processNode, on: addEventListenerImpl, off: removeEventListenerImpl, trigger : triggerEvent, find : find, findAll : findAll, closest : closest, remove : removeElement, addClass : addClassToElement, removeClass : removeClassFromElement, toggleClass : toggleClassOnElement, takeClass : takeClassForElement, defineExtension : defineExtension, removeExtension : removeExtension, logAll : logAll, logger : null, config : { historyEnabled:true, historyCacheSize:10, defaultSwapStyle:'innerHTML', defaultSwapDelay:0, defaultSettleDelay:100, includeIndicatorStyles:true, indicatorClass:'htmx-indicator', requestClass:'htmx-request', settlingClass:'htmx-settling', swappingClass:'htmx-swapping', attributesToSwizzle:["class", "style", "width", "height"] }, parseInterval:parseInterval, _:internalEval, createEventSource: function(url){ return new EventSource(url, {withCredentials:true}) }, createWebSocket: function(url){ return new WebSocket(url, []); } }; var VERBS = ['get', 'post', 'put', 'delete', 'patch']; var VERB_SELECTOR = VERBS.map(function(verb){ return "[hx-" + verb + "], [data-hx-" + verb + "]" }).join(", "); var windowIsScrolling = false // used by initScrollHandler //==================================================================== // Utilities //==================================================================== function parseInterval(str) { if (str == null || str === "null" || str === "false" || str === "") { return null; } else if (str.lastIndexOf("ms") === str.length - 2) { return parseFloat(str.substr(0, str.length - 2)); } else if (str.lastIndexOf("s") === str.length - 1) { return parseFloat(str.substr(0, str.length - 1)) * 1000; } else { return parseFloat(str); } } 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)); } function getAttributeValue(elt, qualifiedName) { return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName); } function parentElt(elt) { return elt.parentElement; } function getDocument() { return document; } function getClosestMatch(elt, condition) { if (condition(elt)) { return elt; } else if (parentElt(elt)) { return getClosestMatch(parentElt(elt), condition); } else { return null; } } function getClosestAttributeValue(elt, attributeName) { var closestAttr = null; getClosestMatch(elt, function (e) { return closestAttr = getAttributeValue(e, attributeName); }); return closestAttr; } function matches(elt, selector) { // noinspection JSUnresolvedVariable var matchesFunction = elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector; return matchesFunction && matchesFunction.call(elt, selector); } 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 ""; } } function parseHTML(resp, depth) { var parser = new DOMParser(); var responseDoc = parser.parseFromString(resp, "text/html"); var responseNode = responseDoc.body; while (depth > 0) { depth--; responseNode = responseNode.firstChild; } if (responseNode == null) { responseNode = getDocument().createDocumentFragment(); } return responseNode; } function makeFragment(resp) { var startTag = getStartTag(resp); switch (startTag) { case "thead": case "tbody": case "tfoot": case "colgroup": case "caption": return parseHTML("<table>" + resp + "</table>", 1); case "col": return parseHTML("<table><colgroup>" + resp + "</colgroup></table>", 2); case "tr": return parseHTML("<table><tbody>" + resp + "</tbody></table>", 2); case "td": case "th": return parseHTML("<table><tbody><tr>" + resp + "</tr></tbody></table>", 3); default: return parseHTML(resp, 0); } } function isType(o, type) { return Object.prototype.toString.call(o) === "[object " + type + "]"; } function isFunction(o) { return isType(o, "Function"); } function isRawObject(o) { return isType(o, "Object"); } function getInternalData(elt) { var dataProp = 'htmx-internal-data'; var data = elt[dataProp]; if (!data) { data = elt[dataProp] = {}; } return data; } 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) { return getDocument().body.contains(elt); } function splitOnWhitespace(trigger) { return trigger.trim().split(/\s+/); } 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; } } //========================================================================================== // public API //========================================================================================== function internalEval(str){ 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 find(eltOrSelector, selector) { if (selector) { return eltOrSelector.querySelector(selector); } else { return getDocument().body.querySelector(eltOrSelector); } } function findAll(eltOrSelector, selector) { if (selector) { return eltOrSelector.querySelectorAll(selector); } else { return getDocument().body.querySelectorAll(eltOrSelector); } } function removeElement(elt, delay) { if (delay) { setTimeout(function(){removeElement(elt);}, delay) } else { elt.parentElement.removeChild(elt); } } function addClassToElement(elt, clazz, delay) { if (delay) { setTimeout(function(){addClassToElement(elt, clazz);}, delay) } else { elt.classList.add(clazz); } } function removeClassFromElement(elt, clazz, delay) { if (delay) { setTimeout(function(){removeClassFromElement(elt, clazz);}, delay) } else { elt.classList.remove(clazz); } } function toggleClassOnElement(elt, clazz) { elt.classList.toggle(clazz); } function takeClassForElement(elt, clazz) { forEach(elt.parentElement.children, function(child){ removeClassFromElement(child, clazz); }) addClassToElement(elt, clazz); } function closest(elt, selector) { do if (elt == null || matches(elt, selector)) return elt; while (elt = elt && parentElt(elt)); } function processEventArgs(arg1, arg2, arg3) { if (isFunction(arg2)) { return { target: getDocument().body, event: arg1, listener: arg2 } } else { return { target: 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 //==================================================================== function getTarget(elt) { var explicitTarget = getClosestMatch(elt, function(e){return getAttributeValue(e,"hx-target") !== null}); if (explicitTarget) { var targetStr = getAttributeValue(explicitTarget, "hx-target"); if (targetStr === "this") { return explicitTarget; } else if (targetStr.indexOf("closest ") === 0) { return closest(elt, targetStr.substr(8)); } else if (targetStr.indexOf("find ") === 0) { return find(elt, targetStr.substr(5)); } else { return getDocument().querySelector(targetStr); } } else { var data = getInternalData(elt); if (data.boosted) { return getDocument().body; } else { return elt; } } } function shouldSettleAttribute(name) { var attributesToSwizzle = htmx.config.attributesToSwizzle; for (var i = 0; i < attributesToSwizzle.length; i++) { if (name === attributesToSwizzle[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"; } function oobSwap(oobValue, child, settleInfo) { if (oobValue === "true") { oobValue = "outerHTML" } var target = getDocument().getElementById(child.id); if (target) { var fragment; fragment = getDocument().createDocumentFragment(); fragment.appendChild(child); // pulls the child out of the existing fragment if (!isInlineSwap(oobValue, target)) { fragment = child; // if this is not an inline swap, we use the content of the node, not the node itself } swap(oobValue, target, target, fragment, settleInfo); } else { child.parentNode.removeChild(child); triggerErrorEvent(getDocument().body, "htmx:oobErrorNoTarget", {content: child}) } return oobValue; } function handleOutOfBandSwaps(fragment, settleInfo) { forEach(toArray(fragment.children), function (child) { var oobValue = getAttributeValue(child, "hx-swap-oob"); if (oobValue != null) { oobSwap(oobValue, child, settleInfo); } }); } function handleAttributes(parentNode, fragment, settleInfo) { forEach(fragment.querySelectorAll("[id]"), function (newNode) { if (newNode.id && newNode.id.length > 0) { var oldNode = parentNode.querySelector(newNode.tagName + "[id='" + newNode.id + "']"); if (oldNode && oldNode !== parentNode) { var newAttributes = newNode.cloneNode(); cloneAttributes(newNode, oldNode); settleInfo.tasks.push(function () { cloneAttributes(newNode, newAttributes); }); } } }); } function makeAjaxLoadTask(child) { return function () { 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; parentNode.insertBefore(child, insertBefore); if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) { settleInfo.tasks.push(makeAjaxLoadTask(child)); } } } function closeConnections(target) { var internalData = getInternalData(target); if (internalData.webSocket) { internalData.webSocket.close(); } if (internalData.sseEventSource) { internalData.sseEventSource.close(); } if (target.children) { // IE forEach(target.children, function(child) { closeConnections(child) }); } } function swapOuterHTML(target, fragment, settleInfo) { if (target.tagName === "BODY") { return swapInnerHTML(target, fragment); } else { var eltBeforeNewContent = target.previousSibling; insertNodesBefore(parentElt(target), target, fragment, settleInfo); if (eltBeforeNewContent == null) { var newElt = parentElt(target).firstChild; } else { var newElt = eltBeforeNewContent.nextSibling; } getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later while(newElt && newElt !== target) { settleInfo.elts.push(newElt); newElt = newElt.nextSibling; } closeConnections(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 swapInnerHTML(target, fragment, settleInfo) { var firstChild = target.firstChild; insertNodesBefore(target, firstChild, fragment, settleInfo); if (firstChild) { while (firstChild.nextSibling) { closeConnections(firstChild.nextSibling) target.removeChild(firstChild.nextSibling); } closeConnections(firstChild) target.removeChild(firstChild); } } function maybeSelectFromResponse(elt, fragment) { var selector = 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; 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); } } swapInnerHTML(target, fragment, settleInfo); } } function selectAndSwap(swapStyle, target, elt, responseText, settleInfo) { var fragment = makeFragment(responseText); if (fragment) { handleOutOfBandSwaps(fragment, settleInfo); fragment = maybeSelectFromResponse(elt, fragment); return swap(swapStyle, elt, target, fragment, settleInfo); } } function handleTrigger(elt, trigger) { if (trigger) { if (trigger.indexOf("{") === 0) { var triggers = parseJSON(trigger); 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, trigger, []); } } } function getTriggerSpecs(elt) { var explicitTrigger = getAttributeValue(elt, 'hx-trigger'); if (explicitTrigger) { var triggerSpecs = explicitTrigger.split(',').map(function(triggerString) { var tokens = splitOnWhitespace(triggerString.trim()); var trigger = tokens[0]; // splitOnWhitespace returns at least one element if (!trigger) return null; if (trigger === "every") return {trigger: 'every', pollInterval: parseInterval(tokens[1])}; if (trigger.indexOf("sse:") === 0) return {trigger: 'sse', sseEvent: trigger.substr(4)}; var triggerSpec = {trigger: trigger}; for (var i = 1; i < tokens.length; i++) { var token = tokens[i].trim(); if (token === "changed") { triggerSpec.changed = true; } if (token === "once") { triggerSpec.once = true; } if (token.indexOf("delay:") === 0) { triggerSpec.delay = parseInterval(token.substr(6)); } if (token.indexOf("throttle:") === 0) { triggerSpec.throttle = parseInterval(token.substr(9)); } } return triggerSpec; }).filter(function(x){ return x !== null }); if (triggerSpecs.length) return triggerSpecs; } if (matches(elt, 'form')) return [{trigger: 'submit'}]; if (matches(elt, 'input, textarea, select')) return [{trigger: 'change'}]; return [{trigger: 'click'}]; } function cancelPolling(elt) { getInternalData(elt).cancelled = true; } function processPolling(elt, verb, path, interval) { var nodeData = getInternalData(elt); nodeData.timeout = setTimeout(function () { if (bodyContains(elt) && nodeData.cancelled !== true) { issueAjaxRequest(elt, verb, path); processPolling(elt, verb, getAttributeValue(elt, "hx-" + verb), interval); } }, interval); } 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.tagName === "FORM") { nodeData.boosted = true; var verb, path; if (elt.tagName === "A") { verb = "get"; path = getRawAttribute(elt, 'href'); } else { var rawAttribute = getRawAttribute(elt, "method"); verb = rawAttribute ? rawAttribute.toLowerCase() : "get"; path = getRawAttribute(elt, 'action'); } triggerSpecs.forEach(function(triggerSpec) { addEventListener(elt, verb, path, nodeData, triggerSpec, true); }); } } function shouldCancel(elt) { return elt.tagName === "FORM" || (matches(elt, 'input[type="submit"], button') && closest(elt, 'form') !== null) || (elt.tagName === "A" && elt.href && elt.href.indexOf('#') !== 0); } function ignoreBoostedAnchorCtrlClick(elt, evt) { return getInternalData(elt).boosted && elt.tagName === "A" && evt.type === "click" && evt.ctrlKey; } function addEventListener(elt, verb, path, nodeData, triggerSpec, explicitCancel) { var eventListener = function (evt) { if (ignoreBoostedAnchorCtrlClick(elt, evt)) { return; } if(explicitCancel || shouldCancel(elt)){ evt.preventDefault(); } var eventData = getInternalData(evt); var elementData = getInternalData(elt); if (!eventData.handled) { eventData.handled = true; 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) { elementData.throttle = setTimeout(function(){ issueAjaxRequest(elt, verb, path, evt.target); elementData.throttle = null; }, triggerSpec.throttle); } else if (triggerSpec.delay) { elementData.delayed = setTimeout(function(){ issueAjaxRequest(elt, verb, path, evt.target); }, triggerSpec.delay); } else { issueAjaxRequest(elt, verb, path, evt.target); } } }; nodeData.trigger = triggerSpec.trigger; nodeData.eventListener = eventListener; elt.addEventListener(triggerSpec.trigger, eventListener); } function initScrollHandler() { if (!window['htmxScrollHandler']) { var scrollHandler = function() { windowIsScrolling = true }; window['htmxScrollHandler'] = scrollHandler; 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) { var nodeData = getInternalData(elt); if (!nodeData.revealed && isScrolledIntoView(elt)) { nodeData.revealed = true; issueAjaxRequest(elt, nodeData.verb, nodeData.path); } } 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") { processWebSocketSource(elt, value[1]); } if (value[0] === "send") { processWebSocketSend(elt); } } } function processWebSocketSource(elt, wssSource) { if (wssSource.indexOf("ws:") !== 0 && wssSource.indexOf("wss:") !== 0) { wssSource = "wss:" + wssSource; } var socket = htmx.createWebSocket(wssSource); socket.onerror = function (e) { triggerErrorEvent(elt, "htmx:wsError", {error:e, socket:socket}); maybeCloseWebSocketSource(elt); }; 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) { var webSocket = getInternalData(webSocketSourceElt).webSocket; elt.addEventListener(getTriggerSpecs(elt)[0].trigger, function (evt) { var headers = getHeaders(elt, webSocketSourceElt, null, elt); var results = getInputValues(elt, 'post'); var rawParameters = results.values; var errors = results.errors; var filteredParameters = filterValues(rawParameters, elt); filteredParameters['HEADERS'] = headers; if (errors && errors.length > 0) { triggerEvent(elt, 'htmx:validation:halted', errors); return; } webSocket.send(JSON.stringify(filteredParameters)); if(shouldCancel(elt)){ evt.preventDefault(); } }); } else { triggerErrorEvent(elt, "htmx:noWebSocketSourceError"); } } //==================================================================== // 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)) { 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, elt, target, response, settleInfo) triggerEvent(elt, "htmx:sseMessage", event) }; getInternalData(elt).sseListener = sseListener; sseEventSource.addEventListener(sseEventName, sseListener); } else { triggerErrorEvent(elt, "htmx:noSSESourceError"); } } function processSSETrigger(elt, verb, path, sseEventName) { var sseSourceElt = getClosestMatch(elt, hasEventSource); if (sseSourceElt) { var sseEventSource = getInternalData(sseSourceElt).sseEventSource; var sseListener = function () { if (!maybeCloseSSESource(sseSourceElt)) { if (bodyContains(elt)) { issueAjaxRequest(elt, verb, path); } 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, verb, path, nodeData, delay) { var load = function(){ if (!nodeData.loaded) { nodeData.loaded = true; issueAjaxRequest(elt, verb, path); } } 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) { if (triggerSpec.sseEvent) { processSSETrigger(elt, verb, path, triggerSpec.sseEvent); } else if (triggerSpec.trigger === "revealed") { initScrollHandler(); maybeReveal(elt); } else if (triggerSpec.trigger === "load") { loadImmediately(elt, verb, path, nodeData, triggerSpec.delay); } else if (triggerSpec.pollInterval) { nodeData.polling = true; processPolling(elt, verb, path, triggerSpec.pollInterval); } else { addEventListener(elt, verb, path, nodeData, triggerSpec); } }); } }); return explicitAction; } function evalScript(script) { if (script.type === "text/javascript") { try { eval(script.innerText); } catch (e) { logError(e); } } } function processScripts(elt) { if (matches(elt, "script")) { evalScript(elt); } forEach(findAll(elt, "script"), function (script) { evalScript(script); }); } function findElementsToProcess(elt) { if (elt.querySelectorAll) { var results = elt.querySelectorAll(VERB_SELECTOR + ", a, form, [hx-sse], [data-hx-sse], [hx-ws]," + " [data-hx-ws]"); return results; } else { return []; } } function initNode(elt) { var nodeData = getInternalData(elt); if (!nodeData.initialized) { nodeData.initialized = true; if (elt.value) { nodeData.lastValue = elt.value; } var triggerSpecs = getTriggerSpecs(elt); var explicitAction = processVerbs(elt, nodeData, triggerSpecs); if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") { boostElement(elt, nodeData, triggerSpecs); } 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:processedNode"); } } function processNode(elt) { initNode(elt); forEach(findElementsToProcess(elt), function(child) { initNode(child) }); } //==================================================================== // 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:processedNode" } 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) { 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 = null; function getHistoryElement() { var historyElt = getDocument().querySelector('[hx-history-elt],[data-hx-history-elt]'); return historyElt || getDocument().body; } function saveToHistoryCache(url, content, title, scroll) { var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || []; for (var i = 0; i < historyCache.length; i++) { if (historyCache[i].url === url) { historyCache = historyCache.slice(i, 1); break; } } historyCache.push({url:url, content: content, title:title, scroll:scroll}) while (historyCache.length > htmx.config.historyCacheSize) { historyCache.shift(); } localStorage.setItem("htmx-history-cache", JSON.stringify(historyCache)); } function getCachedHistory(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 saveHistory() { var elt = getHistoryElement(); var path = currentPathForHistory || location.pathname+location.search; triggerEvent(getDocument().body, "htmx:beforeHistorySave", {path:path, historyElt:elt}); if(htmx.config.historyEnabled) history.replaceState({htmx:true}, getDocument().title, window.location.href); saveToHistoryCache(path, cleanInnerHtmlForHistory(elt), getDocument().title, window.scrollY); } function pushUrlIntoHistory(path) { if(htmx.config.historyEnabled) history.pushState({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.onload = function () { if (this.status >= 200 && this.status < 400) { triggerEvent(getDocument().body, "htmx:historyCacheMissLoad", details); var fragment = makeFragment(this.response); fragment = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment; var historyElement = getHistoryElement(); var settleInfo = makeSettleInfo(historyElement); swapInnerHTML(historyElement, fragment, settleInfo) settleImmediately(settleInfo.tasks); currentPathForHistory = path; } else { triggerErrorEvent(getDocument().body, "htmx:historyCacheMissLoadError", details); } }; request.send(); } function restoreHistory(path) { saveHistory(currentPathForHistory); path = path || location.pathname+location.search; triggerEvent(getDocument().body, "htmx:historyRestore", {path:path}); 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; window.scrollTo(0, cached.scroll); currentPathForHistory = path; } else { loadHistoryFromServer(path); } } function shouldPush(elt) { var pushUrl = getClosestAttributeValue(elt, "hx-push-url"); return (pushUrl && pushUrl !== "false") || (elt.tagName === "A" && getInternalData(elt).boosted); } function getPushUrl(elt) { var pushUrl = getClosestAttributeValue(elt, "hx-push-url"); return (pushUrl === "true" || pushUrl === "false") ? null : pushUrl; } function addRequestIndicatorClasses(elt) { mutateRequestIndicatorClasses(elt, "add"); } function removeRequestIndicatorClasses(elt) { mutateRequestIndicatorClasses(elt, "remove"); } function mutateRequestIndicatorClasses(elt, action) { var indicator = getClosestAttributeValue(elt, 'hx-indicator'); if (indicator) { var indicators = getDocument().querySelectorAll(indicator); } else { indicators = [elt];