Browse code

Update htmx lib

Benjamin Roth authored on08/03/2024 10:15:01
Showing1 changed files
... ...
@@ -5,7 +5,7 @@ This extension adds support for Server Sent Events to htmx.  See /www/extensions
5 5
 
6 6
 */
7 7
 
8
-(function(){
8
+(function() {
9 9
 
10 10
 	/** @type {import("../htmx").HtmxInternalApi} */
11 11
 	var api;
... ...
@@ -39,17 +39,19 @@ This extension adds support for Server Sent Events to htmx.  See /www/extensions
39 39
 
40 40
 			switch (name) {
41 41
 
42
-			// Try to remove remove an EventSource when elements are removed
43
-			case "htmx:beforeCleanupElement":
44
-				var internalData = api.getInternalData(evt.target)
45
-				if (internalData.sseEventSource) {
46
-					internalData.sseEventSource.close();
47
-				}
48
-				return;
42
+				case "htmx:beforeCleanupElement":
43
+					var internalData = api.getInternalData(evt.target)
44
+					// Try to remove remove an EventSource when elements are removed
45
+					if (internalData.sseEventSource) {
46
+						internalData.sseEventSource.close();
47
+					}
49 48
 
50
-			// Try to create EventSources when elements are processed
51
-			case "htmx:afterProcessNode":
52
-				createEventSourceOnElement(evt.target);
49
+					return;
50
+
51
+				// Try to create EventSources when elements are processed
52
+				case "htmx:afterProcessNode":
53
+					ensureEventSourceOnElement(evt.target);
54
+					registerSSE(evt.target);
53 55
 			}
54 56
 		}
55 57
 	});
... ...
@@ -66,8 +68,8 @@ This extension adds support for Server Sent Events to htmx.  See /www/extensions
66 68
 	 * @param {string} url 
67 69
 	 * @returns EventSource
68 70
 	 */
69
-	 function createEventSource(url) {
70
-		return new EventSource(url, {withCredentials:true});
71
+	function createEventSource(url) {
72
+		return new EventSource(url, { withCredentials: true });
71 73
 	}
72 74
 
73 75
 	function splitOnWhitespace(trigger) {
... ...
@@ -90,7 +92,7 @@ This extension adds support for Server Sent Events to htmx.  See /www/extensions
90 92
 	function getLegacySSESwaps(elt) {
91 93
 		var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
92 94
 		var returnArr = [];
93
-		if (legacySSEValue) {
95
+		if (legacySSEValue != null) {
94 96
 			var values = splitOnWhitespace(legacySSEValue);
95 97
 			for (var i = 0; i < values.length; i++) {
96 98
 				var value = values[i].split(/:(.+)/);
... ...
@@ -103,63 +105,24 @@ This extension adds support for Server Sent Events to htmx.  See /www/extensions
103 105
 	}
104 106
 
105 107
 	/**
106
-	 * createEventSourceOnElement creates a new EventSource connection on the provided element.
107
-	 * If a usable EventSource already exists, then it is returned.  If not, then a new EventSource
108
-	 * is created and stored in the element's internalData.
108
+	 * registerSSE looks for attributes that can contain sse events, right 
109
+	 * now hx-trigger and sse-swap and adds listeners based on these attributes too
110
+	 * the closest event source
111
+	 *
109 112
 	 * @param {HTMLElement} elt
110
-	 * @param {number} retryCount
111
-	 * @returns {EventSource | null}
112 113
 	 */
113
-	function createEventSourceOnElement(elt, retryCount) {
114
-
115
-		if (elt == null) {
116
-			return null;
114
+	function registerSSE(elt) {
115
+		// Find closest existing event source
116
+		var sourceElement = api.getClosestMatch(elt, hasEventSource);
117
+		if (sourceElement == null) {
118
+			// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
119
+			return null; // no eventsource in parentage, orphaned element
117 120
 		}
118 121
 
119
-		var internalData = api.getInternalData(elt);
120
-
121
-		// get URL from element's attribute
122
-		var sseURL = api.getAttributeValue(elt, "sse-connect");
123
-
124
-
125
-		if (sseURL == undefined) {
126
-			var legacyURL = getLegacySSEURL(elt)
127
-			if (legacyURL) {
128
-				sseURL = legacyURL;
129
-			} else {
130
-				return null;
131
-			}
132
-		}
133
-
134
-		// Connect to the EventSource
135
-		var source = htmx.createEventSource(sseURL);
136
-		internalData.sseEventSource = source;
137
-
138
-		// Create event handlers
139
-		source.onerror = function (err) {
140
-
141
-			// Log an error event
142
-			api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source});
122
+		// Set internalData and source
123
+		var internalData = api.getInternalData(sourceElement);
124
+		var source = internalData.sseEventSource;
143 125
 
144
-			// If parent no longer exists in the document, then clean up this EventSource
145
-			if (maybeCloseSSESource(elt)) {
146
-				return;
147
-			}
148
-
149
-			// Otherwise, try to reconnect the EventSource
150
-			if (source.readyState === EventSource.CLOSED) {
151
-				retryCount = retryCount || 0;
152
-				var timeout = Math.random() * (2 ^ retryCount) * 500;
153
-				window.setTimeout(function() {
154
-					createEventSourceOnElement(elt, Math.min(7, retryCount+1));
155
-				}, timeout);
156
-			}			
157
-		};
158
-
159
-		source.onopen = function (evt) {
160
-			api.triggerEvent(elt, "htmx::sseOpen", {source: source});
161
-		}
162
-		
163 126
 		// Add message handlers for every `sse-swap` attribute
164 127
 		queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
165 128
 
... ...
@@ -170,23 +133,27 @@ This extension adds support for Server Sent Events to htmx.  See /www/extensions
170 133
 				var sseEventNames = getLegacySSESwaps(child);
171 134
 			}
172 135
 
173
-			for (var i = 0 ; i < sseEventNames.length ; i++) {
136
+			for (var i = 0; i < sseEventNames.length; i++) {
174 137
 				var sseEventName = sseEventNames[i].trim();
175 138
 				var listener = function(event) {
176 139
 
177
-					// If the parent is missing then close SSE and remove listener
178
-					if (maybeCloseSSESource(elt)) {
179
-						source.removeEventListener(sseEventName, listener);
140
+					// If the source is missing then close SSE
141
+					if (maybeCloseSSESource(sourceElement)) {
180 142
 						return;
181 143
 					}
182 144
 
145
+					// If the body no longer contains the element, remove the listener
146
+					if (!api.bodyContains(child)) {
147
+						source.removeEventListener(sseEventName, listener);
148
+					}
149
+
183 150
 					// swap the response into the DOM and trigger a notification
184 151
 					swap(child, event.data);
185 152
 					api.triggerEvent(elt, "htmx:sseMessage", event);
186 153
 				};
187 154
 
188 155
 				// Register the new listener
189
-				api.getInternalData(elt).sseEventListener = listener;
156
+				api.getInternalData(child).sseEventListener = listener;
190 157
 				source.addEventListener(sseEventName, listener);
191 158
 			}
192 159
 		});
... ...
@@ -203,24 +170,86 @@ This extension adds support for Server Sent Events to htmx.  See /www/extensions
203 170
 			if (sseEventName.slice(0, 4) != "sse:") {
204 171
 				return;
205 172
 			}
173
+			
174
+			// remove the sse: prefix from here on out
175
+			sseEventName = sseEventName.substr(4);
206 176
 
207
-			var listener = function(event) {
177
+			var listener = function() {
178
+				if (maybeCloseSSESource(sourceElement)) {
179
+					return
180
+				}
208 181
 
209
-				// If parent is missing, then close SSE and remove listener
210
-				if (maybeCloseSSESource(elt)) {
182
+				if (!api.bodyContains(child)) {
211 183
 					source.removeEventListener(sseEventName, listener);
212
-					return;
213 184
 				}
185
+			}
186
+		});
187
+	}
188
+
189
+	/**
190
+	 * ensureEventSourceOnElement creates a new EventSource connection on the provided element.
191
+	 * If a usable EventSource already exists, then it is returned.  If not, then a new EventSource
192
+	 * is created and stored in the element's internalData.
193
+	 * @param {HTMLElement} elt
194
+	 * @param {number} retryCount
195
+	 * @returns {EventSource | null}
196
+	 */
197
+	function ensureEventSourceOnElement(elt, retryCount) {
198
+
199
+		if (elt == null) {
200
+			return null;
201
+		}
214 202
 
215
-				// Trigger events to be handled by the rest of htmx
216
-				htmx.trigger(child, sseEventName, event);
217
-				htmx.trigger(child, "htmx:sseMessage", event);
203
+		// handle extension source creation attribute
204
+		queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
205
+			var sseURL = api.getAttributeValue(child, "sse-connect");
206
+			if (sseURL == null) {
207
+				return;
218 208
 			}
219 209
 
220
-			// Register the new listener
221
-			api.getInternalData(elt).sseEventListener = listener;
222
-			source.addEventListener(sseEventName.slice(4), listener);
210
+			ensureEventSource(child, sseURL, retryCount);
223 211
 		});
212
+
213
+		// handle legacy sse, remove for HTMX2
214
+		queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
215
+			var sseURL = getLegacySSEURL(child);
216
+			if (sseURL == null) {
217
+				return;
218
+			}
219
+
220
+			ensureEventSource(child, sseURL, retryCount);
221
+		});
222
+
223
+	}
224
+
225
+	function ensureEventSource(elt, url, retryCount) {
226
+		var source = htmx.createEventSource(url);
227
+
228
+		source.onerror = function(err) {
229
+
230
+			// Log an error event
231
+			api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
232
+
233
+			// If parent no longer exists in the document, then clean up this EventSource
234
+			if (maybeCloseSSESource(elt)) {
235
+				return;
236
+			}
237
+
238
+			// Otherwise, try to reconnect the EventSource
239
+			if (source.readyState === EventSource.CLOSED) {
240
+				retryCount = retryCount || 0;
241
+				var timeout = Math.random() * (2 ^ retryCount) * 500;
242
+				window.setTimeout(function() {
243
+					ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
244
+				}, timeout);
245
+			}
246
+		};
247
+
248
+		source.onopen = function(evt) {
249
+			api.triggerEvent(elt, "htmx:sseOpen", { source: source });
250
+		}
251
+
252
+		api.getInternalData(elt).sseEventSource = source;
224 253
 	}
225 254
 
226 255
 	/**
... ...
@@ -253,12 +282,12 @@ This extension adds support for Server Sent Events to htmx.  See /www/extensions
253 282
 		var result = [];
254 283
 
255 284
 		// If the parent element also contains the requested attribute, then add it to the results too.
256
-		if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) {
285
+		if (api.hasAttribute(elt, attributeName)) {
257 286
 			result.push(elt);
258 287
 		}
259 288
 
260 289
 		// Search all child nodes that match the requested attribute
261
-		elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) {
290
+		elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
262 291
 			result.push(node);
263 292
 		});
264 293
 
... ...
@@ -281,7 +310,7 @@ This extension adds support for Server Sent Events to htmx.  See /www/extensions
281 310
 
282 311
 		api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
283 312
 
284
-		settleInfo.elts.forEach(function (elt) {
313
+		settleInfo.elts.forEach(function(elt) {
285 314
 			if (elt.classList) {
286 315
 				elt.classList.add(htmx.config.settlingClass);
287 316
 			}
... ...
@@ -306,11 +335,11 @@ This extension adds support for Server Sent Events to htmx.  See /www/extensions
306 335
 	function doSettle(settleInfo) {
307 336
 
308 337
 		return function() {
309
-			settleInfo.tasks.forEach(function (task) {
338
+			settleInfo.tasks.forEach(function(task) {
310 339
 				task.call();
311 340
 			});
312 341
 
313
-			settleInfo.elts.forEach(function (elt) {
342
+			settleInfo.elts.forEach(function(elt) {
314 343
 				if (elt.classList) {
315 344
 					elt.classList.remove(htmx.config.settlingClass);
316 345
 				}
... ...
@@ -319,4 +348,8 @@ This extension adds support for Server Sent Events to htmx.  See /www/extensions
319 348
 		}
320 349
 	}
321 350
 
322
-})();
323 351
\ No newline at end of file
352
+	function hasEventSource(node) {
353
+		return api.getInternalData(node).sseEventSource != null;
354
+	}
355
+
356
+})();
Browse code

Initial htmx npm packages installation

Benjamin Roth authored on25/05/2023 09:52:13
Showing1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,322 @@
1
+/*
2
+Server Sent Events Extension
3
+============================
4
+This extension adds support for Server Sent Events to htmx.  See /www/extensions/sse.md for usage instructions.
5
+
6
+*/
7
+
8
+(function(){
9
+
10
+	/** @type {import("../htmx").HtmxInternalApi} */
11
+	var api;
12
+
13
+	htmx.defineExtension("sse", {
14
+
15
+		/**
16
+		 * Init saves the provided reference to the internal HTMX API.
17
+		 * 
18
+		 * @param {import("../htmx").HtmxInternalApi} api 
19
+		 * @returns void
20
+		 */
21
+		init: function(apiRef) {
22
+			// store a reference to the internal API.
23
+			api = apiRef;
24
+
25
+			// set a function in the public API for creating new EventSource objects
26
+			if (htmx.createEventSource == undefined) {
27
+				htmx.createEventSource = createEventSource;
28
+			}
29
+		},
30
+
31
+		/**
32
+		 * onEvent handles all events passed to this extension.
33
+		 * 
34
+		 * @param {string} name 
35
+		 * @param {Event} evt 
36
+		 * @returns void
37
+		 */
38
+		onEvent: function(name, evt) {
39
+
40
+			switch (name) {
41
+
42
+			// Try to remove remove an EventSource when elements are removed
43
+			case "htmx:beforeCleanupElement":
44
+				var internalData = api.getInternalData(evt.target)
45
+				if (internalData.sseEventSource) {
46
+					internalData.sseEventSource.close();
47
+				}
48
+				return;
49
+
50
+			// Try to create EventSources when elements are processed
51
+			case "htmx:afterProcessNode":
52
+				createEventSourceOnElement(evt.target);
53
+			}
54
+		}
55
+	});
56
+
57
+	///////////////////////////////////////////////
58
+	// HELPER FUNCTIONS
59
+	///////////////////////////////////////////////
60
+
61
+
62
+	/**
63
+	 * createEventSource is the default method for creating new EventSource objects.
64
+	 * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
65
+	 * 
66
+	 * @param {string} url 
67
+	 * @returns EventSource
68
+	 */
69
+	 function createEventSource(url) {
70
+		return new EventSource(url, {withCredentials:true});
71
+	}
72
+
73
+	function splitOnWhitespace(trigger) {
74
+		return trigger.trim().split(/\s+/);
75
+	}
76
+
77
+	function getLegacySSEURL(elt) {
78
+		var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
79
+		if (legacySSEValue) {
80
+			var values = splitOnWhitespace(legacySSEValue);
81
+			for (var i = 0; i < values.length; i++) {
82
+				var value = values[i].split(/:(.+)/);
83
+				if (value[0] === "connect") {
84
+					return value[1];
85
+				}
86
+			}
87
+		}
88
+	}
89
+
90
+	function getLegacySSESwaps(elt) {
91
+		var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
92
+		var returnArr = [];
93
+		if (legacySSEValue) {
94
+			var values = splitOnWhitespace(legacySSEValue);
95
+			for (var i = 0; i < values.length; i++) {
96
+				var value = values[i].split(/:(.+)/);
97
+				if (value[0] === "swap") {
98
+					returnArr.push(value[1]);
99
+				}
100
+			}
101
+		}
102
+		return returnArr;
103
+	}
104
+
105
+	/**
106
+	 * createEventSourceOnElement creates a new EventSource connection on the provided element.
107
+	 * If a usable EventSource already exists, then it is returned.  If not, then a new EventSource
108
+	 * is created and stored in the element's internalData.
109
+	 * @param {HTMLElement} elt
110
+	 * @param {number} retryCount
111
+	 * @returns {EventSource | null}
112
+	 */
113
+	function createEventSourceOnElement(elt, retryCount) {
114
+
115
+		if (elt == null) {
116
+			return null;
117
+		}
118
+
119
+		var internalData = api.getInternalData(elt);
120
+
121
+		// get URL from element's attribute
122
+		var sseURL = api.getAttributeValue(elt, "sse-connect");
123
+
124
+
125
+		if (sseURL == undefined) {
126
+			var legacyURL = getLegacySSEURL(elt)
127
+			if (legacyURL) {
128
+				sseURL = legacyURL;
129
+			} else {
130
+				return null;
131
+			}
132
+		}
133
+
134
+		// Connect to the EventSource
135
+		var source = htmx.createEventSource(sseURL);
136
+		internalData.sseEventSource = source;
137
+
138
+		// Create event handlers
139
+		source.onerror = function (err) {
140
+
141
+			// Log an error event
142
+			api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source});
143
+
144
+			// If parent no longer exists in the document, then clean up this EventSource
145
+			if (maybeCloseSSESource(elt)) {
146
+				return;
147
+			}
148
+
149
+			// Otherwise, try to reconnect the EventSource
150
+			if (source.readyState === EventSource.CLOSED) {
151
+				retryCount = retryCount || 0;
152
+				var timeout = Math.random() * (2 ^ retryCount) * 500;
153
+				window.setTimeout(function() {
154
+					createEventSourceOnElement(elt, Math.min(7, retryCount+1));
155
+				}, timeout);
156
+			}			
157
+		};
158
+
159
+		source.onopen = function (evt) {
160
+			api.triggerEvent(elt, "htmx::sseOpen", {source: source});
161
+		}
162
+		
163
+		// Add message handlers for every `sse-swap` attribute
164
+		queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
165
+
166
+			var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
167
+			if (sseSwapAttr) {
168
+				var sseEventNames = sseSwapAttr.split(",");
169
+			} else {
170
+				var sseEventNames = getLegacySSESwaps(child);
171
+			}
172
+
173
+			for (var i = 0 ; i < sseEventNames.length ; i++) {
174
+				var sseEventName = sseEventNames[i].trim();
175
+				var listener = function(event) {
176
+
177
+					// If the parent is missing then close SSE and remove listener
178
+					if (maybeCloseSSESource(elt)) {
179
+						source.removeEventListener(sseEventName, listener);
180
+						return;
181
+					}
182
+
183
+					// swap the response into the DOM and trigger a notification
184
+					swap(child, event.data);
185
+					api.triggerEvent(elt, "htmx:sseMessage", event);
186
+				};
187
+
188
+				// Register the new listener
189
+				api.getInternalData(elt).sseEventListener = listener;
190
+				source.addEventListener(sseEventName, listener);
191
+			}
192
+		});
193
+
194
+		// Add message handlers for every `hx-trigger="sse:*"` attribute
195
+		queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
196
+
197
+			var sseEventName = api.getAttributeValue(child, "hx-trigger");
198
+			if (sseEventName == null) {
199
+				return;
200
+			}
201
+
202
+			// Only process hx-triggers for events with the "sse:" prefix
203
+			if (sseEventName.slice(0, 4) != "sse:") {
204
+				return;
205
+			}
206
+
207
+			var listener = function(event) {
208
+
209
+				// If parent is missing, then close SSE and remove listener
210
+				if (maybeCloseSSESource(elt)) {
211
+					source.removeEventListener(sseEventName, listener);
212
+					return;
213
+				}
214
+
215
+				// Trigger events to be handled by the rest of htmx
216
+				htmx.trigger(child, sseEventName, event);
217
+				htmx.trigger(child, "htmx:sseMessage", event);
218
+			}
219
+
220
+			// Register the new listener
221
+			api.getInternalData(elt).sseEventListener = listener;
222
+			source.addEventListener(sseEventName.slice(4), listener);
223
+		});
224
+	}
225
+
226
+	/**
227
+	 * maybeCloseSSESource confirms that the parent element still exists.
228
+	 * If not, then any associated SSE source is closed and the function returns true.
229
+	 * 
230
+	 * @param {HTMLElement} elt 
231
+	 * @returns boolean
232
+	 */
233
+	function maybeCloseSSESource(elt) {
234
+		if (!api.bodyContains(elt)) {
235
+			var source = api.getInternalData(elt).sseEventSource;
236
+			if (source != undefined) {
237
+				source.close();
238
+				// source = null
239
+				return true;
240
+			}
241
+		}
242
+		return false;
243
+	}
244
+
245
+	/**
246
+	 * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
247
+	 * 
248
+	 * @param {HTMLElement} elt 
249
+	 * @param {string} attributeName 
250
+	 */
251
+	function queryAttributeOnThisOrChildren(elt, attributeName) {
252
+
253
+		var result = [];
254
+
255
+		// If the parent element also contains the requested attribute, then add it to the results too.
256
+		if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) {
257
+			result.push(elt);
258
+		}
259
+
260
+		// Search all child nodes that match the requested attribute
261
+		elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) {
262
+			result.push(node);
263
+		});
264
+
265
+		return result;
266
+	}
267
+
268
+	/**
269
+	 * @param {HTMLElement} elt
270
+	 * @param {string} content 
271
+	 */
272
+	function swap(elt, content) {
273
+
274
+		api.withExtensions(elt, function(extension) {
275
+			content = extension.transformResponse(content, null, elt);
276
+		});
277
+
278
+		var swapSpec = api.getSwapSpecification(elt);
279
+		var target = api.getTarget(elt);
280
+		var settleInfo = api.makeSettleInfo(elt);
281
+
282
+		api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
283
+
284
+		settleInfo.elts.forEach(function (elt) {
285
+			if (elt.classList) {
286
+				elt.classList.add(htmx.config.settlingClass);
287
+			}
288
+			api.triggerEvent(elt, 'htmx:beforeSettle');
289
+		});
290
+
291
+		// Handle settle tasks (with delay if requested)
292
+		if (swapSpec.settleDelay > 0) {
293
+			setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
294
+		} else {
295
+			doSettle(settleInfo)();
296
+		}
297
+	}
298
+
299
+	/**
300
+	 * doSettle mirrors much of the functionality in htmx that 
301
+	 * settles elements after their content has been swapped.
302
+	 * TODO: this should be published by htmx, and not duplicated here
303
+	 * @param {import("../htmx").HtmxSettleInfo} settleInfo 
304
+	 * @returns () => void
305
+	 */
306
+	function doSettle(settleInfo) {
307
+
308
+		return function() {
309
+			settleInfo.tasks.forEach(function (task) {
310
+				task.call();
311
+			});
312
+
313
+			settleInfo.elts.forEach(function (elt) {
314
+				if (elt.classList) {
315
+					elt.classList.remove(htmx.config.settlingClass);
316
+				}
317
+				api.triggerEvent(elt, 'htmx:afterSettle');
318
+			});
319
+		}
320
+	}
321
+
322
+})();
0 323
\ No newline at end of file