Merge pull request #273 from benpate/pullrequest-preload

Extension: Preload Links
This commit is contained in:
1cg
2021-01-01 17:02:49 -07:00
committed by GitHub
4 changed files with 193 additions and 0 deletions

149
src/ext/preload.js Normal file
View File

@@ -0,0 +1,149 @@
// 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 detauls
htmx.defineExtension("preload", {
onEvent: function(name, event) {
// Only take actions on "htmx:processedNode"
if (name !== "htmx:processedNode") {
return;
}
// SOME HELPER FUNCTIONS WE'LL NEED ALONG THE WAY
// closest gets the closest token value in the preload attribute of the node, so
// calling closest(node, 'wait') on this node: <div preload="on:mouseover wait:100ms">
// would return "100ms".
var closest = function(node, token) {
// Handle undefined inputs gracefully
if (node == undefined) {return undefined;}
// Get the attribute
var attr = node.getAttribute("preload");
// If we find a token in this attribute value, return it. Otherwise, search parent elements.
return parseToken(attr, token) || closest(node.parentElement, token);
}
// parseToken finds the value for a specific name:value pair
// embedded in an input string.
// For example, parseToken("one:1 two:2 three:3", "two") => "2"
var parseToken = function(input, name) {
// Handle undefined inputs gracefully
if (input == undefined) {
return undefined;
}
// Split options on whitespace
var options = input.split(/\s/);
// Search all options for a matching name...
for (var i = 0 ; i < options.length ; i++) {
var option = options[i].split(":");
if (option[0] === name) {
return option[1]; // ... return token value
}
}
// Nothing found, return undefined
return undefined;
}
// 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) {
return function() {
// If this value has already been loaded, then do not try again.
if (node.preloadState !== "READY") {
return;
}
var done = function() {
node.preloadState = "DONE"
}
// 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
if (node.getAttribute("hx-get")) {
htmx.ajax("GET", node.getAttribute("hx-get"), {handler:done});
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 = done;
r.send();
}
}
}
// Search for all child nodes that have a "preload" attribute. Making this explicit
// ensures that all elements in a large DOM tree are not accidentally targeted
event.target.querySelectorAll("[preload]").forEach(function(node) {
// Guarantee that we only process each node once.
if (node.preloadState !== undefined) {
return;
}
// This means that the node has been initialized
node.preloadState = "PAUSE";
// Get event name. Default="mousedown"
var on = closest(node, "on") || "mousedown";
// One-Line monstrosity to get wait time. For mouseover events, Default=100ms. All others, default=0ms.
var wait = htmx.parseInterval(closest(node, "wait")) || ((on == "mouseover") ? 100 : 0);
// Special handling for "load" events. No EventListener necessary, just trigger the timer now.
if (on === "load") {
node.preloadState = "READY"; // Required for the `load` function to trigger
window.setTimeout(load(node), wait);
return;
}
// 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"; // Requred for the `load` function to trigger
window.setTimeout(load(node), wait);
}
})
switch (on) {
// Special handling for "mouseover" events
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;
// Special handling for "mousedown" events
case "mousedown":
// Mirror `touchstart` events (fires immediately)
node.addEventListener("touchstart", load(node));
break;
}
})
}
})

View File

@@ -0,0 +1 @@
111

View File

@@ -0,0 +1 @@
222

42
test/manual/preload.html Normal file
View File

@@ -0,0 +1,42 @@
<html>
<head>
<script src="../../src/htmx.js"></script>
<script src="../../src/ext/preload.js"></script>
<style>
div.container {
border:solid 1px black;
margin-bottom:25px;
padding:25px;
}
</style>
</head>
<body hx-ext="preload">
<div class="container">
<h4>Buttons</h4>
<button hx-get="preload-1.html?mouseover" preload="on:mouseover wait:1.5s">Trigger mouseover after 1.5s</button><br>
<button hx-get="preload-2.html?mouseover" preload="on:mouseover">Trigger on mouseover after 100ms</button><br>
</div>
<div class="container">
<a href="preload-1.html?mousedown" preload="on:mousedown wait:1s">Trigger on mousedown after 1s</a><br>
<a href="preload-2.html?mousedown" preload="on:mousedown wait:0">Trigger on mousedown immediately</a><br>
</div>
<div class="container" preload="on:load wait:3s">
<a href="preload-1.html?onload=true" preload>Trigger on load after 3s</a><br>
<a href="preload-2.html?onload=true" preload="wait:200ms">Trigger on load after 200ms</a><br>
</div>
<div class="container" hx-get="preload-1.html?complex=true" preload="on:mouseover wait:1s">
<div>This is a complex object</div>
<div>With many sub-objects</div>
<div>Preload should only be canceled</div>
<div>If we leave the whole parent</div>
<div>Triggers on:mouseover after 1s</div>
</div>
</body>
</html>