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