aboutsummaryrefslogtreecommitdiff
path: root/static/js/ext
diff options
context:
space:
mode:
authorAlexander Neonxp Kiryukhin <i@neonxp.ru>2024-10-12 02:52:22 +0300
committerAlexander Neonxp Kiryukhin <i@neonxp.ru>2024-10-12 02:53:52 +0300
commitd05ea66f4bbcf0cc5c8908f3435c68de1b070fa1 (patch)
tree7c7a769206646f2b81a0eda0680f0be5033a4197 /static/js/ext
Начальная версияv0.0.1
Diffstat (limited to 'static/js/ext')
-rw-r--r--static/js/ext/README.md9
-rw-r--r--static/js/ext/_WARNING_DEPRECATED_FILES_.txt5
-rw-r--r--static/js/ext/ajax-header.js11
-rw-r--r--static/js/ext/alpine-morph.js20
-rw-r--r--static/js/ext/class-tools.js97
-rw-r--r--static/js/ext/client-side-templates.js100
-rw-r--r--static/js/ext/debug.js15
-rw-r--r--static/js/ext/disable-element.js20
-rw-r--r--static/js/ext/event-header.js41
-rw-r--r--static/js/ext/head-support.js146
-rw-r--r--static/js/ext/include-vals.js28
-rw-r--r--static/js/ext/json-enc.js16
-rw-r--r--static/js/ext/loading-states.js189
-rw-r--r--static/js/ext/method-override.js15
-rw-r--r--static/js/ext/morphdom-swap.js21
-rw-r--r--static/js/ext/multi-swap.js50
-rw-r--r--static/js/ext/path-deps.js63
-rw-r--r--static/js/ext/path-params.js15
-rw-r--r--static/js/ext/preload.js151
-rw-r--r--static/js/ext/rails-method.js14
-rw-r--r--static/js/ext/remove-me.js31
-rw-r--r--static/js/ext/response-targets.js135
-rw-r--r--static/js/ext/restored.js19
-rw-r--r--static/js/ext/sse.js374
-rw-r--r--static/js/ext/ws.js481
25 files changed, 2066 insertions, 0 deletions
diff --git a/static/js/ext/README.md b/static/js/ext/README.md
new file mode 100644
index 0000000..366e714
--- /dev/null
+++ b/static/js/ext/README.md
@@ -0,0 +1,9 @@
+# Why Are These Files Here?
+
+These are legacy extensions for htmx 1.x and are **NOT** actively maintained or guaranteed to work with htmx 2.x.
+They are here because we unfortunately linked to unversioned unpkg URLs in the installation guides for them
+in 1.x, so we need to keep them here to preserve those URLs and not break existing users functionality.
+
+If you are looking for extensions for htmx 2.x, please see the [htmx 2.0 extensions site](https://htmx.org/extensions),
+which has links to the new extensions repos (They have all been moved to their own NPM projects and URLs, like
+they should have been from the start!)
diff --git a/static/js/ext/_WARNING_DEPRECATED_FILES_.txt b/static/js/ext/_WARNING_DEPRECATED_FILES_.txt
new file mode 100644
index 0000000..6dff4c2
--- /dev/null
+++ b/static/js/ext/_WARNING_DEPRECATED_FILES_.txt
@@ -0,0 +1,5 @@
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+!! THESE FILES ARE DEPRECATED AND UNSUPPORTED !!
+!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+SEE README.md FOR MORE DETAILS
diff --git a/static/js/ext/ajax-header.js b/static/js/ext/ajax-header.js
new file mode 100644
index 0000000..e8c2dbc
--- /dev/null
+++ b/static/js/ext/ajax-header.js
@@ -0,0 +1,11 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+htmx.defineExtension('ajax-header', {
+ onEvent: function (name, evt) {
+ if (name === "htmx:configRequest") {
+ evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest';
+ }
+ }
+});
diff --git a/static/js/ext/alpine-morph.js b/static/js/ext/alpine-morph.js
new file mode 100644
index 0000000..ea41cf0
--- /dev/null
+++ b/static/js/ext/alpine-morph.js
@@ -0,0 +1,20 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+htmx.defineExtension('alpine-morph', {
+ isInlineSwap: function (swapStyle) {
+ return swapStyle === 'morph';
+ },
+ handleSwap: function (swapStyle, target, fragment) {
+ if (swapStyle === 'morph') {
+ if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+ Alpine.morph(target, fragment.firstElementChild);
+ return [target];
+ } else {
+ Alpine.morph(target, fragment.outerHTML);
+ return [target];
+ }
+ }
+ }
+});
diff --git a/static/js/ext/class-tools.js b/static/js/ext/class-tools.js
new file mode 100644
index 0000000..83f5c7c
--- /dev/null
+++ b/static/js/ext/class-tools.js
@@ -0,0 +1,97 @@
+(function () {
+
+ if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+ }
+
+ function splitOnWhitespace(trigger) {
+ return trigger.split(/\s+/);
+ }
+
+ function parseClassOperation(trimmedValue) {
+ var split = splitOnWhitespace(trimmedValue);
+ if (split.length > 1) {
+ var operation = split[0];
+ var classDef = split[1].trim();
+ var cssClass;
+ var delay;
+ if (classDef.indexOf(":") > 0) {
+ var splitCssClass = classDef.split(':');
+ cssClass = splitCssClass[0];
+ delay = htmx.parseInterval(splitCssClass[1]);
+ } else {
+ cssClass = classDef;
+ delay = 100;
+ }
+ return {
+ operation: operation,
+ cssClass: cssClass,
+ delay: delay
+ }
+ } else {
+ return null;
+ }
+ }
+
+ function performOperation(elt, classOperation, classList, currentRunTime) {
+ setTimeout(function () {
+ elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
+ }, currentRunTime)
+ }
+
+ function toggleOperation(elt, classOperation, classList, currentRunTime) {
+ setTimeout(function () {
+ setInterval(function () {
+ elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
+ }, classOperation.delay);
+ }, currentRunTime)
+ }
+
+ function processClassList(elt, classList) {
+ var runs = classList.split("&");
+ for (var i = 0; i < runs.length; i++) {
+ var run = runs[i];
+ var currentRunTime = 0;
+ var classOperations = run.split(",");
+ for (var j = 0; j < classOperations.length; j++) {
+ var value = classOperations[j];
+ var trimmedValue = value.trim();
+ var classOperation = parseClassOperation(trimmedValue);
+ if (classOperation) {
+ if (classOperation.operation === "toggle") {
+ toggleOperation(elt, classOperation, classList, currentRunTime);
+ currentRunTime = currentRunTime + classOperation.delay;
+ } else {
+ currentRunTime = currentRunTime + classOperation.delay;
+ performOperation(elt, classOperation, classList, currentRunTime);
+ }
+ }
+ }
+ }
+ }
+
+ function maybeProcessClasses(elt) {
+ if (elt.getAttribute) {
+ var classList = elt.getAttribute("classes") || elt.getAttribute("data-classes");
+ if (classList) {
+ processClassList(elt, classList);
+ }
+ }
+ }
+
+ htmx.defineExtension('class-tools', {
+ onEvent: function (name, evt) {
+ if (name === "htmx:afterProcessNode") {
+ var elt = evt.detail.elt;
+ maybeProcessClasses(elt);
+ if (elt.querySelectorAll) {
+ var children = elt.querySelectorAll("[classes], [data-classes]");
+ for (var i = 0; i < children.length; i++) {
+ maybeProcessClasses(children[i]);
+ }
+ }
+ }
+ }
+ });
+})();
diff --git a/static/js/ext/client-side-templates.js b/static/js/ext/client-side-templates.js
new file mode 100644
index 0000000..d23c5c9
--- /dev/null
+++ b/static/js/ext/client-side-templates.js
@@ -0,0 +1,100 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+htmx.defineExtension('client-side-templates', {
+ transformResponse : function(text, xhr, elt) {
+
+ var mustacheTemplate = htmx.closest(elt, "[mustache-template]");
+ if (mustacheTemplate) {
+ var data = JSON.parse(text);
+ var templateId = mustacheTemplate.getAttribute('mustache-template');
+ var template = htmx.find("#" + templateId);
+ if (template) {
+ return Mustache.render(template.innerHTML, data);
+ } else {
+ throw "Unknown mustache template: " + templateId;
+ }
+ }
+
+ var mustacheArrayTemplate = htmx.closest(elt, "[mustache-array-template]");
+ if (mustacheArrayTemplate) {
+ var data = JSON.parse(text);
+ var templateId = mustacheArrayTemplate.getAttribute('mustache-array-template');
+ var template = htmx.find("#" + templateId);
+ if (template) {
+ return Mustache.render(template.innerHTML, {"data": data });
+ } else {
+ throw "Unknown mustache template: " + templateId;
+ }
+ }
+
+ var handlebarsTemplate = htmx.closest(elt, "[handlebars-template]");
+ if (handlebarsTemplate) {
+ var data = JSON.parse(text);
+ var templateId = handlebarsTemplate.getAttribute('handlebars-template');
+ var templateElement = htmx.find('#' + templateId).innerHTML;
+ var renderTemplate = Handlebars.compile(templateElement);
+ if (renderTemplate) {
+ return renderTemplate(data);
+ } else {
+ throw "Unknown handlebars template: " + templateId;
+ }
+ }
+
+ var handlebarsArrayTemplate = htmx.closest(elt, "[handlebars-array-template]");
+ if (handlebarsArrayTemplate) {
+ var data = JSON.parse(text);
+ var templateId = handlebarsArrayTemplate.getAttribute('handlebars-array-template');
+ var templateElement = htmx.find('#' + templateId).innerHTML;
+ var renderTemplate = Handlebars.compile(templateElement);
+ if (renderTemplate) {
+ return renderTemplate(data);
+ } else {
+ throw "Unknown handlebars template: " + templateId;
+ }
+ }
+
+ var nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
+ if (nunjucksTemplate) {
+ var data = JSON.parse(text);
+ var templateName = nunjucksTemplate.getAttribute('nunjucks-template');
+ var template = htmx.find('#' + templateName);
+ if (template) {
+ return nunjucks.renderString(template.innerHTML, data);
+ } else {
+ return nunjucks.render(templateName, data);
+ }
+ }
+
+ var xsltTemplate = htmx.closest(elt, "[xslt-template]");
+ if (xsltTemplate) {
+ var templateId = xsltTemplate.getAttribute('xslt-template');
+ var template = htmx.find("#" + templateId);
+ if (template) {
+ var content = template.innerHTML ? new DOMParser().parseFromString(template.innerHTML, 'application/xml')
+ : template.contentDocument;
+ var processor = new XSLTProcessor();
+ processor.importStylesheet(content);
+ var data = new DOMParser().parseFromString(text, "application/xml");
+ var frag = processor.transformToFragment(data, document);
+ return new XMLSerializer().serializeToString(frag);
+ } else {
+ throw "Unknown XSLT template: " + templateId;
+ }
+ }
+
+ var nunjucksArrayTemplate = htmx.closest(elt, "[nunjucks-array-template]");
+ if (nunjucksArrayTemplate) {
+ var data = JSON.parse(text);
+ var templateName = nunjucksArrayTemplate.getAttribute('nunjucks-array-template');
+ var template = htmx.find('#' + templateName);
+ if (template) {
+ return nunjucks.renderString(template.innerHTML, {"data": data});
+ } else {
+ return nunjucks.render(templateName, {"data": data});
+ }
+ }
+ return text;
+ }
+});
diff --git a/static/js/ext/debug.js b/static/js/ext/debug.js
new file mode 100644
index 0000000..aa88c9e
--- /dev/null
+++ b/static/js/ext/debug.js
@@ -0,0 +1,15 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+htmx.defineExtension('debug', {
+ onEvent: function (name, evt) {
+ if (console.debug) {
+ console.debug(name, evt);
+ } else if (console) {
+ console.log("DEBUG:", name, evt);
+ } else {
+ throw "NO CONSOLE SUPPORTED"
+ }
+ }
+});
diff --git a/static/js/ext/disable-element.js b/static/js/ext/disable-element.js
new file mode 100644
index 0000000..0192661
--- /dev/null
+++ b/static/js/ext/disable-element.js
@@ -0,0 +1,20 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+// Disable Submit Button
+htmx.defineExtension('disable-element', {
+ onEvent: function (name, evt) {
+ let elt = evt.detail.elt;
+ let target = elt.getAttribute("hx-disable-element");
+ let targetElements = (target == "self") ? [ elt ] : document.querySelectorAll(target);
+
+ for (var i = 0; i < targetElements.length; i++) {
+ if (name === "htmx:beforeRequest" && targetElements[i]) {
+ targetElements[i].disabled = true;
+ } else if (name == "htmx:afterRequest" && targetElements[i]) {
+ targetElements[i].disabled = false;
+ }
+ }
+ }
+});
diff --git a/static/js/ext/event-header.js b/static/js/ext/event-header.js
new file mode 100644
index 0000000..0ccb7de
--- /dev/null
+++ b/static/js/ext/event-header.js
@@ -0,0 +1,41 @@
+(function(){
+ if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+ }
+ function stringifyEvent(event) {
+ var obj = {};
+ for (var key in event) {
+ obj[key] = event[key];
+ }
+ return JSON.stringify(obj, function(key, value){
+ if(value instanceof Node){
+ var nodeRep = value.tagName;
+ if (nodeRep) {
+ nodeRep = nodeRep.toLowerCase();
+ if(value.id){
+ nodeRep += "#" + value.id;
+ }
+ if(value.classList && value.classList.length){
+ nodeRep += "." + value.classList.toString().replace(" ", ".")
+ }
+ return nodeRep;
+ } else {
+ return "Node"
+ }
+ }
+ if (value instanceof Window) return 'Window';
+ return value;
+ });
+ }
+
+ htmx.defineExtension('event-header', {
+ onEvent: function (name, evt) {
+ if (name === "htmx:configRequest") {
+ if (evt.detail.triggeringEvent) {
+ evt.detail.headers['Triggering-Event'] = stringifyEvent(evt.detail.triggeringEvent);
+ }
+ }
+ }
+ });
+})();
diff --git a/static/js/ext/head-support.js b/static/js/ext/head-support.js
new file mode 100644
index 0000000..07c0231
--- /dev/null
+++ b/static/js/ext/head-support.js
@@ -0,0 +1,146 @@
+//==========================================================
+// head-support.js
+//
+// An extension to htmx 1.0 to add head tag merging.
+//==========================================================
+(function(){
+
+ if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+ }
+
+ var api = null;
+
+ function log() {
+ //console.log(arguments);
+ }
+
+ function mergeHead(newContent, defaultMergeStrategy) {
+
+ if (newContent && newContent.indexOf('<head') > -1) {
+ const htmlDoc = document.createElement("html");
+ // remove svgs to avoid conflicts
+ var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
+ // extract head tag
+ var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);
+
+ // if the head tag exists...
+ if (headTag) {
+
+ var added = []
+ var removed = []
+ var preserved = []
+ var nodesToAppend = []
+
+ htmlDoc.innerHTML = headTag;
+ var newHeadTag = htmlDoc.querySelector("head");
+ var currentHead = document.head;
+
+ if (newHeadTag == null) {
+ return;
+ } else {
+ // put all new head elements into a Map, by their outerHTML
+ var srcToNewHeadNodes = new Map();
+ for (const newHeadChild of newHeadTag.children) {
+ srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
+ }
+ }
+
+
+
+ // determine merge strategy
+ var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
+
+ // get the current head
+ for (const currentHeadElt of currentHead.children) {
+
+ // If the current head element is in the map
+ var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
+ var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
+ var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
+ if (inNewContent || isPreserved) {
+ if (isReAppended) {
+ // remove the current version and let the new version replace it and re-execute
+ removed.push(currentHeadElt);
+ } else {
+ // this element already exists and should not be re-appended, so remove it from
+ // the new content map, preserving it in the DOM
+ srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
+ preserved.push(currentHeadElt);
+ }
+ } else {
+ if (mergeStrategy === "append") {
+ // we are appending and this existing element is not new content
+ // so if and only if it is marked for re-append do we do anything
+ if (isReAppended) {
+ removed.push(currentHeadElt);
+ nodesToAppend.push(currentHeadElt);
+ }
+ } else {
+ // if this is a merge, we remove this content since it is not in the new head
+ if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
+ removed.push(currentHeadElt);
+ }
+ }
+ }
+ }
+
+ // Push the tremaining new head elements in the Map into the
+ // nodes to append to the head tag
+ nodesToAppend.push(...srcToNewHeadNodes.values());
+ log("to append: ", nodesToAppend);
+
+ for (const newNode of nodesToAppend) {
+ log("adding: ", newNode);
+ var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
+ log(newElt);
+ if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
+ currentHead.appendChild(newElt);
+ added.push(newElt);
+ }
+ }
+
+ // remove all removed elements, after we have appended the new elements to avoid
+ // additional network requests for things like style sheets
+ for (const removedElement of removed) {
+ if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
+ currentHead.removeChild(removedElement);
+ }
+ }
+
+ api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
+ }
+ }
+ }
+
+ htmx.defineExtension("head-support", {
+ init: function(apiRef) {
+ // store a reference to the internal API.
+ api = apiRef;
+
+ htmx.on('htmx:afterSwap', function(evt){
+ var serverResponse = evt.detail.xhr.response;
+ if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
+ mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
+ }
+ })
+
+ htmx.on('htmx:historyRestore', function(evt){
+ if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
+ if (evt.detail.cacheMiss) {
+ mergeHead(evt.detail.serverResponse, "merge");
+ } else {
+ mergeHead(evt.detail.item.head, "merge");
+ }
+ }
+ })
+
+ htmx.on('htmx:historyItemCreated', function(evt){
+ var historyItem = evt.detail.item;
+ historyItem.head = document.head.outerHTML;
+ })
+ }
+ });
+
+})()
diff --git a/static/js/ext/include-vals.js b/static/js/ext/include-vals.js
new file mode 100644
index 0000000..53332d3
--- /dev/null
+++ b/static/js/ext/include-vals.js
@@ -0,0 +1,28 @@
+(function(){
+ if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+ }
+
+ function mergeObjects(obj1, obj2) {
+ for (var key in obj2) {
+ if (obj2.hasOwnProperty(key)) {
+ obj1[key] = obj2[key];
+ }
+ }
+ return obj1;
+ }
+
+ htmx.defineExtension('include-vals', {
+ onEvent: function (name, evt) {
+ if (name === "htmx:configRequest") {
+ var includeValsElt = htmx.closest(evt.detail.elt, "[include-vals],[data-include-vals]");
+ if (includeValsElt) {
+ var includeVals = includeValsElt.getAttribute("include-vals") || includeValsElt.getAttribute("data-include-vals");
+ var valuesToInclude = eval("({" + includeVals + "})");
+ mergeObjects(evt.detail.parameters, valuesToInclude);
+ }
+ }
+ }
+ });
+})();
diff --git a/static/js/ext/json-enc.js b/static/js/ext/json-enc.js
new file mode 100644
index 0000000..ef05680
--- /dev/null
+++ b/static/js/ext/json-enc.js
@@ -0,0 +1,16 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+htmx.defineExtension('json-enc', {
+ onEvent: function (name, evt) {
+ if (name === "htmx:configRequest") {
+ evt.detail.headers['Content-Type'] = "application/json";
+ }
+ },
+
+ encodeParameters : function(xhr, parameters, elt) {
+ xhr.overrideMimeType('text/json');
+ return (JSON.stringify(parameters));
+ }
+});
diff --git a/static/js/ext/loading-states.js b/static/js/ext/loading-states.js
new file mode 100644
index 0000000..1774304
--- /dev/null
+++ b/static/js/ext/loading-states.js
@@ -0,0 +1,189 @@
+;(function () {
+
+ if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+ }
+
+ let loadingStatesUndoQueue = []
+
+ function loadingStateContainer(target) {
+ return htmx.closest(target, '[data-loading-states]') || document.body
+ }
+
+ function mayProcessUndoCallback(target, callback) {
+ if (document.body.contains(target)) {
+ callback()
+ }
+ }
+
+ function mayProcessLoadingStateByPath(elt, requestPath) {
+ const pathElt = htmx.closest(elt, '[data-loading-path]')
+ if (!pathElt) {
+ return true
+ }
+
+ return pathElt.getAttribute('data-loading-path') === requestPath
+ }
+
+ function queueLoadingState(sourceElt, targetElt, doCallback, undoCallback) {
+ const delayElt = htmx.closest(sourceElt, '[data-loading-delay]')
+ if (delayElt) {
+ const delayInMilliseconds =
+ delayElt.getAttribute('data-loading-delay') || 200
+ const timeout = setTimeout(function () {
+ doCallback()
+
+ loadingStatesUndoQueue.push(function () {
+ mayProcessUndoCallback(targetElt, undoCallback)
+ })
+ }, delayInMilliseconds)
+
+ loadingStatesUndoQueue.push(function () {
+ mayProcessUndoCallback(targetElt, function () { clearTimeout(timeout) })
+ })
+ } else {
+ doCallback()
+ loadingStatesUndoQueue.push(function () {
+ mayProcessUndoCallback(targetElt, undoCallback)
+ })
+ }
+ }
+
+ function getLoadingStateElts(loadingScope, type, path) {
+ return Array.from(htmx.findAll(loadingScope, "[" + type + "]")).filter(
+ function (elt) { return mayProcessLoadingStateByPath(elt, path) }
+ )
+ }
+
+ function getLoadingTarget(elt) {
+ if (elt.getAttribute('data-loading-target')) {
+ return Array.from(
+ htmx.findAll(elt.getAttribute('data-loading-target'))
+ )
+ }
+ return [elt]
+ }
+
+ htmx.defineExtension('loading-states', {
+ onEvent: function (name, evt) {
+ if (name === 'htmx:beforeRequest') {
+ const container = loadingStateContainer(evt.target)
+
+ const loadingStateTypes = [
+ 'data-loading',
+ 'data-loading-class',
+ 'data-loading-class-remove',
+ 'data-loading-disable',
+ 'data-loading-aria-busy',
+ ]
+
+ let loadingStateEltsByType = {}
+
+ loadingStateTypes.forEach(function (type) {
+ loadingStateEltsByType[type] = getLoadingStateElts(
+ container,
+ type,
+ evt.detail.pathInfo.requestPath
+ )
+ })
+
+ loadingStateEltsByType['data-loading'].forEach(function (sourceElt) {
+ getLoadingTarget(sourceElt).forEach(function (targetElt) {
+ queueLoadingState(
+ sourceElt,
+ targetElt,
+ function () {
+ targetElt.style.display =
+ sourceElt.getAttribute('data-loading') ||
+ 'inline-block' },
+ function () { targetElt.style.display = 'none' }
+ )
+ })
+ })
+
+ loadingStateEltsByType['data-loading-class'].forEach(
+ function (sourceElt) {
+ const classNames = sourceElt
+ .getAttribute('data-loading-class')
+ .split(' ')
+
+ getLoadingTarget(sourceElt).forEach(function (targetElt) {
+ queueLoadingState(
+ sourceElt,
+ targetElt,
+ function () {
+ classNames.forEach(function (className) {
+ targetElt.classList.add(className)
+ })
+ },
+ function() {
+ classNames.forEach(function (className) {
+ targetElt.classList.remove(className)
+ })
+ }
+ )
+ })
+ }
+ )
+
+ loadingStateEltsByType['data-loading-class-remove'].forEach(
+ function (sourceElt) {
+ const classNames = sourceElt
+ .getAttribute('data-loading-class-remove')
+ .split(' ')
+
+ getLoadingTarget(sourceElt).forEach(function (targetElt) {
+ queueLoadingState(
+ sourceElt,
+ targetElt,
+ function () {
+ classNames.forEach(function (className) {
+ targetElt.classList.remove(className)
+ })
+ },
+ function() {
+ classNames.forEach(function (className) {
+ targetElt.classList.add(className)
+ })
+ }
+ )
+ })
+ }
+ )
+
+ loadingStateEltsByType['data-loading-disable'].forEach(
+ function (sourceElt) {
+ getLoadingTarget(sourceElt).forEach(function (targetElt) {
+ queueLoadingState(
+ sourceElt,
+ targetElt,
+ function() { targetElt.disabled = true },
+ function() { targetElt.disabled = false }
+ )
+ })
+ }
+ )
+
+ loadingStateEltsByType['data-loading-aria-busy'].forEach(
+ function (sourceElt) {
+ getLoadingTarget(sourceElt).forEach(function (targetElt) {
+ queueLoadingState(
+ sourceElt,
+ targetElt,
+ function () { targetElt.setAttribute("aria-busy", "true") },
+ function () { targetElt.removeAttribute("aria-busy") }
+ )
+ })
+ }
+ )
+ }
+
+ if (name === 'htmx:beforeOnLoad') {
+ while (loadingStatesUndoQueue.length > 0) {
+ loadingStatesUndoQueue.shift()()
+ }
+ }
+ },
+ })
+})()
diff --git a/static/js/ext/method-override.js b/static/js/ext/method-override.js
new file mode 100644
index 0000000..44621cd
--- /dev/null
+++ b/static/js/ext/method-override.js
@@ -0,0 +1,15 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+htmx.defineExtension('method-override', {
+ onEvent: function (name, evt) {
+ if (name === "htmx:configRequest") {
+ var method = evt.detail.verb;
+ if (method !== "get" || method !== "post") {
+ evt.detail.headers['X-HTTP-Method-Override'] = method.toUpperCase();
+ evt.detail.verb = "post";
+ }
+ }
+ }
+});
diff --git a/static/js/ext/morphdom-swap.js b/static/js/ext/morphdom-swap.js
new file mode 100644
index 0000000..337a604
--- /dev/null
+++ b/static/js/ext/morphdom-swap.js
@@ -0,0 +1,21 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+htmx.defineExtension('morphdom-swap', {
+ isInlineSwap: function(swapStyle) {
+ return swapStyle === 'morphdom';
+ },
+ handleSwap: function (swapStyle, target, fragment) {
+ if (swapStyle === 'morphdom') {
+ if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
+ // IE11 doesn't support DocumentFragment.firstElementChild
+ morphdom(target, fragment.firstElementChild || fragment.firstChild);
+ return [target];
+ } else {
+ morphdom(target, fragment.outerHTML);
+ return [target];
+ }
+ }
+ }
+});
diff --git a/static/js/ext/multi-swap.js b/static/js/ext/multi-swap.js
new file mode 100644
index 0000000..0c882f8
--- /dev/null
+++ b/static/js/ext/multi-swap.js
@@ -0,0 +1,50 @@
+(function () {
+
+ if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+ }
+
+ /** @type {import("../htmx").HtmxInternalApi} */
+ var api;
+
+ htmx.defineExtension('multi-swap', {
+ init: function (apiRef) {
+ api = apiRef;
+ },
+ isInlineSwap: function (swapStyle) {
+ return swapStyle.indexOf('multi:') === 0;
+ },
+ handleSwap: function (swapStyle, target, fragment, settleInfo) {
+ if (swapStyle.indexOf('multi:') === 0) {
+ var selectorToSwapStyle = {};
+ var elements = swapStyle.replace(/^multi\s*:\s*/, '').split(/\s*,\s*/);
+
+ elements.map(function (element) {
+ var split = element.split(/\s*:\s*/);
+ var elementSelector = split[0];
+ var elementSwapStyle = typeof (split[1]) !== "undefined" ? split[1] : "innerHTML";
+
+ if (elementSelector.charAt(0) !== '#') {
+ console.error("HTMX multi-swap: unsupported selector '" + elementSelector + "'. Only ID selectors starting with '#' are supported.");
+ return;
+ }
+
+ selectorToSwapStyle[elementSelector] = elementSwapStyle;
+ });
+
+ for (var selector in selectorToSwapStyle) {
+ var swapStyle = selectorToSwapStyle[selector];
+ var elementToSwap = fragment.querySelector(selector);
+ if (elementToSwap) {
+ api.oobSwap(swapStyle, elementToSwap, settleInfo);
+ } else {
+ console.warn("HTMX multi-swap: selector '" + selector + "' not found in source content.");
+ }
+ }
+
+ return true;
+ }
+ }
+ });
+})();
diff --git a/static/js/ext/path-deps.js b/static/js/ext/path-deps.js
new file mode 100644
index 0000000..8460e83
--- /dev/null
+++ b/static/js/ext/path-deps.js
@@ -0,0 +1,63 @@
+(function(undefined){
+ 'use strict';
+ if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+ }
+ // Save a reference to the global object (window in the browser)
+ var _root = this;
+
+ function dependsOn(pathSpec, url) {
+ if (pathSpec === "ignore") {
+ return false;
+ }
+ var dependencyPath = pathSpec.split("/");
+ var urlPath = url.split("/");
+ for (var i = 0; i < urlPath.length; i++) {
+ var dependencyElement = dependencyPath.shift();
+ var pathElement = urlPath[i];
+ if (dependencyElement !== pathElement && dependencyElement !== "*") {
+ return false;
+ }
+ if (dependencyPath.length === 0 || (dependencyPath.length === 1 && dependencyPath[0] === "")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ function refreshPath(path) {
+ var eltsWithDeps = htmx.findAll("[path-deps]");
+ for (var i = 0; i < eltsWithDeps.length; i++) {
+ var elt = eltsWithDeps[i];
+ if (dependsOn(elt.getAttribute('path-deps'), path)) {
+ htmx.trigger(elt, "path-deps");
+ }
+ }
+ }
+
+ htmx.defineExtension('path-deps', {
+ onEvent: function (name, evt) {
+ if (name === "htmx:beforeOnLoad") {
+ var config = evt.detail.requestConfig;
+ // mutating call
+ if (config.verb !== "get" && evt.target.getAttribute('path-deps') !== 'ignore') {
+ refreshPath(config.path);
+ }
+ }
+ }
+ });
+
+ /**
+ * ********************
+ * Expose functionality
+ * ********************
+ */
+
+ _root.PathDeps = {
+ refresh: function(path) {
+ refreshPath(path);
+ }
+ };
+
+}).call(this);
diff --git a/static/js/ext/path-params.js b/static/js/ext/path-params.js
new file mode 100644
index 0000000..462be0a
--- /dev/null
+++ b/static/js/ext/path-params.js
@@ -0,0 +1,15 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+htmx.defineExtension('path-params', {
+ onEvent: function(name, evt) {
+ if (name === "htmx:configRequest") {
+ evt.detail.path = evt.detail.path.replace(/{([^}]+)}/g, function (_, param) {
+ var val = evt.detail.parameters[param];
+ delete evt.detail.parameters[param];
+ return val === undefined ? "{" + param + "}" : encodeURIComponent(val);
+ })
+ }
+ }
+});
diff --git a/static/js/ext/preload.js b/static/js/ext/preload.js
new file mode 100644
index 0000000..ac3ef5c
--- /dev/null
+++ b/static/js/ext/preload.js
@@ -0,0 +1,151 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+// This adds the "preload" extension to htmx. By default, this will
+// preload the targets of any tags with `href` or `hx-get` attributes
+// if they also have a `preload` attribute as well. See documentation
+// for more details
+htmx.defineExtension("preload", {
+
+ onEvent: function(name, event) {
+
+ // Only take actions on "htmx:afterProcessNode"
+ if (name !== "htmx:afterProcessNode") {
+ return;
+ }
+
+ // SOME HELPER FUNCTIONS WE'LL NEED ALONG THE WAY
+
+ // attr gets the closest non-empty value from the attribute.
+ var attr = function(node, property) {
+ if (node == undefined) {return undefined;}
+ return node.getAttribute(property) || node.getAttribute("data-" + property) || attr(node.parentElement, property)
+ }
+
+ // load handles the actual HTTP fetch, and uses htmx.ajax in cases where we're
+ // preloading an htmx resource (this sends the same HTTP headers as a regular htmx request)
+ var load = function(node) {
+
+ // Called after a successful AJAX request, to mark the
+ // content as loaded (and prevent additional AJAX calls.)
+ var done = function(html) {
+ if (!node.preloadAlways) {
+ node.preloadState = "DONE"
+ }
+
+ if (attr(node, "preload-images") == "true") {
+ document.createElement("div").innerHTML = html // create and populate a node to load linked resources, too.
+ }
+ }
+
+ return function() {
+
+ // If this value has already been loaded, then do not try again.
+ if (node.preloadState !== "READY") {
+ return;
+ }
+
+ // Special handling for HX-GET - use built-in htmx.ajax function
+ // so that headers match other htmx requests, then set
+ // node.preloadState = TRUE so that requests are not duplicated
+ // in the future
+ var hxGet = node.getAttribute("hx-get") || node.getAttribute("data-hx-get")
+ if (hxGet) {
+ htmx.ajax("GET", hxGet, {
+ source: node,
+ handler:function(elt, info) {
+ done(info.xhr.responseText);
+ }
+ });
+ return;
+ }
+
+ // Otherwise, perform a standard xhr request, then set
+ // node.preloadState = TRUE so that requests are not duplicated
+ // in the future.
+ if (node.getAttribute("href")) {
+ var r = new XMLHttpRequest();
+ r.open("GET", node.getAttribute("href"));
+ r.onload = function() {done(r.responseText);};
+ r.send();
+ return;
+ }
+ }
+ }
+
+ // This function processes a specific node and sets up event handlers.
+ // We'll search for nodes and use it below.
+ var init = function(node) {
+
+ // If this node DOES NOT include a "GET" transaction, then there's nothing to do here.
+ if (node.getAttribute("href") + node.getAttribute("hx-get") + node.getAttribute("data-hx-get") == "") {
+ return;
+ }
+
+ // Guarantee that we only initialize each node once.
+ if (node.preloadState !== undefined) {
+ return;
+ }
+
+ // Get event name from config.
+ var on = attr(node, "preload") || "mousedown"
+ const always = on.indexOf("always") !== -1
+ if (always) {
+ on = on.replace('always', '').trim()
+ }
+
+ // FALL THROUGH to here means we need to add an EventListener
+
+ // Apply the listener to the node
+ node.addEventListener(on, function(evt) {
+ if (node.preloadState === "PAUSE") { // Only add one event listener
+ node.preloadState = "READY"; // Required for the `load` function to trigger
+
+ // Special handling for "mouseover" events. Wait 100ms before triggering load.
+ if (on === "mouseover") {
+ window.setTimeout(load(node), 100);
+ } else {
+ load(node)() // all other events trigger immediately.
+ }
+ }
+ })
+
+ // Special handling for certain built-in event handlers
+ switch (on) {
+
+ case "mouseover":
+ // Mirror `touchstart` events (fires immediately)
+ node.addEventListener("touchstart", load(node));
+
+ // WHhen the mouse leaves, immediately disable the preload
+ node.addEventListener("mouseout", function(evt) {
+ if ((evt.target === node) && (node.preloadState === "READY")) {
+ node.preloadState = "PAUSE";
+ }
+ })
+ break;
+
+ case "mousedown":
+ // Mirror `touchstart` events (fires immediately)
+ node.addEventListener("touchstart", load(node));
+ break;
+ }
+
+ // Mark the node as ready to run.
+ node.preloadState = "PAUSE";
+ node.preloadAlways = always;
+ htmx.trigger(node, "preload:init") // This event can be used to load content immediately.
+ }
+
+ // Search for all child nodes that have a "preload" attribute
+ event.target.querySelectorAll("[preload]").forEach(function(node) {
+
+ // Initialize the node with the "preload" attribute
+ init(node)
+
+ // Initialize all child elements that are anchors or have `hx-get` (use with care)
+ node.querySelectorAll("a,[hx-get],[data-hx-get]").forEach(init)
+ })
+ }
+})
diff --git a/static/js/ext/rails-method.js b/static/js/ext/rails-method.js
new file mode 100644
index 0000000..632d86c
--- /dev/null
+++ b/static/js/ext/rails-method.js
@@ -0,0 +1,14 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+htmx.defineExtension('rails-method', {
+ onEvent: function (name, evt) {
+ if (name === "configRequest.htmx") {
+ var methodOverride = evt.detail.headers['X-HTTP-Method-Override'];
+ if (methodOverride) {
+ evt.detail.parameters['_method'] = methodOverride;
+ }
+ }
+ }
+});
diff --git a/static/js/ext/remove-me.js b/static/js/ext/remove-me.js
new file mode 100644
index 0000000..21cd788
--- /dev/null
+++ b/static/js/ext/remove-me.js
@@ -0,0 +1,31 @@
+(function(){
+ if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+ }
+ function maybeRemoveMe(elt) {
+ var timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
+ if (timing) {
+ setTimeout(function () {
+ elt.parentElement.removeChild(elt);
+ }, htmx.parseInterval(timing));
+ }
+ }
+
+ htmx.defineExtension('remove-me', {
+ onEvent: function (name, evt) {
+ if (name === "htmx:afterProcessNode") {
+ var elt = evt.detail.elt;
+ if (elt.getAttribute) {
+ maybeRemoveMe(elt);
+ if (elt.querySelectorAll) {
+ var children = elt.querySelectorAll("[remove-me], [data-remove-me]");
+ for (var i = 0; i < children.length; i++) {
+ maybeRemoveMe(children[i]);
+ }
+ }
+ }
+ }
+ }
+ });
+})();
diff --git a/static/js/ext/response-targets.js b/static/js/ext/response-targets.js
new file mode 100644
index 0000000..021ec0d
--- /dev/null
+++ b/static/js/ext/response-targets.js
@@ -0,0 +1,135 @@
+(function(){
+
+ if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+ }
+
+ /** @type {import("../htmx").HtmxInternalApi} */
+ var api;
+
+ var attrPrefix = 'hx-target-';
+
+ // IE11 doesn't support string.startsWith
+ function startsWith(str, prefix) {
+ return str.substring(0, prefix.length) === prefix
+ }
+
+ /**
+ * @param {HTMLElement} elt
+ * @param {number} respCode
+ * @returns {HTMLElement | null}
+ */
+ function getRespCodeTarget(elt, respCodeNumber) {
+ if (!elt || !respCodeNumber) return null;
+
+ var respCode = respCodeNumber.toString();
+
+ // '*' is the original syntax, as the obvious character for a wildcard.
+ // The 'x' alternative was added for maximum compatibility with HTML
+ // templating engines, due to ambiguity around which characters are
+ // supported in HTML attributes.
+ //
+ // Start with the most specific possible attribute and generalize from
+ // there.
+ var attrPossibilities = [
+ respCode,
+
+ respCode.substr(0, 2) + '*',
+ respCode.substr(0, 2) + 'x',
+
+ respCode.substr(0, 1) + '*',
+ respCode.substr(0, 1) + 'x',
+ respCode.substr(0, 1) + '**',
+ respCode.substr(0, 1) + 'xx',
+
+ '*',
+ 'x',
+ '***',
+ 'xxx',
+ ];
+ if (startsWith(respCode, '4') || startsWith(respCode, '5')) {
+ attrPossibilities.push('error');
+ }
+
+ for (var i = 0; i < attrPossibilities.length; i++) {
+ var attr = attrPrefix + attrPossibilities[i];
+ var attrValue = api.getClosestAttributeValue(elt, attr);
+ if (attrValue) {
+ if (attrValue === "this") {
+ return api.findThisElement(elt, attr);
+ } else {
+ return api.querySelectorExt(elt, attrValue);
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** @param {Event} evt */
+ function handleErrorFlag(evt) {
+ if (evt.detail.isError) {
+ if (htmx.config.responseTargetUnsetsError) {
+ evt.detail.isError = false;
+ }
+ } else if (htmx.config.responseTargetSetsError) {
+ evt.detail.isError = true;
+ }
+ }
+
+ htmx.defineExtension('response-targets', {
+
+ /** @param {import("../htmx").HtmxInternalApi} apiRef */
+ init: function (apiRef) {
+ api = apiRef;
+
+ if (htmx.config.responseTargetUnsetsError === undefined) {
+ htmx.config.responseTargetUnsetsError = true;
+ }
+ if (htmx.config.responseTargetSetsError === undefined) {
+ htmx.config.responseTargetSetsError = false;
+ }
+ if (htmx.config.responseTargetPrefersExisting === undefined) {
+ htmx.config.responseTargetPrefersExisting = false;
+ }
+ if (htmx.config.responseTargetPrefersRetargetHeader === undefined) {
+ htmx.config.responseTargetPrefersRetargetHeader = true;
+ }
+ },
+
+ /**
+ * @param {string} name
+ * @param {Event} evt
+ */
+ onEvent: function (name, evt) {
+ if (name === "htmx:beforeSwap" &&
+ evt.detail.xhr &&
+ evt.detail.xhr.status !== 200) {
+ if (evt.detail.target) {
+ if (htmx.config.responseTargetPrefersExisting) {
+ evt.detail.shouldSwap = true;
+ handleErrorFlag(evt);
+ return true;
+ }
+ if (htmx.config.responseTargetPrefersRetargetHeader &&
+ evt.detail.xhr.getAllResponseHeaders().match(/HX-Retarget:/i)) {
+ evt.detail.shouldSwap = true;
+ handleErrorFlag(evt);
+ return true;
+ }
+ }
+ if (!evt.detail.requestConfig) {
+ return true;
+ }
+ var target = getRespCodeTarget(evt.detail.requestConfig.elt, evt.detail.xhr.status);
+ if (target) {
+ handleErrorFlag(evt);
+ evt.detail.shouldSwap = true;
+ evt.detail.target = target;
+ }
+ return true;
+ }
+ }
+ });
+})();
diff --git a/static/js/ext/restored.js b/static/js/ext/restored.js
new file mode 100644
index 0000000..a314b07
--- /dev/null
+++ b/static/js/ext/restored.js
@@ -0,0 +1,19 @@
+if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+}
+htmx.defineExtension('restored', {
+ onEvent : function(name, evt) {
+ if (name === 'htmx:restored'){
+ var restoredElts = evt.detail.document.querySelectorAll(
+ "[hx-trigger='restored'],[data-hx-trigger='restored']"
+ );
+ // need a better way to do this, would prefer to just trigger from evt.detail.elt
+ var foundElt = Array.from(restoredElts).find(
+ (x) => (x.outerHTML === evt.detail.elt.outerHTML)
+ );
+ var restoredEvent = evt.detail.triggerEvent(foundElt, 'restored');
+ }
+ return;
+ }
+})
diff --git a/static/js/ext/sse.js b/static/js/ext/sse.js
new file mode 100644
index 0000000..16eee54
--- /dev/null
+++ b/static/js/ext/sse.js
@@ -0,0 +1,374 @@
+/*
+Server Sent Events Extension
+============================
+This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
+
+*/
+
+(function() {
+
+ if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+ }
+
+ /** @type {import("../htmx").HtmxInternalApi} */
+ var api;
+
+ htmx.defineExtension("sse", {
+
+ /**
+ * Init saves the provided reference to the internal HTMX API.
+ *
+ * @param {import("../htmx").HtmxInternalApi} api
+ * @returns void
+ */
+ init: function(apiRef) {
+ // store a reference to the internal API.
+ api = apiRef;
+
+ // set a function in the public API for creating new EventSource objects
+ if (htmx.createEventSource == undefined) {
+ htmx.createEventSource = createEventSource;
+ }
+ },
+
+ /**
+ * onEvent handles all events passed to this extension.
+ *
+ * @param {string} name
+ * @param {Event} evt
+ * @returns void
+ */
+ onEvent: function(name, evt) {
+
+ var parent = evt.target || evt.detail.elt;
+ switch (name) {
+
+ case "htmx:beforeCleanupElement":
+ var internalData = api.getInternalData(parent)
+ // Try to remove remove an EventSource when elements are removed
+ if (internalData.sseEventSource) {
+ internalData.sseEventSource.close();
+ }
+
+ return;
+
+ // Try to create EventSources when elements are processed
+ case "htmx:afterProcessNode":
+ ensureEventSourceOnElement(parent);
+ }
+ }
+ });
+
+ ///////////////////////////////////////////////
+ // HELPER FUNCTIONS
+ ///////////////////////////////////////////////
+
+
+ /**
+ * createEventSource is the default method for creating new EventSource objects.
+ * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
+ *
+ * @param {string} url
+ * @returns EventSource
+ */
+ function createEventSource(url) {
+ return new EventSource(url, { withCredentials: true });
+ }
+
+ function splitOnWhitespace(trigger) {
+ return trigger.trim().split(/\s+/);
+ }
+
+ function getLegacySSEURL(elt) {
+ var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
+ if (legacySSEValue) {
+ var values = splitOnWhitespace(legacySSEValue);
+ for (var i = 0; i < values.length; i++) {
+ var value = values[i].split(/:(.+)/);
+ if (value[0] === "connect") {
+ return value[1];
+ }
+ }
+ }
+ }
+
+ function getLegacySSESwaps(elt) {
+ var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
+ var returnArr = [];
+ if (legacySSEValue != null) {
+ var values = splitOnWhitespace(legacySSEValue);
+ for (var i = 0; i < values.length; i++) {
+ var value = values[i].split(/:(.+)/);
+ if (value[0] === "swap") {
+ returnArr.push(value[1]);
+ }
+ }
+ }
+ return returnArr;
+ }
+
+ /**
+ * registerSSE looks for attributes that can contain sse events, right
+ * now hx-trigger and sse-swap and adds listeners based on these attributes too
+ * the closest event source
+ *
+ * @param {HTMLElement} elt
+ */
+ function registerSSE(elt) {
+ // Add message handlers for every `sse-swap` attribute
+ queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function (child) {
+ // Find closest existing event source
+ var sourceElement = api.getClosestMatch(child, hasEventSource);
+ if (sourceElement == null) {
+ // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
+ return null; // no eventsource in parentage, orphaned element
+ }
+
+ // Set internalData and source
+ var internalData = api.getInternalData(sourceElement);
+ var source = internalData.sseEventSource;
+
+ var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
+ if (sseSwapAttr) {
+ var sseEventNames = sseSwapAttr.split(",");
+ } else {
+ var sseEventNames = getLegacySSESwaps(child);
+ }
+
+ for (var i = 0; i < sseEventNames.length; i++) {
+ var sseEventName = sseEventNames[i].trim();
+ var listener = function(event) {
+
+ // If the source is missing then close SSE
+ if (maybeCloseSSESource(sourceElement)) {
+ return;
+ }
+
+ // If the body no longer contains the element, remove the listener
+ if (!api.bodyContains(child)) {
+ source.removeEventListener(sseEventName, listener);
+ return;
+ }
+
+ // swap the response into the DOM and trigger a notification
+ if(!api.triggerEvent(elt, "htmx:sseBeforeMessage", event)) {
+ return;
+ }
+ swap(child, event.data);
+ api.triggerEvent(elt, "htmx:sseMessage", event);
+ };
+
+ // Register the new listener
+ api.getInternalData(child).sseEventListener = listener;
+ source.addEventListener(sseEventName, listener);
+ }
+ });
+
+ // Add message handlers for every `hx-trigger="sse:*"` attribute
+ queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
+ // Find closest existing event source
+ var sourceElement = api.getClosestMatch(child, hasEventSource);
+ if (sourceElement == null) {
+ // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
+ return null; // no eventsource in parentage, orphaned element
+ }
+
+ // Set internalData and source
+ var internalData = api.getInternalData(sourceElement);
+ var source = internalData.sseEventSource;
+
+ var sseEventName = api.getAttributeValue(child, "hx-trigger");
+ if (sseEventName == null) {
+ return;
+ }
+
+ // Only process hx-triggers for events with the "sse:" prefix
+ if (sseEventName.slice(0, 4) != "sse:") {
+ return;
+ }
+
+ // remove the sse: prefix from here on out
+ sseEventName = sseEventName.substr(4);
+
+ var listener = function() {
+ if (maybeCloseSSESource(sourceElement)) {
+ return
+ }
+
+ if (!api.bodyContains(child)) {
+ source.removeEventListener(sseEventName, listener);
+ }
+ }
+ });
+ }
+
+ /**
+ * ensureEventSourceOnElement creates a new EventSource connection on the provided element.
+ * If a usable EventSource already exists, then it is returned. If not, then a new EventSource
+ * is created and stored in the element's internalData.
+ * @param {HTMLElement} elt
+ * @param {number} retryCount
+ * @returns {EventSource | null}
+ */
+ function ensureEventSourceOnElement(elt, retryCount) {
+
+ if (elt == null) {
+ return null;
+ }
+
+ // handle extension source creation attribute
+ queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
+ var sseURL = api.getAttributeValue(child, "sse-connect");
+ if (sseURL == null) {
+ return;
+ }
+
+ ensureEventSource(child, sseURL, retryCount);
+ });
+
+ // handle legacy sse, remove for HTMX2
+ queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
+ var sseURL = getLegacySSEURL(child);
+ if (sseURL == null) {
+ return;
+ }
+
+ ensureEventSource(child, sseURL, retryCount);
+ });
+
+ registerSSE(elt);
+ }
+
+ function ensureEventSource(elt, url, retryCount) {
+ var source = htmx.createEventSource(url);
+
+ source.onerror = function(err) {
+
+ // Log an error event
+ api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
+
+ // If parent no longer exists in the document, then clean up this EventSource
+ if (maybeCloseSSESource(elt)) {
+ return;
+ }
+
+ // Otherwise, try to reconnect the EventSource
+ if (source.readyState === EventSource.CLOSED) {
+ retryCount = retryCount || 0;
+ var timeout = Math.random() * (2 ^ retryCount) * 500;
+ window.setTimeout(function() {
+ ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
+ }, timeout);
+ }
+ };
+
+ source.onopen = function(evt) {
+ api.triggerEvent(elt, "htmx:sseOpen", { source: source });
+ }
+
+ api.getInternalData(elt).sseEventSource = source;
+ }
+
+ /**
+ * maybeCloseSSESource confirms that the parent element still exists.
+ * If not, then any associated SSE source is closed and the function returns true.
+ *
+ * @param {HTMLElement} elt
+ * @returns boolean
+ */
+ function maybeCloseSSESource(elt) {
+ if (!api.bodyContains(elt)) {
+ var source = api.getInternalData(elt).sseEventSource;
+ if (source != undefined) {
+ source.close();
+ // source = null
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
+ *
+ * @param {HTMLElement} elt
+ * @param {string} attributeName
+ */
+ function queryAttributeOnThisOrChildren(elt, attributeName) {
+
+ var result = [];
+
+ // If the parent element also contains the requested attribute, then add it to the results too.
+ if (api.hasAttribute(elt, attributeName)) {
+ result.push(elt);
+ }
+
+ // Search all child nodes that match the requested attribute
+ elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
+ result.push(node);
+ });
+
+ return result;
+ }
+
+ /**
+ * @param {HTMLElement} elt
+ * @param {string} content
+ */
+ function swap(elt, content) {
+
+ api.withExtensions(elt, function(extension) {
+ content = extension.transformResponse(content, null, elt);
+ });
+
+ var swapSpec = api.getSwapSpecification(elt);
+ var target = api.getTarget(elt);
+ var settleInfo = api.makeSettleInfo(elt);
+
+ api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
+
+ settleInfo.elts.forEach(function(elt) {
+ if (elt.classList) {
+ elt.classList.add(htmx.config.settlingClass);
+ }
+ api.triggerEvent(elt, 'htmx:beforeSettle');
+ });
+
+ // Handle settle tasks (with delay if requested)
+ if (swapSpec.settleDelay > 0) {
+ setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
+ } else {
+ doSettle(settleInfo)();
+ }
+ }
+
+ /**
+ * doSettle mirrors much of the functionality in htmx that
+ * settles elements after their content has been swapped.
+ * TODO: this should be published by htmx, and not duplicated here
+ * @param {import("../htmx").HtmxSettleInfo} settleInfo
+ * @returns () => void
+ */
+ function doSettle(settleInfo) {
+
+ return function() {
+ settleInfo.tasks.forEach(function(task) {
+ task.call();
+ });
+
+ settleInfo.elts.forEach(function(elt) {
+ if (elt.classList) {
+ elt.classList.remove(htmx.config.settlingClass);
+ }
+ api.triggerEvent(elt, 'htmx:afterSettle');
+ });
+ }
+ }
+
+ function hasEventSource(node) {
+ return api.getInternalData(node).sseEventSource != null;
+ }
+
+})();
diff --git a/static/js/ext/ws.js b/static/js/ext/ws.js
new file mode 100644
index 0000000..3d7e44d
--- /dev/null
+++ b/static/js/ext/ws.js
@@ -0,0 +1,481 @@
+/*
+WebSockets Extension
+============================
+This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
+*/
+
+(function () {
+
+ if (htmx.version && !htmx.version.startsWith("1.")) {
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
+ ". It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
+ }
+
+ /** @type {import("../htmx").HtmxInternalApi} */
+ var api;
+
+ htmx.defineExtension("ws", {
+
+ /**
+ * init is called once, when this extension is first registered.
+ * @param {import("../htmx").HtmxInternalApi} apiRef
+ */
+ init: function (apiRef) {
+
+ // Store reference to internal API
+ api = apiRef;
+
+ // Default function for creating new EventSource objects
+ if (!htmx.createWebSocket) {
+ htmx.createWebSocket = createWebSocket;
+ }
+
+ // Default setting for reconnect delay
+ if (!htmx.config.wsReconnectDelay) {
+ htmx.config.wsReconnectDelay = "full-jitter";
+ }
+ },
+
+ /**
+ * onEvent handles all events passed to this extension.
+ *
+ * @param {string} name
+ * @param {Event} evt
+ */
+ onEvent: function (name, evt) {
+ var parent = evt.target || evt.detail.elt;
+
+ switch (name) {
+
+ // Try to close the socket when elements are removed
+ case "htmx:beforeCleanupElement":
+
+ var internalData = api.getInternalData(parent)
+
+ if (internalData.webSocket) {
+ internalData.webSocket.close();
+ }
+ return;
+
+ // Try to create websockets when elements are processed
+ case "htmx:beforeProcessNode":
+ forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
+ ensureWebSocket(child)
+ });
+ forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
+ ensureWebSocketSend(child)
+ });
+ }
+ }
+ });
+
+ function splitOnWhitespace(trigger) {
+ return trigger.trim().split(/\s+/);
+ }
+
+ function getLegacyWebsocketURL(elt) {
+ var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
+ if (legacySSEValue) {
+ var values = splitOnWhitespace(legacySSEValue);
+ for (var i = 0; i < values.length; i++) {
+ var value = values[i].split(/:(.+)/);
+ if (value[0] === "connect") {
+ return value[1];
+ }
+ }
+ }
+ }
+
+ /**
+ * ensureWebSocket creates a new WebSocket on the designated element, using
+ * the element's "ws-connect" attribute.
+ * @param {HTMLElement} socketElt
+ * @returns
+ */
+ function ensureWebSocket(socketElt) {
+
+ // If the element containing the WebSocket connection no longer exists, then
+ // do not connect/reconnect the WebSocket.
+ if (!api.bodyContains(socketElt)) {
+ return;
+ }
+
+ // Get the source straight from the element's value
+ var wssSource = api.getAttributeValue(socketElt, "ws-connect")
+
+ if (wssSource == null || wssSource === "") {
+ var legacySource = getLegacyWebsocketURL(socketElt);
+ if (legacySource == null) {
+ return;
+ } else {
+ wssSource = legacySource;
+ }
+ }
+
+ // Guarantee that the wssSource value is a fully qualified URL
+ if (wssSource.indexOf("/") === 0) {
+ 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 socketWrapper = createWebsocketWrapper(socketElt, function () {
+ return htmx.createWebSocket(wssSource)
+ });
+
+ socketWrapper.addEventListener('message', function (event) {
+ if (maybeCloseWebSocketSource(socketElt)) {
+ return;
+ }
+
+ var response = event.data;
+ if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
+ message: response,
+ socketWrapper: socketWrapper.publicInterface
+ })) {
+ return;
+ }
+
+ api.withExtensions(socketElt, function (extension) {
+ response = extension.transformResponse(response, null, socketElt);
+ });
+
+ var settleInfo = api.makeSettleInfo(socketElt);
+ var fragment = api.makeFragment(response);
+
+ if (fragment.children.length) {
+ var children = Array.from(fragment.children);
+ for (var i = 0; i < children.length; i++) {
+ api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
+ }
+ }
+
+ api.settleImmediately(settleInfo.tasks);
+ api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
+ });
+
+ // Put the WebSocket into the HTML Element's custom data.
+ api.getInternalData(socketElt).webSocket = socketWrapper;
+ }
+
+ /**
+ * @typedef {Object} WebSocketWrapper
+ * @property {WebSocket} socket
+ * @property {Array<{message: string, sendElt: Element}>} messageQueue
+ * @property {number} retryCount
+ * @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
+ * @property {(message: string, sendElt: Element) => void} send
+ * @property {(event: string, handler: Function) => void} addEventListener
+ * @property {() => void} handleQueuedMessages
+ * @property {() => void} init
+ * @property {() => void} close
+ */
+ /**
+ *
+ * @param socketElt
+ * @param socketFunc
+ * @returns {WebSocketWrapper}
+ */
+ function createWebsocketWrapper(socketElt, socketFunc) {
+ var wrapper = {
+ socket: null,
+ messageQueue: [],
+ retryCount: 0,
+
+ /** @type {Object<string, Function[]>} */
+ events: {},
+
+ addEventListener: function (event, handler) {
+ if (this.socket) {
+ this.socket.addEventListener(event, handler);
+ }
+
+ if (!this.events[event]) {
+ this.events[event] = [];
+ }
+
+ this.events[event].push(handler);
+ },
+
+ sendImmediately: function (message, sendElt) {
+ if (!this.socket) {
+ api.triggerErrorEvent()
+ }
+ if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
+ message: message,
+ socketWrapper: this.publicInterface
+ })) {
+ this.socket.send(message);
+ sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
+ message: message,
+ socketWrapper: this.publicInterface
+ })
+ }
+ },
+
+ send: function (message, sendElt) {
+ if (this.socket.readyState !== this.socket.OPEN) {
+ this.messageQueue.push({ message: message, sendElt: sendElt });
+ } else {
+ this.sendImmediately(message, sendElt);
+ }
+ },
+
+ handleQueuedMessages: function () {
+ while (this.messageQueue.length > 0) {
+ var queuedItem = this.messageQueue[0]
+ if (this.socket.readyState === this.socket.OPEN) {
+ this.sendImmediately(queuedItem.message, queuedItem.sendElt);
+ this.messageQueue.shift();
+ } else {
+ break;
+ }
+ }
+ },
+
+ init: function () {
+ if (this.socket && this.socket.readyState === this.socket.OPEN) {
+ // Close discarded socket
+ this.socket.close()
+ }
+
+ // Create a new WebSocket and event handlers
+ /** @type {WebSocket} */
+ var socket = socketFunc();
+
+ // The event.type detail is added for interface conformance with the
+ // other two lifecycle events (open and close) so a single handler method
+ // can handle them polymorphically, if required.
+ api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
+
+ this.socket = socket;
+
+ socket.onopen = function (e) {
+ wrapper.retryCount = 0;
+ api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
+ wrapper.handleQueuedMessages();
+ }
+
+ socket.onclose = function (e) {
+ // If socket should not be connected, stop further attempts to establish connection
+ // If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
+ if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
+ var delay = getWebSocketReconnectDelay(wrapper.retryCount);
+ setTimeout(function () {
+ wrapper.retryCount += 1;
+ wrapper.init();
+ }, delay);
+ }
+
+ // Notify client code that connection has been closed. Client code can inspect `event` field
+ // to determine whether closure has been valid or abnormal
+ api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
+ };
+
+ socket.onerror = function (e) {
+ api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
+ maybeCloseWebSocketSource(socketElt);
+ };
+
+ var events = this.events;
+ Object.keys(events).forEach(function (k) {
+ events[k].forEach(function (e) {
+ socket.addEventListener(k, e);
+ })
+ });
+ },
+
+ close: function () {
+ this.socket.close()
+ }
+ }
+
+ wrapper.init();
+
+ wrapper.publicInterface = {
+ send: wrapper.send.bind(wrapper),
+ sendImmediately: wrapper.sendImmediately.bind(wrapper),
+ queue: wrapper.messageQueue
+ };
+
+ return wrapper;
+ }
+
+ /**
+ * ensureWebSocketSend attaches trigger handles to elements with
+ * "ws-send" attribute
+ * @param {HTMLElement} elt
+ */
+ function ensureWebSocketSend(elt) {
+ var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
+ if (legacyAttribute && legacyAttribute !== 'send') {
+ return;
+ }
+
+ var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
+ processWebSocketSend(webSocketParent, elt);
+ }
+
+ /**
+ * hasWebSocket function checks if a node has webSocket instance attached
+ * @param {HTMLElement} node
+ * @returns {boolean}
+ */
+ function hasWebSocket(node) {
+ return api.getInternalData(node).webSocket != null;
+ }
+
+ /**
+ * processWebSocketSend adds event listeners to the <form> element so that
+ * messages can be sent to the WebSocket server when the form is submitted.
+ * @param {HTMLElement} socketElt
+ * @param {HTMLElement} sendElt
+ */
+ function processWebSocketSend(socketElt, sendElt) {
+ var nodeData = api.getInternalData(sendElt);
+ var triggerSpecs = api.getTriggerSpecs(sendElt);
+ triggerSpecs.forEach(function (ts) {
+ api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
+ if (maybeCloseWebSocketSource(socketElt)) {
+ return;
+ }
+
+ /** @type {WebSocketWrapper} */
+ var socketWrapper = api.getInternalData(socketElt).webSocket;
+ var headers = api.getHeaders(sendElt, api.getTarget(sendElt));
+ var results = api.getInputValues(sendElt, 'post');
+ var errors = results.errors;
+ var rawParameters = results.values;
+ var expressionVars = api.getExpressionVars(sendElt);
+ var allParameters = api.mergeObjects(rawParameters, expressionVars);
+ var filteredParameters = api.filterValues(allParameters, sendElt);
+
+ var sendConfig = {
+ parameters: filteredParameters,
+ unfilteredParameters: allParameters,
+ headers: headers,
+ errors: errors,
+
+ triggeringEvent: evt,
+ messageBody: undefined,
+ socketWrapper: socketWrapper.publicInterface
+ };
+
+ if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
+ return;
+ }
+
+ if (errors && errors.length > 0) {
+ api.triggerEvent(elt, 'htmx:validation:halted', errors);
+ return;
+ }
+
+ var body = sendConfig.messageBody;
+ if (body === undefined) {
+ var toSend = Object.assign({}, sendConfig.parameters);
+ if (sendConfig.headers)
+ toSend['HEADERS'] = headers;
+ body = JSON.stringify(toSend);
+ }
+
+ socketWrapper.send(body, elt);
+
+ if (evt && api.shouldCancel(evt, elt)) {
+ evt.preventDefault();
+ }
+ });
+ });
+ }
+
+ /**
+ * getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
+ * @param {number} retryCount // The number of retries that have already taken place
+ * @returns {number}
+ */
+ function getWebSocketReconnectDelay(retryCount) {
+
+ /** @type {"full-jitter" | ((retryCount:number) => number)} */
+ var delay = htmx.config.wsReconnectDelay;
+ if (typeof delay === 'function') {
+ 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"');
+ }
+
+ /**
+ * maybeCloseWebSocketSource checks to the if the element that created the WebSocket
+ * still exists in the DOM. If NOT, then the WebSocket is closed and this function
+ * returns TRUE. If the element DOES EXIST, then no action is taken, and this function
+ * returns FALSE.
+ *
+ * @param {*} elt
+ * @returns
+ */
+ function maybeCloseWebSocketSource(elt) {
+ if (!api.bodyContains(elt)) {
+ api.getInternalData(elt).webSocket.close();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * createWebSocket is the default method for creating new WebSocket objects.
+ * it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
+ *
+ * @param {string} url
+ * @returns WebSocket
+ */
+ function createWebSocket(url) {
+ var sock = new WebSocket(url, []);
+ sock.binaryType = htmx.config.wsBinaryType;
+ return sock;
+ }
+
+ /**
+ * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
+ *
+ * @param {HTMLElement} elt
+ * @param {string} attributeName
+ */
+ function queryAttributeOnThisOrChildren(elt, attributeName) {
+
+ var result = []
+
+ // If the parent element also contains the requested attribute, then add it to the results too.
+ if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
+ result.push(elt);
+ }
+
+ // Search all child nodes that match the requested attribute
+ elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
+ result.push(node)
+ })
+
+ return result
+ }
+
+ /**
+ * @template T
+ * @param {T[]} arr
+ * @param {(T) => void} func
+ */
+ function forEach(arr, func) {
+ if (arr) {
+ for (var i = 0; i < arr.length; i++) {
+ func(arr[i]);
+ }
+ }
+ }
+
+})();
+