... | ... |
@@ -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 |
+})(); |
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 |