htmx.org 2.0.0-beta2 → 2.0.0-beta3

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 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
@@ -272,7 +272,7 @@ var htmx = (function() {
272
272
  parseInterval: null,
273
273
  /** @type {typeof internalEval} */
274
274
  _: null,
275
- version: '2.0a'
275
+ version: '2.0.0-beta3'
276
276
  }
277
277
  // Tsc madness part 2
278
278
  htmx.onLoad = onLoadHelper
@@ -2603,19 +2603,28 @@ var htmx = (function() {
2603
2603
  * @param {Node} elt
2604
2604
  * @returns {Element[]}
2605
2605
  */
2606
- function findHxOnWildcardElements(elt) {
2606
+ const HX_ON_QUERY = new XPathEvaluator()
2607
+ .createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
2608
+ ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]')
2609
+
2610
+ function processHXOnRoot(elt, elements) {
2611
+ if (shouldProcessHxOn(elt)) {
2612
+ elements.push(asElement(elt))
2613
+ }
2614
+ const iter = HX_ON_QUERY.evaluate(elt)
2607
2615
  let node = null
2616
+ while (node = iter.iterateNext()) elements.push(asElement(node))
2617
+ }
2618
+
2619
+ function findHxOnWildcardElements(elt) {
2608
2620
  /** @type {Element[]} */
2609
2621
  const elements = []
2610
-
2611
- if (!(elt instanceof ShadowRoot)) {
2612
- if (shouldProcessHxOn(elt)) {
2613
- elements.push(asElement(elt))
2622
+ if (elt instanceof DocumentFragment) {
2623
+ for (const child of elt.childNodes) {
2624
+ processHXOnRoot(child, elements)
2614
2625
  }
2615
-
2616
- const iter = document.evaluate('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
2617
- ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', elt)
2618
- while (node = iter.iterateNext()) elements.push(asElement(node))
2626
+ } else {
2627
+ processHXOnRoot(elt, elements)
2619
2628
  }
2620
2629
  return elements
2621
2630
  }
@@ -3592,7 +3601,9 @@ var htmx = (function() {
3592
3601
  return encodedParameters
3593
3602
  } else {
3594
3603
  if (usesFormData(elt)) {
3595
- return formDataFromObject(filteredParameters)
3604
+ // Force conversion to an actual FormData object in case filteredParameters is a formDataProxy
3605
+ // See https://github.com/bigskysoftware/htmx/issues/2317
3606
+ return overrideFormData(new FormData(), formDataFromObject(filteredParameters))
3596
3607
  } else {
3597
3608
  return urlEncode(filteredParameters)
3598
3609
  }
package/dist/htmx.cjs.js CHANGED
@@ -271,7 +271,7 @@ var htmx = (function() {
271
271
  parseInterval: null,
272
272
  /** @type {typeof internalEval} */
273
273
  _: null,
274
- version: '2.0a'
274
+ version: '2.0.0-beta3'
275
275
  }
276
276
  // Tsc madness part 2
277
277
  htmx.onLoad = onLoadHelper
@@ -2602,19 +2602,28 @@ var htmx = (function() {
2602
2602
  * @param {Node} elt
2603
2603
  * @returns {Element[]}
2604
2604
  */
2605
- function findHxOnWildcardElements(elt) {
2605
+ const HX_ON_QUERY = new XPathEvaluator()
2606
+ .createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
2607
+ ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]')
2608
+
2609
+ function processHXOnRoot(elt, elements) {
2610
+ if (shouldProcessHxOn(elt)) {
2611
+ elements.push(asElement(elt))
2612
+ }
2613
+ const iter = HX_ON_QUERY.evaluate(elt)
2606
2614
  let node = null
2615
+ while (node = iter.iterateNext()) elements.push(asElement(node))
2616
+ }
2617
+
2618
+ function findHxOnWildcardElements(elt) {
2607
2619
  /** @type {Element[]} */
2608
2620
  const elements = []
2609
-
2610
- if (!(elt instanceof ShadowRoot)) {
2611
- if (shouldProcessHxOn(elt)) {
2612
- elements.push(asElement(elt))
2621
+ if (elt instanceof DocumentFragment) {
2622
+ for (const child of elt.childNodes) {
2623
+ processHXOnRoot(child, elements)
2613
2624
  }
2614
-
2615
- const iter = document.evaluate('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
2616
- ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', elt)
2617
- while (node = iter.iterateNext()) elements.push(asElement(node))
2625
+ } else {
2626
+ processHXOnRoot(elt, elements)
2618
2627
  }
2619
2628
  return elements
2620
2629
  }
@@ -3591,7 +3600,9 @@ var htmx = (function() {
3591
3600
  return encodedParameters
3592
3601
  } else {
3593
3602
  if (usesFormData(elt)) {
3594
- return formDataFromObject(filteredParameters)
3603
+ // Force conversion to an actual FormData object in case filteredParameters is a formDataProxy
3604
+ // See https://github.com/bigskysoftware/htmx/issues/2317
3605
+ return overrideFormData(new FormData(), formDataFromObject(filteredParameters))
3595
3606
  } else {
3596
3607
  return urlEncode(filteredParameters)
3597
3608
  }
package/dist/htmx.esm.js CHANGED
@@ -271,7 +271,7 @@ var htmx = (function() {
271
271
  parseInterval: null,
272
272
  /** @type {typeof internalEval} */
273
273
  _: null,
274
- version: '2.0a'
274
+ version: '2.0.0-beta3'
275
275
  }
276
276
  // Tsc madness part 2
277
277
  htmx.onLoad = onLoadHelper
@@ -2602,19 +2602,28 @@ var htmx = (function() {
2602
2602
  * @param {Node} elt
2603
2603
  * @returns {Element[]}
2604
2604
  */
2605
- function findHxOnWildcardElements(elt) {
2605
+ const HX_ON_QUERY = new XPathEvaluator()
2606
+ .createExpression('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
2607
+ ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]')
2608
+
2609
+ function processHXOnRoot(elt, elements) {
2610
+ if (shouldProcessHxOn(elt)) {
2611
+ elements.push(asElement(elt))
2612
+ }
2613
+ const iter = HX_ON_QUERY.evaluate(elt)
2606
2614
  let node = null
2615
+ while (node = iter.iterateNext()) elements.push(asElement(node))
2616
+ }
2617
+
2618
+ function findHxOnWildcardElements(elt) {
2607
2619
  /** @type {Element[]} */
2608
2620
  const elements = []
2609
-
2610
- if (!(elt instanceof ShadowRoot)) {
2611
- if (shouldProcessHxOn(elt)) {
2612
- elements.push(asElement(elt))
2621
+ if (elt instanceof DocumentFragment) {
2622
+ for (const child of elt.childNodes) {
2623
+ processHXOnRoot(child, elements)
2613
2624
  }
2614
-
2615
- const iter = document.evaluate('.//*[@*[ starts-with(name(), "hx-on:") or starts-with(name(), "data-hx-on:") or' +
2616
- ' starts-with(name(), "hx-on-") or starts-with(name(), "data-hx-on-") ]]', elt)
2617
- while (node = iter.iterateNext()) elements.push(asElement(node))
2625
+ } else {
2626
+ processHXOnRoot(elt, elements)
2618
2627
  }
2619
2628
  return elements
2620
2629
  }
@@ -3591,7 +3600,9 @@ var htmx = (function() {
3591
3600
  return encodedParameters
3592
3601
  } else {
3593
3602
  if (usesFormData(elt)) {
3594
- return formDataFromObject(filteredParameters)
3603
+ // Force conversion to an actual FormData object in case filteredParameters is a formDataProxy
3604
+ // See https://github.com/bigskysoftware/htmx/issues/2317
3605
+ return overrideFormData(new FormData(), formDataFromObject(filteredParameters))
3595
3606
  } else {
3596
3607
  return urlEncode(filteredParameters)
3597
3608
  }
@@ -5082,4 +5093,4 @@ var htmx = (function() {
5082
5093
  * @property {(swapStyle: HtmxSwapStyle, target: Element, fragment: Node, settleInfo: HtmxSettleInfo) => boolean} handleSwap
5083
5094
  * @property {(xhr: XMLHttpRequest, parameters: FormData, elt: Element) => *|string|null} encodeParameters
5084
5095
  */
5085
- export { htmx }
5096
+ export default htmx