htmx.org 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ if (htmx.version && !htmx.version.startsWith("1.")) {
2
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
3
+ ". It is recommended that you move to the version of this extension found on https://extensions.htmx.org")
4
+ }
5
+ htmx.defineExtension('restored', {
6
+ onEvent : function(name, evt) {
7
+ if (name === 'htmx:restored'){
8
+ var restoredElts = evt.detail.document.querySelectorAll(
9
+ "[hx-trigger='restored'],[data-hx-trigger='restored']"
10
+ );
11
+ // need a better way to do this, would prefer to just trigger from evt.detail.elt
12
+ var foundElt = Array.from(restoredElts).find(
13
+ (x) => (x.outerHTML === evt.detail.elt.outerHTML)
14
+ );
15
+ var restoredEvent = evt.detail.triggerEvent(foundElt, 'restored');
16
+ }
17
+ return;
18
+ }
19
+ })
@@ -0,0 +1,374 @@
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
+ if (htmx.version && !htmx.version.startsWith("1.")) {
11
+ console.warn("WARNING: You are using an htmx 1 extension with htmx " + htmx.version +
12
+ ". It is recommended that you move to the version of this extension found on https://extensions.htmx.org")
13
+ }
14
+
15
+ /** @type {import("../htmx").HtmxInternalApi} */
16
+ var api;
17
+
18
+ htmx.defineExtension("sse", {
19
+
20
+ /**
21
+ * Init saves the provided reference to the internal HTMX API.
22
+ *
23
+ * @param {import("../htmx").HtmxInternalApi} api
24
+ * @returns void
25
+ */
26
+ init: function(apiRef) {
27
+ // store a reference to the internal API.
28
+ api = apiRef;
29
+
30
+ // set a function in the public API for creating new EventSource objects
31
+ if (htmx.createEventSource == undefined) {
32
+ htmx.createEventSource = createEventSource;
33
+ }
34
+ },
35
+
36
+ /**
37
+ * onEvent handles all events passed to this extension.
38
+ *
39
+ * @param {string} name
40
+ * @param {Event} evt
41
+ * @returns void
42
+ */
43
+ onEvent: function(name, evt) {
44
+
45
+ var parent = evt.target || evt.detail.elt;
46
+ switch (name) {
47
+
48
+ case "htmx:beforeCleanupElement":
49
+ var internalData = api.getInternalData(parent)
50
+ // Try to remove remove an EventSource when elements are removed
51
+ if (internalData.sseEventSource) {
52
+ internalData.sseEventSource.close();
53
+ }
54
+
55
+ return;
56
+
57
+ // Try to create EventSources when elements are processed
58
+ case "htmx:afterProcessNode":
59
+ ensureEventSourceOnElement(parent);
60
+ }
61
+ }
62
+ });
63
+
64
+ ///////////////////////////////////////////////
65
+ // HELPER FUNCTIONS
66
+ ///////////////////////////////////////////////
67
+
68
+
69
+ /**
70
+ * createEventSource is the default method for creating new EventSource objects.
71
+ * it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
72
+ *
73
+ * @param {string} url
74
+ * @returns EventSource
75
+ */
76
+ function createEventSource(url) {
77
+ return new EventSource(url, { withCredentials: true });
78
+ }
79
+
80
+ function splitOnWhitespace(trigger) {
81
+ return trigger.trim().split(/\s+/);
82
+ }
83
+
84
+ function getLegacySSEURL(elt) {
85
+ var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
86
+ if (legacySSEValue) {
87
+ var values = splitOnWhitespace(legacySSEValue);
88
+ for (var i = 0; i < values.length; i++) {
89
+ var value = values[i].split(/:(.+)/);
90
+ if (value[0] === "connect") {
91
+ return value[1];
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ function getLegacySSESwaps(elt) {
98
+ var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
99
+ var returnArr = [];
100
+ if (legacySSEValue != null) {
101
+ var values = splitOnWhitespace(legacySSEValue);
102
+ for (var i = 0; i < values.length; i++) {
103
+ var value = values[i].split(/:(.+)/);
104
+ if (value[0] === "swap") {
105
+ returnArr.push(value[1]);
106
+ }
107
+ }
108
+ }
109
+ return returnArr;
110
+ }
111
+
112
+ /**
113
+ * registerSSE looks for attributes that can contain sse events, right
114
+ * now hx-trigger and sse-swap and adds listeners based on these attributes too
115
+ * the closest event source
116
+ *
117
+ * @param {HTMLElement} elt
118
+ */
119
+ function registerSSE(elt) {
120
+ // Add message handlers for every `sse-swap` attribute
121
+ queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function (child) {
122
+ // Find closest existing event source
123
+ var sourceElement = api.getClosestMatch(child, hasEventSource);
124
+ if (sourceElement == null) {
125
+ // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
126
+ return null; // no eventsource in parentage, orphaned element
127
+ }
128
+
129
+ // Set internalData and source
130
+ var internalData = api.getInternalData(sourceElement);
131
+ var source = internalData.sseEventSource;
132
+
133
+ var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
134
+ if (sseSwapAttr) {
135
+ var sseEventNames = sseSwapAttr.split(",");
136
+ } else {
137
+ var sseEventNames = getLegacySSESwaps(child);
138
+ }
139
+
140
+ for (var i = 0; i < sseEventNames.length; i++) {
141
+ var sseEventName = sseEventNames[i].trim();
142
+ var listener = function(event) {
143
+
144
+ // If the source is missing then close SSE
145
+ if (maybeCloseSSESource(sourceElement)) {
146
+ return;
147
+ }
148
+
149
+ // If the body no longer contains the element, remove the listener
150
+ if (!api.bodyContains(child)) {
151
+ source.removeEventListener(sseEventName, listener);
152
+ return;
153
+ }
154
+
155
+ // swap the response into the DOM and trigger a notification
156
+ if(!api.triggerEvent(elt, "htmx:sseBeforeMessage", event)) {
157
+ return;
158
+ }
159
+ swap(child, event.data);
160
+ api.triggerEvent(elt, "htmx:sseMessage", event);
161
+ };
162
+
163
+ // Register the new listener
164
+ api.getInternalData(child).sseEventListener = listener;
165
+ source.addEventListener(sseEventName, listener);
166
+ }
167
+ });
168
+
169
+ // Add message handlers for every `hx-trigger="sse:*"` attribute
170
+ queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
171
+ // Find closest existing event source
172
+ var sourceElement = api.getClosestMatch(child, hasEventSource);
173
+ if (sourceElement == null) {
174
+ // api.triggerErrorEvent(elt, "htmx:noSSESourceError")
175
+ return null; // no eventsource in parentage, orphaned element
176
+ }
177
+
178
+ // Set internalData and source
179
+ var internalData = api.getInternalData(sourceElement);
180
+ var source = internalData.sseEventSource;
181
+
182
+ var sseEventName = api.getAttributeValue(child, "hx-trigger");
183
+ if (sseEventName == null) {
184
+ return;
185
+ }
186
+
187
+ // Only process hx-triggers for events with the "sse:" prefix
188
+ if (sseEventName.slice(0, 4) != "sse:") {
189
+ return;
190
+ }
191
+
192
+ // remove the sse: prefix from here on out
193
+ sseEventName = sseEventName.substr(4);
194
+
195
+ var listener = function() {
196
+ if (maybeCloseSSESource(sourceElement)) {
197
+ return
198
+ }
199
+
200
+ if (!api.bodyContains(child)) {
201
+ source.removeEventListener(sseEventName, listener);
202
+ }
203
+ }
204
+ });
205
+ }
206
+
207
+ /**
208
+ * ensureEventSourceOnElement creates a new EventSource connection on the provided element.
209
+ * If a usable EventSource already exists, then it is returned. If not, then a new EventSource
210
+ * is created and stored in the element's internalData.
211
+ * @param {HTMLElement} elt
212
+ * @param {number} retryCount
213
+ * @returns {EventSource | null}
214
+ */
215
+ function ensureEventSourceOnElement(elt, retryCount) {
216
+
217
+ if (elt == null) {
218
+ return null;
219
+ }
220
+
221
+ // handle extension source creation attribute
222
+ queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
223
+ var sseURL = api.getAttributeValue(child, "sse-connect");
224
+ if (sseURL == null) {
225
+ return;
226
+ }
227
+
228
+ ensureEventSource(child, sseURL, retryCount);
229
+ });
230
+
231
+ // handle legacy sse, remove for HTMX2
232
+ queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
233
+ var sseURL = getLegacySSEURL(child);
234
+ if (sseURL == null) {
235
+ return;
236
+ }
237
+
238
+ ensureEventSource(child, sseURL, retryCount);
239
+ });
240
+
241
+ registerSSE(elt);
242
+ }
243
+
244
+ function ensureEventSource(elt, url, retryCount) {
245
+ var source = htmx.createEventSource(url);
246
+
247
+ source.onerror = function(err) {
248
+
249
+ // Log an error event
250
+ api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
251
+
252
+ // If parent no longer exists in the document, then clean up this EventSource
253
+ if (maybeCloseSSESource(elt)) {
254
+ return;
255
+ }
256
+
257
+ // Otherwise, try to reconnect the EventSource
258
+ if (source.readyState === EventSource.CLOSED) {
259
+ retryCount = retryCount || 0;
260
+ var timeout = Math.random() * (2 ^ retryCount) * 500;
261
+ window.setTimeout(function() {
262
+ ensureEventSourceOnElement(elt, Math.min(7, retryCount + 1));
263
+ }, timeout);
264
+ }
265
+ };
266
+
267
+ source.onopen = function(evt) {
268
+ api.triggerEvent(elt, "htmx:sseOpen", { source: source });
269
+ }
270
+
271
+ api.getInternalData(elt).sseEventSource = source;
272
+ }
273
+
274
+ /**
275
+ * maybeCloseSSESource confirms that the parent element still exists.
276
+ * If not, then any associated SSE source is closed and the function returns true.
277
+ *
278
+ * @param {HTMLElement} elt
279
+ * @returns boolean
280
+ */
281
+ function maybeCloseSSESource(elt) {
282
+ if (!api.bodyContains(elt)) {
283
+ var source = api.getInternalData(elt).sseEventSource;
284
+ if (source != undefined) {
285
+ source.close();
286
+ // source = null
287
+ return true;
288
+ }
289
+ }
290
+ return false;
291
+ }
292
+
293
+ /**
294
+ * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
295
+ *
296
+ * @param {HTMLElement} elt
297
+ * @param {string} attributeName
298
+ */
299
+ function queryAttributeOnThisOrChildren(elt, attributeName) {
300
+
301
+ var result = [];
302
+
303
+ // If the parent element also contains the requested attribute, then add it to the results too.
304
+ if (api.hasAttribute(elt, attributeName)) {
305
+ result.push(elt);
306
+ }
307
+
308
+ // Search all child nodes that match the requested attribute
309
+ elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
310
+ result.push(node);
311
+ });
312
+
313
+ return result;
314
+ }
315
+
316
+ /**
317
+ * @param {HTMLElement} elt
318
+ * @param {string} content
319
+ */
320
+ function swap(elt, content) {
321
+
322
+ api.withExtensions(elt, function(extension) {
323
+ content = extension.transformResponse(content, null, elt);
324
+ });
325
+
326
+ var swapSpec = api.getSwapSpecification(elt);
327
+ var target = api.getTarget(elt);
328
+ var settleInfo = api.makeSettleInfo(elt);
329
+
330
+ api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
331
+
332
+ settleInfo.elts.forEach(function(elt) {
333
+ if (elt.classList) {
334
+ elt.classList.add(htmx.config.settlingClass);
335
+ }
336
+ api.triggerEvent(elt, 'htmx:beforeSettle');
337
+ });
338
+
339
+ // Handle settle tasks (with delay if requested)
340
+ if (swapSpec.settleDelay > 0) {
341
+ setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
342
+ } else {
343
+ doSettle(settleInfo)();
344
+ }
345
+ }
346
+
347
+ /**
348
+ * doSettle mirrors much of the functionality in htmx that
349
+ * settles elements after their content has been swapped.
350
+ * TODO: this should be published by htmx, and not duplicated here
351
+ * @param {import("../htmx").HtmxSettleInfo} settleInfo
352
+ * @returns () => void
353
+ */
354
+ function doSettle(settleInfo) {
355
+
356
+ return function() {
357
+ settleInfo.tasks.forEach(function(task) {
358
+ task.call();
359
+ });
360
+
361
+ settleInfo.elts.forEach(function(elt) {
362
+ if (elt.classList) {
363
+ elt.classList.remove(htmx.config.settlingClass);
364
+ }
365
+ api.triggerEvent(elt, 'htmx:afterSettle');
366
+ });
367
+ }
368
+ }
369
+
370
+ function hasEventSource(node) {
371
+ return api.getInternalData(node).sseEventSource != null;
372
+ }
373
+
374
+ })();