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}} {{end}}
{{define "foot"}} {{define "foot"}}
<script src="/public/js/htmx.base.js"></script>
<script src="/public/js/htmx.sse.js"></script>
{{end}} {{end}}

View file

@ -10,12 +10,14 @@
{{template "head" .}} {{template "head" .}}
</head> </head>
<body class="block h-[100%]"> <body class="block h-[100%]" hx-ext="preload">
{{template "header" .}} {{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)]"> <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" .}} {{template "main" .}}
</main> </main>
<script src="/public/js/htmx.base.js"></script>
<script src="/public/js/htmx.preload.js"></script>
{{template "foot" .}} {{template "foot" .}}
</body> </body>
</html> </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> <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> </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> <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 Back
</a> </a>
@ -52,6 +52,8 @@
</article> </article>
</main> </main>
<script src="/public/js/htmx.preload.js"></script>
<script src="/public/js/htmx.base.js"></script>
{{template "foot" .}} {{template "foot" .}}
</body> </body>
</html> </html>

View file

@ -1,7 +1,7 @@
{{define "buttonlinks"}} {{define "buttonlinks"}}
{{if eq true .Internal}} {{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}} {{.Name}}
</a> </a>

View file

@ -32,6 +32,7 @@
href={{.Href}} href={{.Href}}
aria-label={{.Name}} aria-label={{.Name}}
class="btn btn-circle btn-base-100 text-primary hover:btn-accent hover:text-neutral" 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> <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> </a>

View file

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

View file

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

View file

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

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

@ -6,364 +6,285 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
*/ */
(function() { (function() {
/** @type {import("../htmx").HtmxInternalApi} */
var api
/** @type {import("../htmx").HtmxInternalApi} */ htmx.defineExtension('sse', {
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
* Init saves the provided reference to the internal HTMX API. if (htmx.createEventSource == undefined) {
* htmx.createEventSource = createEventSource
* @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 getSelectors: function() {
if (htmx.createEventSource == undefined) { return ['[sse-connect]', '[data-sse-connect]', '[sse-swap]', '[data-sse-swap]']
htmx.createEventSource = createEventSource; },
}
},
/** /**
* onEvent handles all events passed to this extension. * onEvent handles all events passed to this extension.
* *
* @param {string} name * @param {string} name
* @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
switch (name) {
case 'htmx:beforeCleanupElement':
var internalData = api.getInternalData(parent)
// Try to remove remove an EventSource when elements are removed
var source = internalData.sseEventSource
if (source) {
api.triggerEvent(parent, 'htmx:sseClose', {
source,
type: 'nodeReplaced',
})
internalData.sseEventSource.close()
}
var parent = evt.target || evt.detail.elt; return
switch (name) {
case "htmx:beforeCleanupElement": // Try to create EventSources when elements are processed
var internalData = api.getInternalData(parent) case 'htmx:afterProcessNode':
// Try to remove remove an EventSource when elements are removed ensureEventSourceOnElement(parent)
if (internalData.sseEventSource) { }
internalData.sseEventSource.close(); }
} })
return; /// ////////////////////////////////////////////
// HELPER FUNCTIONS
/// ////////////////////////////////////////////
// Try to create EventSources when elements are processed /**
case "htmx:afterProcessNode": * createEventSource is the default method for creating new EventSource objects.
ensureEventSourceOnElement(parent); * 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 })
}
/////////////////////////////////////////////// /**
// HELPER FUNCTIONS * 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
if (api.getAttributeValue(elt, 'sse-swap')) {
// 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 sseSwapAttr = api.getAttributeValue(elt, 'sse-swap')
var sseEventNames = sseSwapAttr.split(',')
for (var i = 0; i < sseEventNames.length; i++) {
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(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)
})
}
}
/**
* 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
if (api.getAttributeValue(elt, 'sse-connect')) {
var sseURL = api.getAttributeValue(elt, 'sse-connect')
if (sseURL == null) {
return
}
ensureEventSource(elt, 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 })
// 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
retryCount = Math.max(Math.min(retryCount * 2, 128), 1)
var timeout = retryCount * 500
window.setTimeout(function() {
ensureEventSourceOnElement(elt, retryCount)
}, timeout)
}
}
source.onopen = function(evt) {
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
/** var closeAttribute = api.getAttributeValue(elt, "sse-close");
* createEventSource is the default method for creating new EventSource objects. if (closeAttribute) {
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed. // close eventsource when this message is received
* source.addEventListener(closeAttribute, function() {
* @param {string} url api.triggerEvent(elt, 'htmx:sseClose', {
* @returns EventSource source,
*/ type: 'message',
function createEventSource(url) { })
return new EventSource(url, { withCredentials: true }); source.close()
} });
}
}
function splitOnWhitespace(trigger) { /**
return trigger.trim().split(/\s+/); * 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) {
api.triggerEvent(elt, 'htmx:sseClose', {
source,
type: 'nodeMissing',
})
source.close()
// source = null
return true
}
}
return false
}
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"); * @param {HTMLElement} elt
var returnArr = []; * @param {string} content
if (legacySSEValue != null) { */
var values = splitOnWhitespace(legacySSEValue); function swap(elt, content) {
for (var i = 0; i < values.length; i++) { api.withExtensions(elt, function(extension) {
var value = values[i].split(/:(.+)/); content = extension.transformResponse(content, null, elt)
if (value[0] === "swap") { })
returnArr.push(value[1]);
}
}
}
return returnArr;
}
/** var swapSpec = api.getSwapSpecification(elt)
* registerSSE looks for attributes that can contain sse events, right var target = api.getTarget(elt)
* now hx-trigger and sse-swap and adds listeners based on these attributes too api.swap(target, content, swapSpec)
* 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"); function hasEventSource(node) {
if (sseSwapAttr) { return api.getInternalData(node).sseEventSource != null
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;
}
})();

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

@ -4,473 +4,464 @@ WebSockets Extension
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions. This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
*/ */
(function () { (function() {
/** @type {import("../htmx").HtmxInternalApi} */
/** @type {import("../htmx").HtmxInternalApi} */ var api
var api;
htmx.defineExtension('ws', {
htmx.defineExtension("ws", {
/**
/** * init is called once, when this extension is first registered.
* init is called once, when this extension is first registered. * @param {import("../htmx").HtmxInternalApi} apiRef
* @param {import("../htmx").HtmxInternalApi} apiRef */
*/ init: function(apiRef) {
init: function (apiRef) { // Store reference to internal API
api = apiRef
// Store reference to internal API
api = apiRef; // Default function for creating new EventSource objects
if (!htmx.createWebSocket) {
// Default function for creating new EventSource objects htmx.createWebSocket = createWebSocket
if (!htmx.createWebSocket) { }
htmx.createWebSocket = createWebSocket;
} // Default setting for reconnect delay
if (!htmx.config.wsReconnectDelay) {
// Default setting for reconnect delay htmx.config.wsReconnectDelay = 'full-jitter'
if (!htmx.config.wsReconnectDelay) { }
htmx.config.wsReconnectDelay = "full-jitter"; },
}
}, /**
* onEvent handles all events passed to this extension.
/** *
* onEvent handles all events passed to this extension. * @param {string} name
* * @param {Event} evt
* @param {string} name */
* @param {Event} evt onEvent: function(name, evt) {
*/ var parent = evt.target || evt.detail.elt
onEvent: function (name, evt) { switch (name) {
var parent = evt.target || evt.detail.elt; // Try to close the socket when elements are removed
case 'htmx:beforeCleanupElement':
switch (name) {
var internalData = api.getInternalData(parent)
// Try to close the socket when elements are removed
case "htmx:beforeCleanupElement": if (internalData.webSocket) {
internalData.webSocket.close()
var internalData = api.getInternalData(parent) }
return
if (internalData.webSocket) {
internalData.webSocket.close(); // Try to create websockets when elements are processed
} case 'htmx:beforeProcessNode':
return;
forEach(queryAttributeOnThisOrChildren(parent, 'ws-connect'), function(child) {
// Try to create websockets when elements are processed ensureWebSocket(child)
case "htmx:beforeProcessNode": })
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) { forEach(queryAttributeOnThisOrChildren(parent, 'ws-send'), function(child) {
ensureWebSocket(child) ensureWebSocketSend(child)
}); })
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) { }
ensureWebSocketSend(child) }
}); })
}
} function splitOnWhitespace(trigger) {
}); return trigger.trim().split(/\s+/)
}
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/); function getLegacyWebsocketURL(elt) {
} var legacySSEValue = api.getAttributeValue(elt, 'hx-ws')
if (legacySSEValue) {
function getLegacyWebsocketURL(elt) { var values = splitOnWhitespace(legacySSEValue)
var legacySSEValue = api.getAttributeValue(elt, "hx-ws"); for (var i = 0; i < values.length; i++) {
if (legacySSEValue) { var value = values[i].split(/:(.+)/)
var values = splitOnWhitespace(legacySSEValue); if (value[0] === 'connect') {
for (var i = 0; i < values.length; i++) { return value[1]
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
* ensureWebSocket creates a new WebSocket on the designated element, using * @returns
* the element's "ws-connect" attribute. */
* @param {HTMLElement} socketElt function ensureWebSocket(socketElt) {
* @returns // If the element containing the WebSocket connection no longer exists, then
*/ // do not connect/reconnect the WebSocket.
function ensureWebSocket(socketElt) { if (!api.bodyContains(socketElt)) {
return
// If the element containing the WebSocket connection no longer exists, then }
// do not connect/reconnect the WebSocket.
if (!api.bodyContains(socketElt)) { // Get the source straight from the element's value
return; var wssSource = api.getAttributeValue(socketElt, 'ws-connect')
}
if (wssSource == null || wssSource === '') {
// Get the source straight from the element's value var legacySource = getLegacyWebsocketURL(socketElt)
var wssSource = api.getAttributeValue(socketElt, "ws-connect") if (legacySource == null) {
return
if (wssSource == null || wssSource === "") { } else {
var legacySource = getLegacyWebsocketURL(socketElt); wssSource = legacySource
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:') {
// Guarantee that the wssSource value is a fully qualified URL wssSource = 'wss://' + base_part + wssSource
if (wssSource.indexOf("/") === 0) { } else if (location.protocol === 'http:') {
var base_part = location.hostname + (location.port ? ':' + location.port : ''); wssSource = 'ws://' + base_part + wssSource
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)
} })
var socketWrapper = createWebsocketWrapper(socketElt, function () { socketWrapper.addEventListener('message', function(event) {
return htmx.createWebSocket(wssSource) if (maybeCloseWebSocketSource(socketElt)) {
}); return
}
socketWrapper.addEventListener('message', function (event) {
if (maybeCloseWebSocketSource(socketElt)) { var response = event.data
return; if (!api.triggerEvent(socketElt, 'htmx:wsBeforeMessage', {
} message: response,
socketWrapper: socketWrapper.publicInterface
var response = event.data; })) {
if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", { return
message: response, }
socketWrapper: socketWrapper.publicInterface
})) { api.withExtensions(socketElt, function(extension) {
return; response = extension.transformResponse(response, null, socketElt)
} })
api.withExtensions(socketElt, function (extension) { var settleInfo = api.makeSettleInfo(socketElt)
response = extension.transformResponse(response, null, socketElt); var fragment = api.makeFragment(response)
});
if (fragment.children.length) {
var settleInfo = api.makeSettleInfo(socketElt); var children = Array.from(fragment.children)
var fragment = api.makeFragment(response); for (var i = 0; i < children.length; i++) {
api.oobSwap(api.getAttributeValue(children[i], 'hx-swap-oob') || 'true', children[i], settleInfo)
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 })
} })
api.settleImmediately(settleInfo.tasks); // Put the WebSocket into the HTML Element's custom data.
api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface }) api.getInternalData(socketElt).webSocket = socketWrapper
}); }
// 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
* @typedef {Object} WebSocketWrapper * @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
* @property {WebSocket} socket * @property {(message: string, sendElt: Element) => void} send
* @property {Array<{message: string, sendElt: Element}>} messageQueue * @property {(event: string, handler: Function) => void} addEventListener
* @property {number} retryCount * @property {() => void} handleQueuedMessages
* @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state * @property {() => void} init
* @property {(message: string, sendElt: Element) => void} send * @property {() => void} close
* @property {(event: string, handler: Function) => void} addEventListener */
* @property {() => void} handleQueuedMessages /**
* @property {() => void} init *
* @property {() => void} close * @param socketElt
*/ * @param socketFunc
/** * @returns {WebSocketWrapper}
* */
* @param socketElt function createWebsocketWrapper(socketElt, socketFunc) {
* @param socketFunc var wrapper = {
* @returns {WebSocketWrapper} socket: null,
*/ messageQueue: [],
function createWebsocketWrapper(socketElt, socketFunc) { retryCount: 0,
var wrapper = {
socket: null, /** @type {Object<string, Function[]>} */
messageQueue: [], events: {},
retryCount: 0,
addEventListener: function(event, handler) {
/** @type {Object<string, Function[]>} */ if (this.socket) {
events: {}, this.socket.addEventListener(event, handler)
}
addEventListener: function (event, handler) {
if (this.socket) { if (!this.events[event]) {
this.socket.addEventListener(event, handler); this.events[event] = []
} }
if (!this.events[event]) { this.events[event].push(handler)
this.events[event] = []; },
}
sendImmediately: function(message, sendElt) {
this.events[event].push(handler); if (!this.socket) {
}, api.triggerErrorEvent()
}
sendImmediately: function (message, sendElt) { if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
if (!this.socket) { message,
api.triggerErrorEvent() socketWrapper: this.publicInterface
} })) {
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', { this.socket.send(message)
message: message, sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
socketWrapper: this.publicInterface 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, sendElt })
} else {
send: function (message, sendElt) { this.sendImmediately(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) {
handleQueuedMessages: function () { this.sendImmediately(queuedItem.message, queuedItem.sendElt)
while (this.messageQueue.length > 0) { this.messageQueue.shift()
var queuedItem = this.messageQueue[0] } else {
if (this.socket.readyState === this.socket.OPEN) { break
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()
init: function () { }
if (this.socket && this.socket.readyState === this.socket.OPEN) {
// Close discarded socket // Create a new WebSocket and event handlers
this.socket.close() /** @type {WebSocket} */
} var socket = socketFunc()
// Create a new WebSocket and event handlers // The event.type detail is added for interface conformance with the
/** @type {WebSocket} */ // other two lifecycle events (open and close) so a single handler method
var socket = socketFunc(); // can handle them polymorphically, if required.
api.triggerEvent(socketElt, 'htmx:wsConnecting', { event: { type: 'connecting' } })
// The event.type detail is added for interface conformance with the
// other two lifecycle events (open and close) so a single handler method this.socket = socket
// can handle them polymorphically, if required.
api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } }); socket.onopen = function(e) {
wrapper.retryCount = 0
this.socket = socket; api.triggerEvent(socketElt, 'htmx:wsOpen', { event: e, socketWrapper: wrapper.publicInterface })
wrapper.handleQueuedMessages()
socket.onopen = function (e) { }
wrapper.retryCount = 0;
api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface }); socket.onclose = function(e) {
wrapper.handleQueuedMessages(); // 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) {
socket.onclose = function (e) { var delay = getWebSocketReconnectDelay(wrapper.retryCount)
// If socket should not be connected, stop further attempts to establish connection setTimeout(function() {
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause. wrapper.retryCount += 1
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) { wrapper.init()
var delay = getWebSocketReconnectDelay(wrapper.retryCount); }, delay)
setTimeout(function () { }
wrapper.retryCount += 1;
wrapper.init(); // Notify client code that connection has been closed. Client code can inspect `event` field
}, delay); // to determine whether closure has been valid or abnormal
} api.triggerEvent(socketElt, 'htmx:wsClose', { event: e, socketWrapper: wrapper.publicInterface })
}
// Notify client code that connection has been closed. Client code can inspect `event` field
// to determine whether closure has been valid or abnormal socket.onerror = function(e) {
api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface }) api.triggerErrorEvent(socketElt, 'htmx:wsError', { error: e, socketWrapper: wrapper })
}; maybeCloseWebSocketSource(socketElt)
}
socket.onerror = function (e) {
api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper }); var events = this.events
maybeCloseWebSocketSource(socketElt); Object.keys(events).forEach(function(k) {
}; events[k].forEach(function(e) {
socket.addEventListener(k, e)
var events = this.events; })
Object.keys(events).forEach(function (k) { })
events[k].forEach(function (e) { },
socket.addEventListener(k, e);
}) close: function() {
}); this.socket.close()
}, }
}
close: function () {
this.socket.close() wrapper.init()
}
} wrapper.publicInterface = {
send: wrapper.send.bind(wrapper),
wrapper.init(); sendImmediately: wrapper.sendImmediately.bind(wrapper),
queue: wrapper.messageQueue
wrapper.publicInterface = { }
send: wrapper.send.bind(wrapper),
sendImmediately: wrapper.sendImmediately.bind(wrapper), return wrapper
queue: wrapper.messageQueue }
};
/**
return wrapper; * ensureWebSocketSend attaches trigger handles to elements with
} * "ws-send" attribute
* @param {HTMLElement} elt
/** */
* ensureWebSocketSend attaches trigger handles to elements with function ensureWebSocketSend(elt) {
* "ws-send" attribute var legacyAttribute = api.getAttributeValue(elt, 'hx-ws')
* @param {HTMLElement} elt if (legacyAttribute && legacyAttribute !== 'send') {
*/ return
function ensureWebSocketSend(elt) { }
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
if (legacyAttribute && legacyAttribute !== 'send') { var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
return; processWebSocketSend(webSocketParent, elt)
} }
var webSocketParent = api.getClosestMatch(elt, hasWebSocket) /**
processWebSocketSend(webSocketParent, elt); * hasWebSocket function checks if a node has webSocket instance attached
} * @param {HTMLElement} node
* @returns {boolean}
/** */
* hasWebSocket function checks if a node has webSocket instance attached function hasWebSocket(node) {
* @param {HTMLElement} node return api.getInternalData(node).webSocket != null
* @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
* processWebSocketSend adds event listeners to the <form> element so that */
* messages can be sent to the WebSocket server when the form is submitted. function processWebSocketSend(socketElt, sendElt) {
* @param {HTMLElement} socketElt var nodeData = api.getInternalData(sendElt)
* @param {HTMLElement} sendElt var triggerSpecs = api.getTriggerSpecs(sendElt)
*/ triggerSpecs.forEach(function(ts) {
function processWebSocketSend(socketElt, sendElt) { api.addTriggerHandler(sendElt, ts, nodeData, function(elt, evt) {
var nodeData = api.getInternalData(sendElt); if (maybeCloseWebSocketSource(socketElt)) {
var triggerSpecs = api.getTriggerSpecs(sendElt); return
triggerSpecs.forEach(function (ts) { }
api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
if (maybeCloseWebSocketSource(socketElt)) { /** @type {WebSocketWrapper} */
return; var socketWrapper = api.getInternalData(socketElt).webSocket
} var headers = api.getHeaders(sendElt, api.getTarget(sendElt))
var results = api.getInputValues(sendElt, 'post')
/** @type {WebSocketWrapper} */ var errors = results.errors
var socketWrapper = api.getInternalData(socketElt).webSocket; var rawParameters = Object.assign({}, results.values)
var headers = api.getHeaders(sendElt, api.getTarget(sendElt)); var expressionVars = api.getExpressionVars(sendElt)
var results = api.getInputValues(sendElt, 'post'); var allParameters = api.mergeObjects(rawParameters, expressionVars)
var errors = results.errors; var filteredParameters = api.filterValues(allParameters, sendElt)
var rawParameters = results.values;
var expressionVars = api.getExpressionVars(sendElt); var sendConfig = {
var allParameters = api.mergeObjects(rawParameters, expressionVars); parameters: filteredParameters,
var filteredParameters = api.filterValues(allParameters, sendElt); unfilteredParameters: allParameters,
headers,
var sendConfig = { errors,
parameters: filteredParameters,
unfilteredParameters: allParameters, triggeringEvent: evt,
headers: headers, messageBody: undefined,
errors: errors, socketWrapper: socketWrapper.publicInterface
}
triggeringEvent: evt,
messageBody: undefined, if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
socketWrapper: socketWrapper.publicInterface return
}; }
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) { if (errors && errors.length > 0) {
return; api.triggerEvent(elt, 'htmx:validation:halted', errors)
} return
}
if (errors && errors.length > 0) {
api.triggerEvent(elt, 'htmx:validation:halted', errors); var body = sendConfig.messageBody
return; if (body === undefined) {
} var toSend = Object.assign({}, sendConfig.parameters)
if (sendConfig.headers) { toSend.HEADERS = headers }
var body = sendConfig.messageBody; body = JSON.stringify(toSend)
if (body === undefined) { }
var toSend = Object.assign({}, sendConfig.parameters);
if (sendConfig.headers) socketWrapper.send(body, elt)
toSend['HEADERS'] = headers;
body = JSON.stringify(toSend); if (evt && api.shouldCancel(evt, elt)) {
} evt.preventDefault()
}
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) {
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects. /** @type {"full-jitter" | ((retryCount:number) => number)} */
* @param {number} retryCount // The number of retries that have already taken place var delay = htmx.config.wsReconnectDelay
* @returns {number} if (typeof delay === 'function') {
*/ return delay(retryCount)
function getWebSocketReconnectDelay(retryCount) { }
if (delay === 'full-jitter') {
/** @type {"full-jitter" | ((retryCount:number) => number)} */ var exp = Math.min(retryCount, 6)
var delay = htmx.config.wsReconnectDelay; var maxDelay = 1000 * Math.pow(2, exp)
if (typeof delay === 'function') { return maxDelay * Math.random()
return delay(retryCount); }
}
if (delay === 'full-jitter') { logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"')
var exp = Math.min(retryCount, 6); }
var maxDelay = 1000 * Math.pow(2, exp);
return maxDelay * Math.random(); /**
} * 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
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"'); * returns TRUE. If the element DOES EXIST, then no action is taken, and this function
} * returns FALSE.
*
/** * @param {*} elt
* maybeCloseWebSocketSource checks to the if the element that created the WebSocket * @returns
* 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 function maybeCloseWebSocketSource(elt) {
* returns FALSE. if (!api.bodyContains(elt)) {
* api.getInternalData(elt).webSocket.close()
* @param {*} elt return true
* @returns }
*/ return false
function maybeCloseWebSocketSource(elt) { }
if (!api.bodyContains(elt)) {
api.getInternalData(elt).webSocket.close(); /**
return true; * createWebSocket is the default method for creating new WebSocket objects.
} * it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
return false; *
} * @param {string} url
* @returns WebSocket
/** */
* createWebSocket is the default method for creating new WebSocket objects. function createWebSocket(url) {
* it is hoisted into htmx.createWebSocket to be overridden by the user, if needed. var sock = new WebSocket(url, [])
* sock.binaryType = htmx.config.wsBinaryType
* @param {string} url return sock
* @returns WebSocket }
*/
function createWebSocket(url) { /**
var sock = new WebSocket(url, []); * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
sock.binaryType = htmx.config.wsBinaryType; *
return sock; * @param {HTMLElement} elt
} * @param {string} attributeName
*/
/** function queryAttributeOnThisOrChildren(elt, attributeName) {
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT. var result = []
*
* @param {HTMLElement} elt // If the parent element also contains the requested attribute, then add it to the results too.
* @param {string} attributeName if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, 'hx-ws')) {
*/ result.push(elt)
function queryAttributeOnThisOrChildren(elt, attributeName) { }
var result = [] // Search all child nodes that match the requested attribute
elt.querySelectorAll('[' + attributeName + '], [data-' + attributeName + '], [data-hx-ws], [hx-ws]').forEach(function(node) {
// If the parent element also contains the requested attribute, then add it to the results too. result.push(node)
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) { })
result.push(elt);
} return result
}
// 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) * @template T
}) * @param {T[]} arr
* @param {(T) => void} func
return result */
} function forEach(arr, func) {
if (arr) {
/** for (var i = 0; i < arr.length; i++) {
* @template T func(arr[i])
* @param {T[]} arr }
* @param {(T) => void} func }
*/ }
function forEach(arr, func) { })()
if (arr) {
for (var i = 0; i < arr.length; i++) {
func(arr[i]);
}
}
}
})();

File diff suppressed because one or more lines are too long