htmx.org 2.0.0-beta2 → 2.0.0-beta4
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 +1 -3
- 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 +60 -15
- package/dist/htmx.cjs.js +60 -15
- package/dist/htmx.esm.js +61 -16
- package/dist/htmx.js +60 -15
- package/dist/htmx.min.js +1 -1
- package/dist/htmx.min.js.gz +0 -0
- package/package.json +1 -1
- package/CHANGELOG.md +0 -481
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
|
@@ -165,6 +165,12 @@ var htmx = (function() {
|
|
|
165
165
|
* @default ''
|
|
166
166
|
*/
|
|
167
167
|
inlineScriptNonce: '',
|
|
168
|
+
/**
|
|
169
|
+
* If set, the nonce will be added to inline styles.
|
|
170
|
+
* @type string
|
|
171
|
+
* @default ''
|
|
172
|
+
*/
|
|
173
|
+
inlineStyleNonce: '',
|
|
168
174
|
/**
|
|
169
175
|
* The attributes to settle during the settling phase.
|
|
170
176
|
* @type string[]
|
|
@@ -272,7 +278,7 @@ var htmx = (function() {
|
|
|
272
278
|
parseInterval: null,
|
|
273
279
|
/** @type {typeof internalEval} */
|
|
274
280
|
_: null,
|
|
275
|
-
version: '2.
|
|
281
|
+
version: '2.0.0-beta4'
|
|
276
282
|
}
|
|
277
283
|
// Tsc madness part 2
|
|
278
284
|
htmx.onLoad = onLoadHelper
|
|
@@ -2252,6 +2258,13 @@ var htmx = (function() {
|
|
|
2252
2258
|
getRawAttribute(elt, 'href').indexOf('#') !== 0
|
|
2253
2259
|
}
|
|
2254
2260
|
|
|
2261
|
+
/**
|
|
2262
|
+
* @param {Element} elt
|
|
2263
|
+
*/
|
|
2264
|
+
function eltIsDisabled(elt) {
|
|
2265
|
+
return closest(elt, htmx.config.disableSelector)
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2255
2268
|
/**
|
|
2256
2269
|
* @param {Element} elt
|
|
2257
2270
|
* @param {HtmxNodeInternalData} nodeData
|
|
@@ -2274,7 +2287,7 @@ var htmx = (function() {
|
|
|
2274
2287
|
triggerSpecs.forEach(function(triggerSpec) {
|
|
2275
2288
|
addEventListener(elt, function(node, evt) {
|
|
2276
2289
|
const elt = asElement(node)
|
|
2277
|
-
if (
|
|
2290
|
+
if (eltIsDisabled(elt)) {
|
|
2278
2291
|
cleanUpElement(elt)
|
|
2279
2292
|
return
|
|
2280
2293
|
}
|
|
@@ -2603,19 +2616,28 @@ var htmx = (function() {
|
|
|
2603
2616
|
* @param {Node} elt
|
|
2604
2617
|
* @returns {Element[]}
|
|
2605
2618
|
*/
|
|
2606
|
-
|
|
2619
|
+
const HX_ON_QUERY = new XPathEvaluator()
|
|
2620
|
+
.createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
|
|
2621
|
+
' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]')
|
|
2622
|
+
|
|
2623
|
+
function processHXOnRoot(elt, elements) {
|
|
2624
|
+
if (shouldProcessHxOn(elt)) {
|
|
2625
|
+
elements.push(asElement(elt))
|
|
2626
|
+
}
|
|
2627
|
+
const iter = HX_ON_QUERY.evaluate(elt)
|
|
2607
2628
|
let node = null
|
|
2629
|
+
while (node = iter.iterateNext()) elements.push(asElement(node))
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
function findHxOnWildcardElements(elt) {
|
|
2608
2633
|
/** @type {Element[]} */
|
|
2609
2634
|
const elements = []
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
elements.push(asElement(elt))
|
|
2635
|
+
if (elt instanceof DocumentFragment) {
|
|
2636
|
+
for (const child of elt.childNodes) {
|
|
2637
|
+
processHXOnRoot(child, elements)
|
|
2614
2638
|
}
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', elt)
|
|
2618
|
-
while (node = iter.iterateNext()) elements.push(asElement(node))
|
|
2639
|
+
} else {
|
|
2640
|
+
processHXOnRoot(elt, elements)
|
|
2619
2641
|
}
|
|
2620
2642
|
return elements
|
|
2621
2643
|
}
|
|
@@ -2627,8 +2649,21 @@ var htmx = (function() {
|
|
|
2627
2649
|
function findElementsToProcess(elt) {
|
|
2628
2650
|
if (elt.querySelectorAll) {
|
|
2629
2651
|
const boostedSelector = ', [hx-boost] a, [data-hx-boost] a, a[hx-boost], a[data-hx-boost]'
|
|
2652
|
+
|
|
2653
|
+
const extensionSelectors = []
|
|
2654
|
+
for (const e in extensions) {
|
|
2655
|
+
const extension = extensions[e]
|
|
2656
|
+
if (extension.getSelectors) {
|
|
2657
|
+
var selectors = extension.getSelectors()
|
|
2658
|
+
if (selectors) {
|
|
2659
|
+
extensionSelectors.push(selectors)
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2630
2664
|
const results = elt.querySelectorAll(VERB_SELECTOR + boostedSelector + ", form, [type='submit']," +
|
|
2631
|
-
' [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]')
|
|
2665
|
+
' [hx-ext], [data-hx-ext], [hx-trigger], [data-hx-trigger]' + extensionSelectors.flat().map(s => ', ' + s).join(''))
|
|
2666
|
+
|
|
2632
2667
|
return results
|
|
2633
2668
|
} else {
|
|
2634
2669
|
return []
|
|
@@ -2687,7 +2722,7 @@ var htmx = (function() {
|
|
|
2687
2722
|
}
|
|
2688
2723
|
|
|
2689
2724
|
/**
|
|
2690
|
-
* @param {
|
|
2725
|
+
* @param {Element} elt
|
|
2691
2726
|
* @param {string} eventName
|
|
2692
2727
|
* @param {string} code
|
|
2693
2728
|
*/
|
|
@@ -2700,6 +2735,9 @@ var htmx = (function() {
|
|
|
2700
2735
|
/** @type EventListener */
|
|
2701
2736
|
const listener = function(e) {
|
|
2702
2737
|
maybeEval(elt, function() {
|
|
2738
|
+
if (eltIsDisabled(elt)) {
|
|
2739
|
+
return
|
|
2740
|
+
}
|
|
2703
2741
|
if (!func) {
|
|
2704
2742
|
func = new Function('event', code)
|
|
2705
2743
|
}
|
|
@@ -3309,6 +3347,9 @@ var htmx = (function() {
|
|
|
3309
3347
|
}
|
|
3310
3348
|
})
|
|
3311
3349
|
new FormData(elt).forEach(function(value, name) {
|
|
3350
|
+
if (value instanceof File && value.name === '') {
|
|
3351
|
+
return // ignore no-name files
|
|
3352
|
+
}
|
|
3312
3353
|
addValueToFormData(name, value, formData)
|
|
3313
3354
|
})
|
|
3314
3355
|
}
|
|
@@ -3592,7 +3633,9 @@ var htmx = (function() {
|
|
|
3592
3633
|
return encodedParameters
|
|
3593
3634
|
} else {
|
|
3594
3635
|
if (usesFormData(elt)) {
|
|
3595
|
-
|
|
3636
|
+
// Force conversion to an actual FormData object in case filteredParameters is a formDataProxy
|
|
3637
|
+
// See https://github.com/bigskysoftware/htmx/issues/2317
|
|
3638
|
+
return overrideFormData(new FormData(), formDataFromObject(filteredParameters))
|
|
3596
3639
|
} else {
|
|
3597
3640
|
return urlEncode(filteredParameters)
|
|
3598
3641
|
}
|
|
@@ -4744,6 +4787,7 @@ var htmx = (function() {
|
|
|
4744
4787
|
function extensionBase() {
|
|
4745
4788
|
return {
|
|
4746
4789
|
init: function(api) { return null },
|
|
4790
|
+
getSelectors: function() { return null },
|
|
4747
4791
|
onEvent: function(name, evt) { return true },
|
|
4748
4792
|
transformResponse: function(text, xhr, elt) { return text },
|
|
4749
4793
|
isInlineSwap: function(swapStyle) { return false },
|
|
@@ -4842,8 +4886,9 @@ var htmx = (function() {
|
|
|
4842
4886
|
|
|
4843
4887
|
function insertIndicatorStyles() {
|
|
4844
4888
|
if (htmx.config.includeIndicatorStyles !== false) {
|
|
4889
|
+
const nonceAttribute = htmx.config.inlineStyleNonce ? ` nonce="${htmx.config.inlineStyleNonce}"` : ''
|
|
4845
4890
|
getDocument().head.insertAdjacentHTML('beforeend',
|
|
4846
|
-
'<style>\
|
|
4891
|
+
'<style' + nonceAttribute + '>\
|
|
4847
4892
|
.' + htmx.config.indicatorClass + '{opacity:0}\
|
|
4848
4893
|
.' + htmx.config.requestClass + ' .' + htmx.config.indicatorClass + '{opacity:1; transition: opacity 200ms ease-in;}\
|
|
4849
4894
|
.' + htmx.config.requestClass + '.' + htmx.config.indicatorClass + '{opacity:1; transition: opacity 200ms ease-in;}\
|