Updated HTMX to v2
All checks were successful
Docker Deploy / build-and-push (push) Successful in 1m0s

This commit is contained in:
Atridad Lahiji 2025-01-13 23:54:01 -06:00
parent 237b2624b1
commit b6b7b58231
Signed by: atridad
SSH key fingerprint: SHA256:LGomp8Opq0jz+7kbwNcdfTcuaLRb5Nh0k5AchDDb438
2 changed files with 111 additions and 108 deletions

File diff suppressed because one or more lines are too long

217
public/js/htmx.sse.js vendored
View file

@ -5,30 +5,34 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/ */
(function() { (function () {
/** @type {import("../htmx").HtmxInternalApi} */ /** @type {import("../htmx").HtmxInternalApi} */
var api var api;
htmx.defineExtension('sse', {
htmx.defineExtension("sse", {
/** /**
* Init saves the provided reference to the internal HTMX API. * Init saves the provided reference to the internal HTMX API.
* *
* @param {import("../htmx").HtmxInternalApi} api * @param {import("../htmx").HtmxInternalApi} api
* @returns void * @returns void
*/ */
init: function(apiRef) { init: function (apiRef) {
// store a reference to the internal API. // store a reference to the internal API.
api = apiRef api = apiRef;
// set a function in the public API for creating new EventSource objects // set a function in the public API for creating new EventSource objects
if (htmx.createEventSource == undefined) { if (htmx.createEventSource == undefined) {
htmx.createEventSource = createEventSource htmx.createEventSource = createEventSource;
} }
}, },
getSelectors: function() { getSelectors: function () {
return ['[sse-connect]', '[data-sse-connect]', '[sse-swap]', '[data-sse-swap]'] return [
"[sse-connect]",
"[data-sse-connect]",
"[sse-swap]",
"[data-sse-swap]",
];
}, },
/** /**
@ -38,29 +42,29 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
* @param {Event} evt * @param {Event} evt
* @returns void * @returns void
*/ */
onEvent: function(name, evt) { onEvent: function (name, evt) {
var parent = evt.target || evt.detail.elt var parent = evt.target || evt.detail.elt;
switch (name) { switch (name) {
case 'htmx:beforeCleanupElement': case "htmx:beforeCleanupElement":
var internalData = api.getInternalData(parent) var internalData = api.getInternalData(parent);
// Try to remove remove an EventSource when elements are removed // Try to remove remove an EventSource when elements are removed
var source = internalData.sseEventSource var source = internalData.sseEventSource;
if (source) { if (source) {
api.triggerEvent(parent, 'htmx:sseClose', { api.triggerEvent(parent, "htmx:sseClose", {
source, source,
type: 'nodeReplaced', type: "nodeReplaced",
}) });
internalData.sseEventSource.close() internalData.sseEventSource.close();
} }
return return;
// Try to create EventSources when elements are processed // Try to create EventSources when elements are processed
case 'htmx:afterProcessNode': case "htmx:afterProcessNode":
ensureEventSourceOnElement(parent) ensureEventSourceOnElement(parent);
} }
} },
}) });
/// //////////////////////////////////////////// /// ////////////////////////////////////////////
// HELPER FUNCTIONS // HELPER FUNCTIONS
@ -74,7 +78,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
* @returns EventSource * @returns EventSource
*/ */
function createEventSource(url) { function createEventSource(url) {
return new EventSource(url, { withCredentials: true }) return new EventSource(url, { withCredentials: true });
} }
/** /**
@ -86,84 +90,84 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/ */
function registerSSE(elt) { function registerSSE(elt) {
// Add message handlers for every `sse-swap` attribute // Add message handlers for every `sse-swap` attribute
if (api.getAttributeValue(elt, 'sse-swap')) { if (api.getAttributeValue(elt, "sse-swap")) {
// Find closest existing event source // Find closest existing event source
var sourceElement = api.getClosestMatch(elt, hasEventSource) var sourceElement = api.getClosestMatch(elt, hasEventSource);
if (sourceElement == null) { if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError") // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null // no eventsource in parentage, orphaned element return null; // no eventsource in parentage, orphaned element
} }
// Set internalData and source // Set internalData and source
var internalData = api.getInternalData(sourceElement) var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource var source = internalData.sseEventSource;
var sseSwapAttr = api.getAttributeValue(elt, 'sse-swap') var sseSwapAttr = api.getAttributeValue(elt, "sse-swap");
var sseEventNames = sseSwapAttr.split(',') var sseEventNames = sseSwapAttr.split(",");
for (var i = 0; i < sseEventNames.length; i++) { for (var i = 0; i < sseEventNames.length; i++) {
const sseEventName = sseEventNames[i].trim() const sseEventName = sseEventNames[i].trim();
const listener = function(event) { const listener = function (event) {
// If the source is missing then close SSE // If the source is missing then close SSE
if (maybeCloseSSESource(sourceElement)) { if (maybeCloseSSESource(sourceElement)) {
return return;
} }
// If the body no longer contains the element, remove the listener // If the body no longer contains the element, remove the listener
if (!api.bodyContains(elt)) { if (!api.bodyContains(elt)) {
source.removeEventListener(sseEventName, listener) source.removeEventListener(sseEventName, listener);
return return;
} }
// swap the response into the DOM and trigger a notification // swap the response into the DOM and trigger a notification
if (!api.triggerEvent(elt, 'htmx:sseBeforeMessage', event)) { if (!api.triggerEvent(elt, "htmx:sseBeforeMessage", event)) {
return return;
} }
swap(elt, event.data) swap(elt, event.data);
api.triggerEvent(elt, 'htmx:sseMessage', event) api.triggerEvent(elt, "htmx:sseMessage", event);
} };
// Register the new listener // Register the new listener
api.getInternalData(elt).sseEventListener = listener api.getInternalData(elt).sseEventListener = listener;
source.addEventListener(sseEventName, listener) source.addEventListener(sseEventName, listener);
} }
} }
// Add message handlers for every `hx-trigger="sse:*"` attribute // Add message handlers for every `hx-trigger="sse:*"` attribute
if (api.getAttributeValue(elt, 'hx-trigger')) { if (api.getAttributeValue(elt, "hx-trigger")) {
// Find closest existing event source // Find closest existing event source
var sourceElement = api.getClosestMatch(elt, hasEventSource) var sourceElement = api.getClosestMatch(elt, hasEventSource);
if (sourceElement == null) { if (sourceElement == null) {
// api.triggerErrorEvent(elt, "htmx:noSSESourceError") // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
return null // no eventsource in parentage, orphaned element return null; // no eventsource in parentage, orphaned element
} }
// Set internalData and source // Set internalData and source
var internalData = api.getInternalData(sourceElement) var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource var source = internalData.sseEventSource;
var triggerSpecs = api.getTriggerSpecs(elt) var triggerSpecs = api.getTriggerSpecs(elt);
triggerSpecs.forEach(function(ts) { triggerSpecs.forEach(function (ts) {
if (ts.trigger.slice(0, 4) !== 'sse:') { if (ts.trigger.slice(0, 4) !== "sse:") {
return return;
} }
var listener = function (event) { var listener = function (event) {
if (maybeCloseSSESource(sourceElement)) { if (maybeCloseSSESource(sourceElement)) {
return return;
} }
if (!api.bodyContains(elt)) { if (!api.bodyContains(elt)) {
source.removeEventListener(ts.trigger.slice(4), listener) source.removeEventListener(ts.trigger.slice(4), listener);
} }
// Trigger events to be handled by the rest of htmx // Trigger events to be handled by the rest of htmx
htmx.trigger(elt, ts.trigger, event) htmx.trigger(elt, ts.trigger, event);
htmx.trigger(elt, 'htmx:sseMessage', event) htmx.trigger(elt, "htmx:sseMessage", event);
} };
// Register the new listener // Register the new listener
api.getInternalData(elt).sseEventListener = listener api.getInternalData(elt).sseEventListener = listener;
source.addEventListener(ts.trigger.slice(4), listener) source.addEventListener(ts.trigger.slice(4), listener);
}) });
} }
} }
@ -177,70 +181,71 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/ */
function ensureEventSourceOnElement(elt, retryCount) { function ensureEventSourceOnElement(elt, retryCount) {
if (elt == null) { if (elt == null) {
return null return null;
} }
// handle extension source creation attribute // handle extension source creation attribute
if (api.getAttributeValue(elt, 'sse-connect')) { if (api.getAttributeValue(elt, "sse-connect")) {
var sseURL = api.getAttributeValue(elt, 'sse-connect') var sseURL = api.getAttributeValue(elt, "sse-connect");
if (sseURL == null) { if (sseURL == null) {
return return;
} }
ensureEventSource(elt, sseURL, retryCount) ensureEventSource(elt, sseURL, retryCount);
} }
registerSSE(elt) registerSSE(elt);
} }
function ensureEventSource(elt, url, retryCount) { function ensureEventSource(elt, url, retryCount) {
var source = htmx.createEventSource(url) var source = htmx.createEventSource(url);
source.onerror = function(err) { source.onerror = function (err) {
// Log an error event // Log an error event
api.triggerErrorEvent(elt, 'htmx:sseError', { error: err, source }) api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source });
// If parent no longer exists in the document, then clean up this EventSource // If parent no longer exists in the document, then clean up this EventSource
if (maybeCloseSSESource(elt)) { if (maybeCloseSSESource(elt)) {
return return;
} }
// Otherwise, try to reconnect the EventSource // Otherwise, try to reconnect the EventSource
if (source.readyState === EventSource.CLOSED) { if (source.readyState === EventSource.CLOSED) {
retryCount = retryCount || 0 retryCount = retryCount || 0;
retryCount = Math.max(Math.min(retryCount * 2, 128), 1) retryCount = Math.max(Math.min(retryCount * 2, 128), 1);
var timeout = retryCount * 500 var timeout = retryCount * 500;
window.setTimeout(function() { window.setTimeout(function () {
ensureEventSourceOnElement(elt, retryCount) ensureEventSourceOnElement(elt, retryCount);
}, timeout) }, timeout);
} }
} };
source.onopen = function(evt) { source.onopen = function (evt) {
api.triggerEvent(elt, 'htmx:sseOpen', { source }) api.triggerEvent(elt, "htmx:sseOpen", { source });
if (retryCount && retryCount > 0) { if (retryCount && retryCount > 0) {
const childrenToFix = elt.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]") const childrenToFix = elt.querySelectorAll(
"[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]",
);
for (let i = 0; i < childrenToFix.length; i++) { for (let i = 0; i < childrenToFix.length; i++) {
registerSSE(childrenToFix[i]) registerSSE(childrenToFix[i]);
} }
// We want to increase the reconnection delay for consecutive failed attempts only // We want to increase the reconnection delay for consecutive failed attempts only
retryCount = 0 retryCount = 0;
} }
} };
api.getInternalData(elt).sseEventSource = source
api.getInternalData(elt).sseEventSource = source;
var closeAttribute = api.getAttributeValue(elt, "sse-close"); var closeAttribute = api.getAttributeValue(elt, "sse-close");
if (closeAttribute) { if (closeAttribute) {
// close eventsource when this message is received // close eventsource when this message is received
source.addEventListener(closeAttribute, function() { source.addEventListener(closeAttribute, function () {
api.triggerEvent(elt, 'htmx:sseClose', { api.triggerEvent(elt, "htmx:sseClose", {
source, source,
type: 'message', type: "message",
}) });
source.close() source.close();
}); });
} }
} }
@ -254,37 +259,35 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/ */
function maybeCloseSSESource(elt) { function maybeCloseSSESource(elt) {
if (!api.bodyContains(elt)) { if (!api.bodyContains(elt)) {
var source = api.getInternalData(elt).sseEventSource var source = api.getInternalData(elt).sseEventSource;
if (source != undefined) { if (source != undefined) {
api.triggerEvent(elt, 'htmx:sseClose', { api.triggerEvent(elt, "htmx:sseClose", {
source, source,
type: 'nodeMissing', type: "nodeMissing",
}) });
source.close() source.close();
// source = null // source = null
return true return true;
} }
} }
return false return false;
} }
/** /**
* @param {HTMLElement} elt * @param {HTMLElement} elt
* @param {string} content * @param {string} content
*/ */
function swap(elt, content) { function swap(elt, content) {
api.withExtensions(elt, function(extension) { api.withExtensions(elt, function (extension) {
content = extension.transformResponse(content, null, elt) content = extension.transformResponse(content, null, elt);
}) });
var swapSpec = api.getSwapSpecification(elt) var swapSpec = api.getSwapSpecification(elt);
var target = api.getTarget(elt) var target = api.getTarget(elt);
api.swap(target, content, swapSpec) api.swap(target, content, swapSpec);
} }
function hasEventSource(node) { function hasEventSource(node) {
return api.getInternalData(node).sseEventSource != null return api.getInternalData(node).sseEventSource != null;
} }
})() })();