Dep: htmx version update

This commit is contained in:
Grail Finder
2025-05-07 06:30:20 +03:00
parent 3eb54cffff
commit 7bd8e8af06
3 changed files with 266 additions and 332 deletions

2
assets/htmx.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -6,11 +6,10 @@ 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.
@ -20,14 +19,18 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/ */
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() {
return ['[sse-connect]', '[data-sse-connect]', '[sse-swap]', '[data-sse-swap]']
},
/** /**
* onEvent handles all events passed to this extension. * onEvent handles all events passed to this extension.
* *
@ -36,31 +39,33 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
* @returns void * @returns void
*/ */
onEvent: function(name, evt) { onEvent: function(name, evt) {
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(evt.target)
// Try to remove remove an EventSource when elements are removed // Try to remove remove an EventSource when elements are removed
if (internalData.sseEventSource) { var source = internalData.sseEventSource
internalData.sseEventSource.close(); if (source) {
api.triggerEvent(parent, 'htmx:sseClose', {
source,
type: 'nodeReplaced',
})
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(evt.target); ensureEventSourceOnElement(parent)
registerSSE(evt.target);
} }
} }
}); })
/// //////////////////////////////////////////// /// ////////////////////////////////////////////
// HELPER FUNCTIONS // HELPER FUNCTIONS
/// //////////////////////////////////////////// /// ////////////////////////////////////////////
/** /**
* createEventSource is the default method for creating new EventSource objects. * 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. * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
@ -69,39 +74,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 })
}
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;
} }
/** /**
@ -112,78 +85,86 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
* @param {HTMLElement} elt * @param {HTMLElement} elt
*/ */
function registerSSE(elt) { function registerSSE(elt) {
// Add message handlers for every `sse-swap` attribute
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
// Add message handlers for every `sse-swap` attribute var sseSwapAttr = api.getAttributeValue(elt, 'sse-swap')
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) { var sseEventNames = sseSwapAttr.split(',')
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++) { for (var i = 0; i < sseEventNames.length; i++) {
var sseEventName = sseEventNames[i].trim(); const sseEventName = sseEventNames[i].trim()
var 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)) {
return;
}
// If the body no longer contains the element, remove the listener
if (!api.bodyContains(child)) {
source.removeEventListener(sseEventName, listener);
}
// swap the response into the DOM and trigger a notification
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) {
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)) { if (maybeCloseSSESource(sourceElement)) {
return return
} }
if (!api.bodyContains(child)) { // If the body no longer contains the element, remove the listener
source.removeEventListener(sseEventName, listener); if (!api.bodyContains(elt)) {
source.removeEventListener(sseEventName, listener)
return
}
// swap the response into the DOM and trigger a notification
if (!api.triggerEvent(elt, 'htmx:sseBeforeMessage', event)) {
return
}
swap(elt, event.data)
api.triggerEvent(elt, 'htmx:sseMessage', event)
}
// Register the new listener
api.getInternalData(elt).sseEventListener = listener
source.addEventListener(sseEventName, listener)
} }
} }
});
// Add message handlers for every `hx-trigger="sse:*"` attribute
if (api.getAttributeValue(elt, 'hx-trigger')) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(elt, 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 triggerSpecs = api.getTriggerSpecs(elt)
triggerSpecs.forEach(function(ts) {
if (ts.trigger.slice(0, 4) !== 'sse:') {
return
}
var listener = function (event) {
if (maybeCloseSSESource(sourceElement)) {
return
}
if (!api.bodyContains(elt)) {
source.removeEventListener(ts.trigger.slice(4), listener)
}
// Trigger events to be handled by the rest of htmx
htmx.trigger(elt, ts.trigger, event)
htmx.trigger(elt, 'htmx:sseMessage', event)
}
// Register the new listener
api.getInternalData(elt).sseEventListener = listener
source.addEventListener(ts.trigger.slice(4), listener)
})
}
} }
/** /**
@ -195,61 +176,73 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
* @returns {EventSource | null} * @returns {EventSource | null}
*/ */
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
queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) { if (api.getAttributeValue(elt, 'sse-connect')) {
var sseURL = api.getAttributeValue(child, "sse-connect"); var sseURL = api.getAttributeValue(elt, 'sse-connect')
if (sseURL == null) { if (sseURL == null) {
return; return
} }
ensureEventSource(child, sseURL, retryCount); ensureEventSource(elt, 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) { 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: 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
var timeout = Math.random() * (2 ^ retryCount) * 500; retryCount = Math.max(Math.min(retryCount * 2, 128), 1)
var timeout = retryCount * 500
window.setTimeout(function() { window.setTimeout(function() {
ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1)); ensureEventSourceOnElement(elt, retryCount)
}, timeout); }, timeout)
}
} }
};
source.onopen = function(evt) { source.onopen = function(evt) {
api.triggerEvent(elt, "htmx:sseOpen", { source: source }); api.triggerEvent(elt, 'htmx:sseOpen', { source })
if (retryCount && retryCount > 0) {
const childrenToFix = elt.querySelectorAll("[sse-swap], [data-sse-swap], [hx-trigger], [data-hx-trigger]")
for (let i = 0; i < childrenToFix.length; i++) {
registerSSE(childrenToFix[i])
}
// We want to increase the reconnection delay for consecutive failed attempts only
retryCount = 0
}
} }
api.getInternalData(elt).sseEventSource = source; api.getInternalData(elt).sseEventSource = source
var closeAttribute = api.getAttributeValue(elt, "sse-close");
if (closeAttribute) {
// close eventsource when this message is received
source.addEventListener(closeAttribute, function() {
api.triggerEvent(elt, 'htmx:sseClose', {
source,
type: 'message',
})
source.close()
});
}
} }
/** /**
@ -261,95 +254,37 @@ 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) {
source.close(); api.triggerEvent(elt, 'htmx:sseClose', {
source,
type: 'nodeMissing',
})
source.close()
// source = null // source = null
return true; return true
} }
} }
return false; 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 {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)
var settleInfo = api.makeSettleInfo(elt); api.swap(target, content, swapSpec)
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) { function hasEventSource(node) {
return api.getInternalData(node).sseEventSource != null; return api.getInternalData(node).sseEventSource != null
} }
})()
})();

View File

@ -3,13 +3,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<title>Alias</title> <title>Alias</title>
<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>
<script src="/assets/htmx.min.js"></script> <script src="/assets/htmx.min.js"></script>
<script src="/assets/htmx.sse.js"></script> <script src="/assets/htmx.sse.js"></script>
<script src="/assets/tailwind.css"></script> <script src="/assets/tailwind.css"></script>
<link rel="stylesheet" href="/assets/style.css"/> <link rel="stylesheet" href="/assets/style.css"/>
<meta charset="utf-8" name="viewport" content="width=device-width,initial-scale=1"/> <meta charset="utf-8" name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="icon" sizes="64x64" href="favicon.ico"/> <link rel="icon" sizes="64x64" href="/assets/favicon/wolfhead_negated.ico"/>
<style type="text/css"> <style type="text/css">
body{ body{
background-color: #0C1616FF; background-color: #0C1616FF;