-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Vendor HTMX, _hyperscript, and missing.css
How did I miss doing this before?
- Loading branch information
1 parent
af2e2af
commit ca279b8
Showing
6 changed files
with
378 additions
and
6 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,369 @@ | ||
/* | ||
Server Sent Events Extension | ||
============================ | ||
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions. | ||
*/ | ||
|
||
(function() { | ||
|
||
/** @type {import("../htmx").HtmxInternalApi} */ | ||
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 | ||
if (htmx.createEventSource == undefined) { | ||
htmx.createEventSource = createEventSource; | ||
} | ||
}, | ||
|
||
/** | ||
* onEvent handles all events passed to this extension. | ||
* | ||
* @param {string} name | ||
* @param {Event} evt | ||
* @returns void | ||
*/ | ||
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 | ||
if (internalData.sseEventSource) { | ||
internalData.sseEventSource.close(); | ||
} | ||
|
||
return; | ||
|
||
// Try to create EventSources when elements are processed | ||
case "htmx:afterProcessNode": | ||
ensureEventSourceOnElement(parent); | ||
} | ||
} | ||
}); | ||
|
||
/////////////////////////////////////////////// | ||
// HELPER FUNCTIONS | ||
/////////////////////////////////////////////// | ||
|
||
|
||
/** | ||
* createEventSource is the default method for creating new EventSource objects. | ||
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed. | ||
* | ||
* @param {string} url | ||
* @returns EventSource | ||
*/ | ||
function createEventSource(url) { | ||
return new EventSource(url, { withCredentials: true }); | ||
} | ||
|
||
function splitOnWhitespace(trigger) { | ||
return trigger.trim().split(/\s+/); | ||
} | ||
|
||
function getLegacySSEURL(elt) { | ||
var legacySSEValue = api.getAttributeValue(elt, "hx-sse"); | ||
if (legacySSEValue) { | ||
var values = splitOnWhitespace(legacySSEValue); | ||
for (var i = 0; i < values.length; i++) { | ||
var value = values[i].split(/:(.+)/); | ||
if (value[0] === "connect") { | ||
return value[1]; | ||
} | ||
} | ||
} | ||
} | ||
|
||
function getLegacySSESwaps(elt) { | ||
var legacySSEValue = api.getAttributeValue(elt, "hx-sse"); | ||
var returnArr = []; | ||
if (legacySSEValue != null) { | ||
var values = splitOnWhitespace(legacySSEValue); | ||
for (var i = 0; i < values.length; i++) { | ||
var value = values[i].split(/:(.+)/); | ||
if (value[0] === "swap") { | ||
returnArr.push(value[1]); | ||
} | ||
} | ||
} | ||
return returnArr; | ||
} | ||
|
||
/** | ||
* 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 | ||
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"); | ||
if (sseSwapAttr) { | ||
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; | ||
} | ||
|
||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,10 +6,10 @@ | |
<meta charset="UTF-8"> | ||
<title>{% block title %}OpenShow{% endblock %}</title> | ||
{# <link rel="stylesheet" href="{% static 'hat/hat.css' %}"#} | ||
<link rel="stylesheet" href="https://unpkg.com/missing.css@1.1.1"> | ||
<link rel="stylesheet" href="{% static 'css/missing.css' %}"> | ||
<link rel="stylesheet" href="{% static 'core/extras.css' %}"> | ||
<script src="https://unpkg.com/htmx.[email protected]"></script> | ||
<script src="https://unpkg.com/hyperscript.[email protected]"></script> | ||
<script src="{% static 'js/htmx.js' %}"></script> | ||
<script src="{% static 'js/hyperscript.js' %}"></script> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
{% block extra_js %} | ||
{% endblock %} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,9 +2,9 @@ | |
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<script src="https://unpkg.com/htmx.[email protected]" integrity="sha384-Bj8qm/6B+71E6FQSySofJOUjA/gq330vEqjFx9LakWybUySyI1IQHwPtbTU7bNwx" crossorigin="anonymous"></script> | ||
<script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script> | ||
<script src="https://unpkg.com/hyperscript.[email protected]"></script> | ||
<script src="{% static 'js/htmx.js' %}"></script> | ||
<script src="{% static 'js/sse.js' %}"></script> | ||
<script src="{% static 'js/hyperscript.js' %}"></script> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
<title>{% block title %}OpenShow Editor{% endblock %}</title> | ||
|