gooby htmx
Signed-off-by: ari melody <ari@arimelody.me>
This commit is contained in:
parent
b42b37ff9c
commit
1cbcece3d2
|
@ -1,141 +0,0 @@
|
||||||
//==========================================================
|
|
||||||
// head-support.js
|
|
||||||
//
|
|
||||||
// An extension to htmx 1.0 to add head tag merging.
|
|
||||||
//==========================================================
|
|
||||||
(function(){
|
|
||||||
|
|
||||||
var api = null;
|
|
||||||
|
|
||||||
function log() {
|
|
||||||
//console.log(arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeHead(newContent, defaultMergeStrategy) {
|
|
||||||
|
|
||||||
if (newContent && newContent.indexOf('<head') > -1) {
|
|
||||||
const htmlDoc = document.createElement("html");
|
|
||||||
// remove svgs to avoid conflicts
|
|
||||||
var contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
|
|
||||||
// extract head tag
|
|
||||||
var headTag = contentWithSvgsRemoved.match(/(<head(\s[^>]*>|>)([\s\S]*?)<\/head>)/im);
|
|
||||||
|
|
||||||
// if the head tag exists...
|
|
||||||
if (headTag) {
|
|
||||||
|
|
||||||
var added = []
|
|
||||||
var removed = []
|
|
||||||
var preserved = []
|
|
||||||
var nodesToAppend = []
|
|
||||||
|
|
||||||
htmlDoc.innerHTML = headTag;
|
|
||||||
var newHeadTag = htmlDoc.querySelector("head");
|
|
||||||
var currentHead = document.head;
|
|
||||||
|
|
||||||
if (newHeadTag == null) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// put all new head elements into a Map, by their outerHTML
|
|
||||||
var srcToNewHeadNodes = new Map();
|
|
||||||
for (const newHeadChild of newHeadTag.children) {
|
|
||||||
srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// determine merge strategy
|
|
||||||
var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
|
|
||||||
|
|
||||||
// get the current head
|
|
||||||
for (const currentHeadElt of currentHead.children) {
|
|
||||||
|
|
||||||
// If the current head element is in the map
|
|
||||||
var inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
|
|
||||||
var isReAppended = currentHeadElt.getAttribute("hx-head") === "re-eval";
|
|
||||||
var isPreserved = api.getAttributeValue(currentHeadElt, "hx-preserve") === "true";
|
|
||||||
if (inNewContent || isPreserved) {
|
|
||||||
if (isReAppended) {
|
|
||||||
// remove the current version and let the new version replace it and re-execute
|
|
||||||
removed.push(currentHeadElt);
|
|
||||||
} else {
|
|
||||||
// this element already exists and should not be re-appended, so remove it from
|
|
||||||
// the new content map, preserving it in the DOM
|
|
||||||
srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
|
|
||||||
preserved.push(currentHeadElt);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (mergeStrategy === "append") {
|
|
||||||
// we are appending and this existing element is not new content
|
|
||||||
// so if and only if it is marked for re-append do we do anything
|
|
||||||
if (isReAppended) {
|
|
||||||
removed.push(currentHeadElt);
|
|
||||||
nodesToAppend.push(currentHeadElt);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// if this is a merge, we remove this content since it is not in the new head
|
|
||||||
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: currentHeadElt}) !== false) {
|
|
||||||
removed.push(currentHeadElt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the tremaining new head elements in the Map into the
|
|
||||||
// nodes to append to the head tag
|
|
||||||
nodesToAppend.push(...srcToNewHeadNodes.values());
|
|
||||||
log("to append: ", nodesToAppend);
|
|
||||||
|
|
||||||
for (const newNode of nodesToAppend) {
|
|
||||||
log("adding: ", newNode);
|
|
||||||
var newElt = document.createRange().createContextualFragment(newNode.outerHTML);
|
|
||||||
log(newElt);
|
|
||||||
if (api.triggerEvent(document.body, "htmx:addingHeadElement", {headElement: newElt}) !== false) {
|
|
||||||
currentHead.appendChild(newElt);
|
|
||||||
added.push(newElt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove all removed elements, after we have appended the new elements to avoid
|
|
||||||
// additional network requests for things like style sheets
|
|
||||||
for (const removedElement of removed) {
|
|
||||||
if (api.triggerEvent(document.body, "htmx:removingHeadElement", {headElement: removedElement}) !== false) {
|
|
||||||
currentHead.removeChild(removedElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.triggerEvent(document.body, "htmx:afterHeadMerge", {added: added, kept: preserved, removed: removed});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
htmx.defineExtension("head-support", {
|
|
||||||
init: function(apiRef) {
|
|
||||||
// store a reference to the internal API.
|
|
||||||
api = apiRef;
|
|
||||||
|
|
||||||
htmx.on('htmx:afterSwap', function(evt){
|
|
||||||
var serverResponse = evt.detail.xhr.response;
|
|
||||||
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
|
|
||||||
mergeHead(serverResponse, evt.detail.boosted ? "merge" : "append");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
htmx.on('htmx:historyRestore', function(evt){
|
|
||||||
if (api.triggerEvent(document.body, "htmx:beforeHeadMerge", evt.detail)) {
|
|
||||||
if (evt.detail.cacheMiss) {
|
|
||||||
mergeHead(evt.detail.serverResponse, "merge");
|
|
||||||
} else {
|
|
||||||
mergeHead(evt.detail.item.head, "merge");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
htmx.on('htmx:historyItemCreated', function(evt){
|
|
||||||
var historyItem = evt.detail.item;
|
|
||||||
historyItem.head = document.head.outerHTML;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
})()
|
|
|
@ -1,147 +0,0 @@
|
||||||
// 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();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
event.target.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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
File diff suppressed because it is too large
Load diff
1
public/script/lib/htmx.min.js
vendored
1
public/script/lib/htmx.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue