aboutsummaryrefslogtreecommitdiff
path: root/static/js/ext/preload.js
blob: ac3ef5c9a4e6919132de873702ba262f26695ff6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
if (htmx.version && !htmx.version.startsWith("1.")) {
	console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
		".  It is recommended that you move to the version of this extension found on https://htmx.org/extensions")
}
// 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)
		})
	}
})