htmx.org 2.0.0-beta1 → 2.0.0-beta2

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.
package/dist/ext/ws.js DELETED
@@ -1,476 +0,0 @@
1
- /*
2
- WebSockets Extension
3
- ============================
4
- This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
5
- */
6
-
7
- (function () {
8
-
9
- /** @type {import("../htmx").HtmxInternalApi} */
10
- var api;
11
-
12
- htmx.defineExtension("ws", {
13
-
14
- /**
15
- * init is called once, when this extension is first registered.
16
- * @param {import("../htmx").HtmxInternalApi} apiRef
17
- */
18
- init: function (apiRef) {
19
-
20
- // Store reference to internal API
21
- api = apiRef;
22
-
23
- // Default function for creating new EventSource objects
24
- if (!htmx.createWebSocket) {
25
- htmx.createWebSocket = createWebSocket;
26
- }
27
-
28
- // Default setting for reconnect delay
29
- if (!htmx.config.wsReconnectDelay) {
30
- htmx.config.wsReconnectDelay = "full-jitter";
31
- }
32
- },
33
-
34
- /**
35
- * onEvent handles all events passed to this extension.
36
- *
37
- * @param {string} name
38
- * @param {Event} evt
39
- */
40
- onEvent: function (name, evt) {
41
- var parent = evt.target || evt.detail.elt;
42
-
43
- switch (name) {
44
-
45
- // Try to close the socket when elements are removed
46
- case "htmx:beforeCleanupElement":
47
-
48
- var internalData = api.getInternalData(parent)
49
-
50
- if (internalData.webSocket) {
51
- internalData.webSocket.close();
52
- }
53
- return;
54
-
55
- // Try to create websockets when elements are processed
56
- case "htmx:beforeProcessNode":
57
- forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
58
- ensureWebSocket(child)
59
- });
60
- forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
61
- ensureWebSocketSend(child)
62
- });
63
- }
64
- }
65
- });
66
-
67
- function splitOnWhitespace(trigger) {
68
- return trigger.trim().split(/\s+/);
69
- }
70
-
71
- function getLegacyWebsocketURL(elt) {
72
- var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
73
- if (legacySSEValue) {
74
- var values = splitOnWhitespace(legacySSEValue);
75
- for (var i = 0; i < values.length; i++) {
76
- var value = values[i].split(/:(.+)/);
77
- if (value[0] === "connect") {
78
- return value[1];
79
- }
80
- }
81
- }
82
- }
83
-
84
- /**
85
- * ensureWebSocket creates a new WebSocket on the designated element, using
86
- * the element's "ws-connect" attribute.
87
- * @param {HTMLElement} socketElt
88
- * @returns
89
- */
90
- function ensureWebSocket(socketElt) {
91
-
92
- // If the element containing the WebSocket connection no longer exists, then
93
- // do not connect/reconnect the WebSocket.
94
- if (!api.bodyContains(socketElt)) {
95
- return;
96
- }
97
-
98
- // Get the source straight from the element's value
99
- var wssSource = api.getAttributeValue(socketElt, "ws-connect")
100
-
101
- if (wssSource == null || wssSource === "") {
102
- var legacySource = getLegacyWebsocketURL(socketElt);
103
- if (legacySource == null) {
104
- return;
105
- } else {
106
- wssSource = legacySource;
107
- }
108
- }
109
-
110
- // Guarantee that the wssSource value is a fully qualified URL
111
- if (wssSource.indexOf("/") === 0) {
112
- var base_part = location.hostname + (location.port ? ':' + location.port : '');
113
- if (location.protocol === 'https:') {
114
- wssSource = "wss://" + base_part + wssSource;
115
- } else if (location.protocol === 'http:') {
116
- wssSource = "ws://" + base_part + wssSource;
117
- }
118
- }
119
-
120
- var socketWrapper = createWebsocketWrapper(socketElt, function () {
121
- return htmx.createWebSocket(wssSource)
122
- });
123
-
124
- socketWrapper.addEventListener('message', function (event) {
125
- if (maybeCloseWebSocketSource(socketElt)) {
126
- return;
127
- }
128
-
129
- var response = event.data;
130
- if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
131
- message: response,
132
- socketWrapper: socketWrapper.publicInterface
133
- })) {
134
- return;
135
- }
136
-
137
- api.withExtensions(socketElt, function (extension) {
138
- response = extension.transformResponse(response, null, socketElt);
139
- });
140
-
141
- var settleInfo = api.makeSettleInfo(socketElt);
142
- var fragment = api.makeFragment(response);
143
-
144
- if (fragment.children.length) {
145
- var children = Array.from(fragment.children);
146
- for (var i = 0; i < children.length; i++) {
147
- api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
148
- }
149
- }
150
-
151
- api.settleImmediately(settleInfo.tasks);
152
- api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
153
- });
154
-
155
- // Put the WebSocket into the HTML Element's custom data.
156
- api.getInternalData(socketElt).webSocket = socketWrapper;
157
- }
158
-
159
- /**
160
- * @typedef {Object} WebSocketWrapper
161
- * @property {WebSocket} socket
162
- * @property {Array<{message: string, sendElt: Element}>} messageQueue
163
- * @property {number} retryCount
164
- * @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
165
- * @property {(message: string, sendElt: Element) => void} send
166
- * @property {(event: string, handler: Function) => void} addEventListener
167
- * @property {() => void} handleQueuedMessages
168
- * @property {() => void} init
169
- * @property {() => void} close
170
- */
171
- /**
172
- *
173
- * @param socketElt
174
- * @param socketFunc
175
- * @returns {WebSocketWrapper}
176
- */
177
- function createWebsocketWrapper(socketElt, socketFunc) {
178
- var wrapper = {
179
- socket: null,
180
- messageQueue: [],
181
- retryCount: 0,
182
-
183
- /** @type {Object<string, Function[]>} */
184
- events: {},
185
-
186
- addEventListener: function (event, handler) {
187
- if (this.socket) {
188
- this.socket.addEventListener(event, handler);
189
- }
190
-
191
- if (!this.events[event]) {
192
- this.events[event] = [];
193
- }
194
-
195
- this.events[event].push(handler);
196
- },
197
-
198
- sendImmediately: function (message, sendElt) {
199
- if (!this.socket) {
200
- api.triggerErrorEvent()
201
- }
202
- if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
203
- message: message,
204
- socketWrapper: this.publicInterface
205
- })) {
206
- this.socket.send(message);
207
- sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
208
- message: message,
209
- socketWrapper: this.publicInterface
210
- })
211
- }
212
- },
213
-
214
- send: function (message, sendElt) {
215
- if (this.socket.readyState !== this.socket.OPEN) {
216
- this.messageQueue.push({ message: message, sendElt: sendElt });
217
- } else {
218
- this.sendImmediately(message, sendElt);
219
- }
220
- },
221
-
222
- handleQueuedMessages: function () {
223
- while (this.messageQueue.length > 0) {
224
- var queuedItem = this.messageQueue[0]
225
- if (this.socket.readyState === this.socket.OPEN) {
226
- this.sendImmediately(queuedItem.message, queuedItem.sendElt);
227
- this.messageQueue.shift();
228
- } else {
229
- break;
230
- }
231
- }
232
- },
233
-
234
- init: function () {
235
- if (this.socket && this.socket.readyState === this.socket.OPEN) {
236
- // Close discarded socket
237
- this.socket.close()
238
- }
239
-
240
- // Create a new WebSocket and event handlers
241
- /** @type {WebSocket} */
242
- var socket = socketFunc();
243
-
244
- // The event.type detail is added for interface conformance with the
245
- // other two lifecycle events (open and close) so a single handler method
246
- // can handle them polymorphically, if required.
247
- api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
248
-
249
- this.socket = socket;
250
-
251
- socket.onopen = function (e) {
252
- wrapper.retryCount = 0;
253
- api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
254
- wrapper.handleQueuedMessages();
255
- }
256
-
257
- socket.onclose = function (e) {
258
- // If socket should not be connected, stop further attempts to establish connection
259
- // If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
260
- if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
261
- var delay = getWebSocketReconnectDelay(wrapper.retryCount);
262
- setTimeout(function () {
263
- wrapper.retryCount += 1;
264
- wrapper.init();
265
- }, delay);
266
- }
267
-
268
- // Notify client code that connection has been closed. Client code can inspect `event` field
269
- // to determine whether closure has been valid or abnormal
270
- api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
271
- };
272
-
273
- socket.onerror = function (e) {
274
- api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
275
- maybeCloseWebSocketSource(socketElt);
276
- };
277
-
278
- var events = this.events;
279
- Object.keys(events).forEach(function (k) {
280
- events[k].forEach(function (e) {
281
- socket.addEventListener(k, e);
282
- })
283
- });
284
- },
285
-
286
- close: function () {
287
- this.socket.close()
288
- }
289
- }
290
-
291
- wrapper.init();
292
-
293
- wrapper.publicInterface = {
294
- send: wrapper.send.bind(wrapper),
295
- sendImmediately: wrapper.sendImmediately.bind(wrapper),
296
- queue: wrapper.messageQueue
297
- };
298
-
299
- return wrapper;
300
- }
301
-
302
- /**
303
- * ensureWebSocketSend attaches trigger handles to elements with
304
- * "ws-send" attribute
305
- * @param {HTMLElement} elt
306
- */
307
- function ensureWebSocketSend(elt) {
308
- var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
309
- if (legacyAttribute && legacyAttribute !== 'send') {
310
- return;
311
- }
312
-
313
- var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
314
- processWebSocketSend(webSocketParent, elt);
315
- }
316
-
317
- /**
318
- * hasWebSocket function checks if a node has webSocket instance attached
319
- * @param {HTMLElement} node
320
- * @returns {boolean}
321
- */
322
- function hasWebSocket(node) {
323
- return api.getInternalData(node).webSocket != null;
324
- }
325
-
326
- /**
327
- * processWebSocketSend adds event listeners to the <form> element so that
328
- * messages can be sent to the WebSocket server when the form is submitted.
329
- * @param {HTMLElement} socketElt
330
- * @param {HTMLElement} sendElt
331
- */
332
- function processWebSocketSend(socketElt, sendElt) {
333
- var nodeData = api.getInternalData(sendElt);
334
- var triggerSpecs = api.getTriggerSpecs(sendElt);
335
- triggerSpecs.forEach(function (ts) {
336
- api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
337
- if (maybeCloseWebSocketSource(socketElt)) {
338
- return;
339
- }
340
-
341
- /** @type {WebSocketWrapper} */
342
- var socketWrapper = api.getInternalData(socketElt).webSocket;
343
- var headers = api.getHeaders(sendElt, api.getTarget(sendElt));
344
- var results = api.getInputValues(sendElt, 'post');
345
- var errors = results.errors;
346
- var rawParameters = results.values;
347
- var expressionVars = api.getExpressionVars(sendElt);
348
- var allParameters = api.mergeObjects(rawParameters, expressionVars);
349
- var filteredParameters = api.filterValues(allParameters, sendElt);
350
-
351
- var sendConfig = {
352
- parameters: filteredParameters,
353
- unfilteredParameters: allParameters,
354
- headers: headers,
355
- errors: errors,
356
-
357
- triggeringEvent: evt,
358
- messageBody: undefined,
359
- socketWrapper: socketWrapper.publicInterface
360
- };
361
-
362
- if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
363
- return;
364
- }
365
-
366
- if (errors && errors.length > 0) {
367
- api.triggerEvent(elt, 'htmx:validation:halted', errors);
368
- return;
369
- }
370
-
371
- var body = sendConfig.messageBody;
372
- if (body === undefined) {
373
- var toSend = Object.assign({}, sendConfig.parameters);
374
- if (sendConfig.headers)
375
- toSend['HEADERS'] = headers;
376
- body = JSON.stringify(toSend);
377
- }
378
-
379
- socketWrapper.send(body, elt);
380
-
381
- if (evt && api.shouldCancel(evt, elt)) {
382
- evt.preventDefault();
383
- }
384
- });
385
- });
386
- }
387
-
388
- /**
389
- * getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
390
- * @param {number} retryCount // The number of retries that have already taken place
391
- * @returns {number}
392
- */
393
- function getWebSocketReconnectDelay(retryCount) {
394
-
395
- /** @type {"full-jitter" | ((retryCount:number) => number)} */
396
- var delay = htmx.config.wsReconnectDelay;
397
- if (typeof delay === 'function') {
398
- return delay(retryCount);
399
- }
400
- if (delay === 'full-jitter') {
401
- var exp = Math.min(retryCount, 6);
402
- var maxDelay = 1000 * Math.pow(2, exp);
403
- return maxDelay * Math.random();
404
- }
405
-
406
- logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
407
- }
408
-
409
- /**
410
- * maybeCloseWebSocketSource checks to the if the element that created the WebSocket
411
- * still exists in the DOM. If NOT, then the WebSocket is closed and this function
412
- * returns TRUE. If the element DOES EXIST, then no action is taken, and this function
413
- * returns FALSE.
414
- *
415
- * @param {*} elt
416
- * @returns
417
- */
418
- function maybeCloseWebSocketSource(elt) {
419
- if (!api.bodyContains(elt)) {
420
- api.getInternalData(elt).webSocket.close();
421
- return true;
422
- }
423
- return false;
424
- }
425
-
426
- /**
427
- * createWebSocket is the default method for creating new WebSocket objects.
428
- * it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
429
- *
430
- * @param {string} url
431
- * @returns WebSocket
432
- */
433
- function createWebSocket(url) {
434
- var sock = new WebSocket(url, []);
435
- sock.binaryType = htmx.config.wsBinaryType;
436
- return sock;
437
- }
438
-
439
- /**
440
- * queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
441
- *
442
- * @param {HTMLElement} elt
443
- * @param {string} attributeName
444
- */
445
- function queryAttributeOnThisOrChildren(elt, attributeName) {
446
-
447
- var result = []
448
-
449
- // If the parent element also contains the requested attribute, then add it to the results too.
450
- if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
451
- result.push(elt);
452
- }
453
-
454
- // Search all child nodes that match the requested attribute
455
- elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
456
- result.push(node)
457
- })
458
-
459
- return result
460
- }
461
-
462
- /**
463
- * @template T
464
- * @param {T[]} arr
465
- * @param {(T) => void} func
466
- */
467
- function forEach(arr, func) {
468
- if (arr) {
469
- for (var i = 0; i < arr.length; i++) {
470
- func(arr[i]);
471
- }
472
- }
473
- }
474
-
475
- })();
476
-