Proper pre-loading and deps

This commit is contained in:
Atridad Lahiji 2024-10-30 23:50:34 -06:00
parent 24bf04aa7a
commit 561905aabb
Signed by: atridad
SSH key fingerprint: SHA256:LGomp8Opq0jz+7kbwNcdfTcuaLRb5Nh0k5AchDDb438
13 changed files with 880 additions and 825 deletions

View file

@ -47,6 +47,4 @@ Atridad Lahiji // Root
{{end}}
{{define "foot"}}
<script src="/public/js/htmx.base.js"></script>
<script src="/public/js/htmx.sse.js"></script>
{{end}}

View file

@ -10,12 +10,14 @@
{{template "head" .}}
</head>
<body class="block h-[100%]">
<body class="block h-[100%]" hx-ext="preload">
{{template "header" .}}
<main class="container flex flex-col items-center justify-center gap-3 sm:gap-6 p-4 text-center mx-auto min-h-[calc(100%-64px)]">
{{template "main" .}}
</main>
<script src="/public/js/htmx.base.js"></script>
<script src="/public/js/htmx.preload.js"></script>
{{template "foot" .}}
</body>
</html>

View file

@ -42,7 +42,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
</button>
<a href="/posts" class="btn btn-primary btn-outline">
<a href="/posts" class="btn btn-primary btn-outline" preload="mouseover">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo-2"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11"/></svg>
Back
</a>
@ -52,6 +52,8 @@
</article>
</main>
<script src="/public/js/htmx.preload.js"></script>
<script src="/public/js/htmx.base.js"></script>
{{template "foot" .}}
</body>
</html>

View file

@ -1,7 +1,7 @@
{{define "buttonlinks"}}
{{if eq true .Internal}}
<a class="btn btn-primary btn-outline btn-md lg:btn-lg" href={{.Href}}>
<a class="btn btn-primary btn-outline btn-md lg:btn-lg" href={{.Href}} preload="mouseover">
{{.Name}}
</a>

View file

@ -32,6 +32,7 @@
href={{.Href}}
aria-label={{.Name}}
class="btn btn-circle btn-base-100 text-primary hover:btn-accent hover:text-neutral"
preload="mouseover"
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-right"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
</a>

View file

@ -11,6 +11,7 @@
<ul
tabindex="0"
class="menu menu-compact dropdown-content gap-2 mt-3 p-2 shadow bg-base-100 rounded-box"
preload="mouseover"
>
{{template "navitems" .}}
</ul>

View file

@ -15,6 +15,5 @@ Atridad Lahiji // Post
{{end}}
{{define "foot"}}
<script src="/public/js/htmx.base.js"></script>
<script src="/public/js/hyperscript.js"></script>
{{end}}

View file

@ -31,6 +31,5 @@ Atridad Lahiji // Tools // SSE Demo
{{end}}
{{define "foot"}}
<script src="/public/js/htmx.base.js"></script>
<script src="/public/js/htmx.sse.js"></script>
{{end}}

File diff suppressed because one or more lines are too long

141
public/js/htmx.preload.js vendored Normal file
View file

@ -0,0 +1,141 @@
// 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()
}
}
}
// 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
const parent = event.target || event.detail.elt;
parent.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)
})
}
})

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

@ -6,11 +6,10 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
var api
htmx.defineExtension("sse", {
htmx.defineExtension('sse', {
/**
* 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) {
// store a reference to the internal API.
api = apiRef;
api = apiRef
// set a function in the public API for creating new EventSource objects
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.
*
@ -36,31 +39,33 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
* @returns void
*/
onEvent: function(name, evt) {
var parent = evt.target || evt.detail.elt;
var parent = evt.target || evt.detail.elt
switch (name) {
case "htmx:beforeCleanupElement":
case 'htmx:beforeCleanupElement':
var internalData = api.getInternalData(parent)
// Try to remove remove an EventSource when elements are removed
if (internalData.sseEventSource) {
internalData.sseEventSource.close();
var source = internalData.sseEventSource
if (source) {
api.triggerEvent(parent, 'htmx:sseClose', {
source,
type: 'nodeReplaced',
})
internalData.sseEventSource.close()
}
return;
return
// Try to create EventSources when elements are processed
case "htmx:afterProcessNode":
ensureEventSourceOnElement(parent);
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.
@ -69,39 +74,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
* @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;
return new EventSource(url, { withCredentials: true })
}
/**
@ -113,90 +86,85 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/
function registerSSE(elt) {
// Add message handlers for every `sse-swap` attribute
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function (child) {
if (api.getAttributeValue(elt, 'sse-swap')) {
// Find closest existing event source
var sourceElement = api.getClosestMatch(child, hasEventSource);
var sourceElement = api.getClosestMatch(elt, hasEventSource)
if (sourceElement == null) {
// 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
var internalData = api.getInternalData(sourceElement);
var source = internalData.sseEventSource;
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);
}
var sseSwapAttr = api.getAttributeValue(elt, 'sse-swap')
var sseEventNames = sseSwapAttr.split(',')
for (var i = 0; i < sseEventNames.length; i++) {
var sseEventName = sseEventNames[i].trim();
var listener = function(event) {
const sseEventName = sseEventNames[i].trim()
const 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);
// If the body no longer contains the element, remove the 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)
})
}
}
/**
@ -208,62 +176,73 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
* @returns {EventSource | null}
*/
function ensureEventSourceOnElement(elt, retryCount) {
if (elt == null) {
return null;
return null
}
// handle extension source creation attribute
queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
var sseURL = api.getAttributeValue(child, "sse-connect");
if (api.getAttributeValue(elt, 'sse-connect')) {
var sseURL = api.getAttributeValue(elt, 'sse-connect')
if (sseURL == null) {
return;
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(elt, sseURL, retryCount)
}
ensureEventSource(child, sseURL, retryCount);
});
registerSSE(elt);
registerSSE(elt)
}
function ensureEventSource(elt, url, retryCount) {
var source = htmx.createEventSource(url);
var source = htmx.createEventSource(url)
source.onerror = function(err) {
// 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 (maybeCloseSSESource(elt)) {
return;
return
}
// Otherwise, try to reconnect the EventSource
if (source.readyState === EventSource.CLOSED) {
retryCount = retryCount || 0;
var timeout = Math.random() * (2 ^ retryCount) * 500;
retryCount = retryCount || 0
retryCount = Math.max(Math.min(retryCount * 2, 128), 1)
var timeout = retryCount * 500
window.setTimeout(function() {
ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
}, timeout);
ensureEventSourceOnElement(elt, retryCount)
}, timeout)
}
}
};
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()
});
}
}
/**
@ -275,95 +254,37 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/
function maybeCloseSSESource(elt) {
if (!api.bodyContains(elt)) {
var source = api.getInternalData(elt).sseEventSource;
var source = api.getInternalData(elt).sseEventSource
if (source != undefined) {
source.close();
api.triggerEvent(elt, 'htmx:sseClose', {
source,
type: 'nodeMissing',
})
source.close()
// 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 {string} content
*/
function swap(elt, content) {
api.withExtensions(elt, function(extension) {
content = extension.transformResponse(content, null, elt);
});
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)();
}
var swapSpec = api.getSwapSpecification(elt)
var target = api.getTarget(elt)
api.swap(target, content, swapSpec)
}
/**
* 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;
return api.getInternalData(node).sseEventSource != null
}
})();
})()

259
public/js/htmx.ws.js vendored
View file

@ -5,29 +5,27 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
*/
(function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api;
var api
htmx.defineExtension("ws", {
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;
api = apiRef
// Default function for creating new EventSource objects
if (!htmx.createWebSocket) {
htmx.createWebSocket = createWebSocket;
htmx.createWebSocket = createWebSocket
}
// Default setting for reconnect delay
if (!htmx.config.wsReconnectDelay) {
htmx.config.wsReconnectDelay = "full-jitter";
htmx.config.wsReconnectDelay = 'full-jitter'
}
},
@ -38,44 +36,43 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
* @param {Event} evt
*/
onEvent: function(name, evt) {
var parent = evt.target || evt.detail.elt;
var parent = evt.target || evt.detail.elt
switch (name) {
// Try to close the socket when elements are removed
case "htmx:beforeCleanupElement":
case 'htmx:beforeCleanupElement':
var internalData = api.getInternalData(parent)
if (internalData.webSocket) {
internalData.webSocket.close();
internalData.webSocket.close()
}
return;
return
// Try to create websockets when elements are processed
case "htmx:beforeProcessNode":
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
case 'htmx:beforeProcessNode':
forEach(queryAttributeOnThisOrChildren(parent, 'ws-connect'), function(child) {
ensureWebSocket(child)
});
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
})
forEach(queryAttributeOnThisOrChildren(parent, 'ws-send'), function(child) {
ensureWebSocketSend(child)
});
})
}
}
});
})
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
return trigger.trim().split(/\s+/)
}
function getLegacyWebsocketURL(elt) {
var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
var legacySSEValue = api.getAttributeValue(elt, 'hx-ws')
if (legacySSEValue) {
var values = splitOnWhitespace(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];
var value = values[i].split(/:(.+)/)
if (value[0] === 'connect') {
return value[1]
}
}
}
@ -88,72 +85,71 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
* @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;
return
}
// Get the source straight from the element's value
var wssSource = api.getAttributeValue(socketElt, "ws-connect")
var wssSource = api.getAttributeValue(socketElt, 'ws-connect')
if (wssSource == null || wssSource === "") {
var legacySource = getLegacyWebsocketURL(socketElt);
if (wssSource == null || wssSource === '') {
var legacySource = getLegacyWebsocketURL(socketElt)
if (legacySource == null) {
return;
return
} else {
wssSource = legacySource;
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 (wssSource.indexOf('/') === 0) {
var base_part = location.hostname + (location.port ? ':' + location.port : '')
if (location.protocol === 'https:') {
wssSource = "wss://" + base_part + wssSource;
wssSource = 'wss://' + base_part + wssSource
} else if (location.protocol === 'http:') {
wssSource = "ws://" + base_part + wssSource;
wssSource = 'ws://' + base_part + wssSource
}
}
var socketWrapper = createWebsocketWrapper(socketElt, function() {
return htmx.createWebSocket(wssSource)
});
})
socketWrapper.addEventListener('message', function(event) {
if (maybeCloseWebSocketSource(socketElt)) {
return;
return
}
var response = event.data;
if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
var response = event.data
if (!api.triggerEvent(socketElt, 'htmx:wsBeforeMessage', {
message: response,
socketWrapper: socketWrapper.publicInterface
})) {
return;
return
}
api.withExtensions(socketElt, function(extension) {
response = extension.transformResponse(response, null, socketElt);
});
response = extension.transformResponse(response, null, socketElt)
})
var settleInfo = api.makeSettleInfo(socketElt);
var fragment = api.makeFragment(response);
var settleInfo = api.makeSettleInfo(socketElt)
var fragment = api.makeFragment(response)
if (fragment.children.length) {
var children = Array.from(fragment.children);
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.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 })
});
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;
api.getInternalData(socketElt).webSocket = socketWrapper
}
/**
@ -185,14 +181,14 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
addEventListener: function(event, handler) {
if (this.socket) {
this.socket.addEventListener(event, handler);
this.socket.addEventListener(event, handler)
}
if (!this.events[event]) {
this.events[event] = [];
this.events[event] = []
}
this.events[event].push(handler);
this.events[event].push(handler)
},
sendImmediately: function(message, sendElt) {
@ -200,12 +196,12 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
api.triggerErrorEvent()
}
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
message: message,
message,
socketWrapper: this.publicInterface
})) {
this.socket.send(message);
this.socket.send(message)
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
message: message,
message,
socketWrapper: this.publicInterface
})
}
@ -213,9 +209,9 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
send: function(message, sendElt) {
if (this.socket.readyState !== this.socket.OPEN) {
this.messageQueue.push({ message: message, sendElt: sendElt });
this.messageQueue.push({ message, sendElt })
} else {
this.sendImmediately(message, sendElt);
this.sendImmediately(message, sendElt)
}
},
@ -223,10 +219,10 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
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();
this.sendImmediately(queuedItem.message, queuedItem.sendElt)
this.messageQueue.shift()
} else {
break;
break
}
}
},
@ -239,48 +235,48 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
// Create a new WebSocket and event handlers
/** @type {WebSocket} */
var socket = socketFunc();
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' } });
api.triggerEvent(socketElt, 'htmx:wsConnecting', { event: { type: 'connecting' } })
this.socket = socket;
this.socket = socket
socket.onopen = function(e) {
wrapper.retryCount = 0;
api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
wrapper.handleQueuedMessages();
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);
var delay = getWebSocketReconnectDelay(wrapper.retryCount)
setTimeout(function() {
wrapper.retryCount += 1;
wrapper.init();
}, delay);
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 })
};
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);
};
api.triggerErrorEvent(socketElt, 'htmx:wsError', { error: e, socketWrapper: wrapper })
maybeCloseWebSocketSource(socketElt)
}
var events = this.events;
var events = this.events
Object.keys(events).forEach(function(k) {
events[k].forEach(function(e) {
socket.addEventListener(k, e);
socket.addEventListener(k, e)
})
})
});
},
close: function() {
@ -288,15 +284,15 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
}
}
wrapper.init();
wrapper.init()
wrapper.publicInterface = {
send: wrapper.send.bind(wrapper),
sendImmediately: wrapper.sendImmediately.bind(wrapper),
queue: wrapper.messageQueue
};
}
return wrapper;
return wrapper
}
/**
@ -305,13 +301,13 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
* @param {HTMLElement} elt
*/
function ensureWebSocketSend(elt) {
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
var legacyAttribute = api.getAttributeValue(elt, 'hx-ws')
if (legacyAttribute && legacyAttribute !== 'send') {
return;
return
}
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
processWebSocketSend(webSocketParent, elt);
processWebSocketSend(webSocketParent, elt)
}
/**
@ -320,7 +316,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
* @returns {boolean}
*/
function hasWebSocket(node) {
return api.getInternalData(node).webSocket != null;
return api.getInternalData(node).webSocket != null
}
/**
@ -330,59 +326,58 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
* @param {HTMLElement} sendElt
*/
function processWebSocketSend(socketElt, sendElt) {
var nodeData = api.getInternalData(sendElt);
var triggerSpecs = api.getTriggerSpecs(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;
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 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 = Object.assign({}, 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,
headers,
errors,
triggeringEvent: evt,
messageBody: undefined,
socketWrapper: socketWrapper.publicInterface
};
}
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
return;
return
}
if (errors && errors.length > 0) {
api.triggerEvent(elt, 'htmx:validation:halted', errors);
return;
api.triggerEvent(elt, 'htmx:validation:halted', errors)
return
}
var body = sendConfig.messageBody;
var body = sendConfig.messageBody
if (body === undefined) {
var toSend = Object.assign({}, sendConfig.parameters);
if (sendConfig.headers)
toSend['HEADERS'] = headers;
body = JSON.stringify(toSend);
var toSend = Object.assign({}, sendConfig.parameters)
if (sendConfig.headers) { toSend.HEADERS = headers }
body = JSON.stringify(toSend)
}
socketWrapper.send(body, elt);
socketWrapper.send(body, elt)
if (evt && api.shouldCancel(evt, elt)) {
evt.preventDefault();
evt.preventDefault()
}
});
});
})
})
}
/**
@ -391,19 +386,18 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
* @returns {number}
*/
function getWebSocketReconnectDelay(retryCount) {
/** @type {"full-jitter" | ((retryCount:number) => number)} */
var delay = htmx.config.wsReconnectDelay;
var delay = htmx.config.wsReconnectDelay
if (typeof delay === 'function') {
return delay(retryCount);
return delay(retryCount)
}
if (delay === 'full-jitter') {
var exp = Math.min(retryCount, 6);
var maxDelay = 1000 * Math.pow(2, exp);
return maxDelay * Math.random();
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"');
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')
}
/**
@ -417,10 +411,10 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
*/
function maybeCloseWebSocketSource(elt) {
if (!api.bodyContains(elt)) {
api.getInternalData(elt).webSocket.close();
return true;
api.getInternalData(elt).webSocket.close()
return true
}
return false;
return false
}
/**
@ -431,9 +425,9 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
* @returns WebSocket
*/
function createWebSocket(url) {
var sock = new WebSocket(url, []);
sock.binaryType = htmx.config.wsBinaryType;
return sock;
var sock = new WebSocket(url, [])
sock.binaryType = htmx.config.wsBinaryType
return sock
}
/**
@ -443,16 +437,15 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
* @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);
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) {
elt.querySelectorAll('[' + attributeName + '], [data-' + attributeName + '], [data-hx-ws], [hx-ws]').forEach(function(node) {
result.push(node)
})
@ -467,10 +460,8 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
function forEach(arr, func) {
if (arr) {
for (var i = 0; i < arr.length; i++) {
func(arr[i]);
func(arr[i])
}
}
}
})();
})()

File diff suppressed because one or more lines are too long