Compare commits
2 commits
2768cbe2af
...
d1fd1da54b
Author | SHA1 | Date | |
---|---|---|---|
d1fd1da54b | |||
31a2719ca2 |
4 changed files with 133 additions and 100 deletions
4
static/vendor/Sortable.min.js
vendored
4
static/vendor/Sortable.min.js
vendored
File diff suppressed because one or more lines are too long
211
static/vendor/htmx-sse.js
vendored
211
static/vendor/htmx-sse.js
vendored
|
@ -5,7 +5,7 @@ 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;
|
||||||
|
@ -39,17 +39,19 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
|
|
||||||
// Try to remove remove an EventSource when elements are removed
|
case "htmx:beforeCleanupElement":
|
||||||
case "htmx:beforeCleanupElement":
|
var internalData = api.getInternalData(evt.target)
|
||||||
var internalData = api.getInternalData(evt.target)
|
// Try to remove remove an EventSource when elements are removed
|
||||||
if (internalData.sseEventSource) {
|
if (internalData.sseEventSource) {
|
||||||
internalData.sseEventSource.close();
|
internalData.sseEventSource.close();
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
|
|
||||||
// Try to create EventSources when elements are processed
|
return;
|
||||||
case "htmx:afterProcessNode":
|
|
||||||
createEventSourceOnElement(evt.target);
|
// Try to create EventSources when elements are processed
|
||||||
|
case "htmx:afterProcessNode":
|
||||||
|
ensureEventSourceOnElement(evt.target);
|
||||||
|
registerSSE(evt.target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -66,8 +68,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||||
* @param {string} url
|
* @param {string} url
|
||||||
* @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) {
|
function splitOnWhitespace(trigger) {
|
||||||
|
@ -90,7 +92,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||||
function getLegacySSESwaps(elt) {
|
function getLegacySSESwaps(elt) {
|
||||||
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
||||||
var returnArr = [];
|
var returnArr = [];
|
||||||
if (legacySSEValue) {
|
if (legacySSEValue != null) {
|
||||||
var values = splitOnWhitespace(legacySSEValue);
|
var values = splitOnWhitespace(legacySSEValue);
|
||||||
for (var i = 0; i < values.length; i++) {
|
for (var i = 0; i < values.length; i++) {
|
||||||
var value = values[i].split(/:(.+)/);
|
var value = values[i].split(/:(.+)/);
|
||||||
|
@ -103,63 +105,24 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* createEventSourceOnElement creates a new EventSource connection on the provided element.
|
* registerSSE looks for attributes that can contain sse events, right
|
||||||
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
* now hx-trigger and sse-swap and adds listeners based on these attributes too
|
||||||
* is created and stored in the element's internalData.
|
* the closest event source
|
||||||
|
*
|
||||||
* @param {HTMLElement} elt
|
* @param {HTMLElement} elt
|
||||||
* @param {number} retryCount
|
|
||||||
* @returns {EventSource | null}
|
|
||||||
*/
|
*/
|
||||||
function createEventSourceOnElement(elt, retryCount) {
|
function registerSSE(elt) {
|
||||||
|
// Find closest existing event source
|
||||||
if (elt == null) {
|
var sourceElement = api.getClosestMatch(elt, hasEventSource);
|
||||||
return null;
|
if (sourceElement == null) {
|
||||||
|
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
||||||
|
return null; // no eventsource in parentage, orphaned element
|
||||||
}
|
}
|
||||||
|
|
||||||
var internalData = api.getInternalData(elt);
|
// Set internalData and source
|
||||||
|
var internalData = api.getInternalData(sourceElement);
|
||||||
|
var source = internalData.sseEventSource;
|
||||||
|
|
||||||
// get URL from element's attribute
|
|
||||||
var sseURL = api.getAttributeValue(elt, "sse-connect");
|
|
||||||
|
|
||||||
|
|
||||||
if (sseURL == undefined) {
|
|
||||||
var legacyURL = getLegacySSEURL(elt)
|
|
||||||
if (legacyURL) {
|
|
||||||
sseURL = legacyURL;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to the EventSource
|
|
||||||
var source = htmx.createEventSource(sseURL);
|
|
||||||
internalData.sseEventSource = source;
|
|
||||||
|
|
||||||
// Create event handlers
|
|
||||||
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() {
|
|
||||||
createEventSourceOnElement(elt, Math.min(7, retryCount+1));
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
source.onopen = function (evt) {
|
|
||||||
api.triggerEvent(elt, "htmx::sseOpen", {source: source});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add message handlers for every `sse-swap` attribute
|
// Add message handlers for every `sse-swap` attribute
|
||||||
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
|
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
|
||||||
|
|
||||||
|
@ -170,23 +133,27 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||||
var sseEventNames = getLegacySSESwaps(child);
|
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();
|
var sseEventName = sseEventNames[i].trim();
|
||||||
var listener = function(event) {
|
var listener = function(event) {
|
||||||
|
|
||||||
// If the parent is missing then close SSE and remove listener
|
// If the source is missing then close SSE
|
||||||
if (maybeCloseSSESource(elt)) {
|
if (maybeCloseSSESource(sourceElement)) {
|
||||||
source.removeEventListener(sseEventName, listener);
|
|
||||||
return;
|
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 the response into the DOM and trigger a notification
|
||||||
swap(child, event.data);
|
swap(child, 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(child).sseEventListener = listener;
|
||||||
source.addEventListener(sseEventName, listener);
|
source.addEventListener(sseEventName, listener);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -203,24 +170,86 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||||
if (sseEventName.slice(0, 4) != "sse:") {
|
if (sseEventName.slice(0, 4) != "sse:") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove the sse: prefix from here on out
|
||||||
|
sseEventName = sseEventName.substr(4);
|
||||||
|
|
||||||
var listener = function(event) {
|
var listener = function() {
|
||||||
|
if (maybeCloseSSESource(sourceElement)) {
|
||||||
// If parent is missing, then close SSE and remove listener
|
return
|
||||||
if (maybeCloseSSESource(elt)) {
|
|
||||||
source.removeEventListener(sseEventName, listener);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger events to be handled by the rest of htmx
|
if (!api.bodyContains(child)) {
|
||||||
htmx.trigger(child, sseEventName, event);
|
source.removeEventListener(sseEventName, listener);
|
||||||
htmx.trigger(child, "htmx:sseMessage", event);
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register the new listener
|
ensureEventSource(child, sseURL, retryCount);
|
||||||
api.getInternalData(elt).sseEventListener = listener;
|
|
||||||
source.addEventListener(sseEventName.slice(4), listener);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -253,12 +282,12 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||||
var result = [];
|
var result = [];
|
||||||
|
|
||||||
// If the parent element also contains the requested attribute, then add it to the results too.
|
// 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-sse")) {
|
if (api.hasAttribute(elt, attributeName)) {
|
||||||
result.push(elt);
|
result.push(elt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search all child nodes that match the requested attribute
|
// Search all child nodes that match the requested attribute
|
||||||
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) {
|
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
|
||||||
result.push(node);
|
result.push(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -281,7 +310,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||||
|
|
||||||
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
|
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
|
||||||
|
|
||||||
settleInfo.elts.forEach(function (elt) {
|
settleInfo.elts.forEach(function(elt) {
|
||||||
if (elt.classList) {
|
if (elt.classList) {
|
||||||
elt.classList.add(htmx.config.settlingClass);
|
elt.classList.add(htmx.config.settlingClass);
|
||||||
}
|
}
|
||||||
|
@ -306,11 +335,11 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||||
function doSettle(settleInfo) {
|
function doSettle(settleInfo) {
|
||||||
|
|
||||||
return function() {
|
return function() {
|
||||||
settleInfo.tasks.forEach(function (task) {
|
settleInfo.tasks.forEach(function(task) {
|
||||||
task.call();
|
task.call();
|
||||||
});
|
});
|
||||||
|
|
||||||
settleInfo.elts.forEach(function (elt) {
|
settleInfo.elts.forEach(function(elt) {
|
||||||
if (elt.classList) {
|
if (elt.classList) {
|
||||||
elt.classList.remove(htmx.config.settlingClass);
|
elt.classList.remove(htmx.config.settlingClass);
|
||||||
}
|
}
|
||||||
|
@ -319,4 +348,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
})();
|
function hasEventSource(node) {
|
||||||
|
return api.getInternalData(node).sseEventSource != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
2
static/vendor/htmx.min.js
vendored
2
static/vendor/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -30,15 +30,15 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const scrollCurrentSongIntoView = () => {
|
|
||||||
const hoveredSong = document.querySelector(".queue li:hover");
|
|
||||||
if (hoveredSong === null) {
|
|
||||||
const currentSong = document.querySelector(".queue li.playing");
|
|
||||||
currentSong.scrollIntoView({ block: "nearest" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
htmx.onLoad(() => {
|
htmx.onLoad(() => {
|
||||||
|
const scrollCurrentSongIntoView = () => {
|
||||||
|
const hoveredSong = document.querySelector(".queue li:hover");
|
||||||
|
if (hoveredSong === null) {
|
||||||
|
const currentSong = document.querySelector(".queue li.playing");
|
||||||
|
currentSong?.scrollIntoView({ block: "nearest" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isReduced = window
|
const isReduced = window
|
||||||
.matchMedia("(prefers-reduced-motion: reduce)")
|
.matchMedia("(prefers-reduced-motion: reduce)")
|
||||||
.matches;
|
.matches;
|
||||||
|
|
Loading…
Reference in a new issue