Proper pre-loading and deps
This commit is contained in:
parent
24bf04aa7a
commit
561905aabb
13 changed files with 880 additions and 825 deletions
|
@ -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}}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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}}
|
2
public/js/htmx.base.js
vendored
2
public/js/htmx.base.js
vendored
File diff suppressed because one or more lines are too long
141
public/js/htmx.preload.js
vendored
Normal file
141
public/js/htmx.preload.js
vendored
Normal 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
607
public/js/htmx.sse.js
vendored
|
@ -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
931
public/js/htmx.ws.js
vendored
|
@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
2
public/js/hyperscript.js
vendored
2
public/js/hyperscript.js
vendored
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue