Skip to content

Commit

Permalink
Vendor HTMX, _hyperscript, and missing.css
Browse files Browse the repository at this point in the history
How did I miss doing this before?
  • Loading branch information
sheepman4267 committed Sep 7, 2024
1 parent af2e2af commit ca279b8
Show file tree
Hide file tree
Showing 6 changed files with 378 additions and 6 deletions.
1 change: 1 addition & 0 deletions OpenShow/core/static/css/missing.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions OpenShow/core/static/js/htmx.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions OpenShow/core/static/js/hyperscript.js

Large diffs are not rendered by default.

369 changes: 369 additions & 0 deletions OpenShow/core/static/js/sse.js
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;
}

})();
6 changes: 3 additions & 3 deletions OpenShow/core/templates/core/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
6 changes: 3 additions & 3 deletions OpenShow/slides/editor/templates/editor/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down

0 comments on commit ca279b8

Please sign in to comment.