htmx.org 1.8.4 → 1.8.6
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/CHANGELOG.md +19 -0
- package/README.md +7 -4
- package/dist/ext/class-tools.js +21 -13
- package/dist/ext/head-support.js +2 -1
- package/dist/ext/loading-states.js +1 -1
- package/dist/ext/preload.js +8 -1
- package/dist/ext/ws.js +242 -120
- package/dist/htmx.js +74 -22
- package/dist/htmx.min.js +1 -1
- package/dist/htmx.min.js.gz +0 -0
- package/editors/jetbrains/htmx.web-types.json +282 -0
- package/package.json +7 -6
- /package/{src → dist}/htmx.d.ts +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.8.6] - 2023-03-02
|
|
4
|
+
|
|
5
|
+
* ESM support!
|
|
6
|
+
* Sass has been vanquished from the htmx.org website, which should set us up for some good progress going forward
|
|
7
|
+
* Fixed a bug where the `changed` modifier on `keyup` did not work properly if an input was tabbed into
|
|
8
|
+
* Many other smaller bug fixes and doc fixes
|
|
9
|
+
|
|
10
|
+
## [1.8.5] - 2023-01-17
|
|
11
|
+
|
|
12
|
+
* Support a new optional cache-busting configuration option, `getCacheBusterParam`, to allow browsers to disambiguate
|
|
13
|
+
between `GET` requests from htmx and from the raw browser
|
|
14
|
+
* Support new `hx-history='false'` attribute, to prevent sensitive data from being stored in the history cache. (Thank you @croxton!)
|
|
15
|
+
* Extensive new event-oriented features are available in the [Web Socket](/extensions/web-sockets/) extension (Thank you @Renerick!)
|
|
16
|
+
* A bug fix for when a form contains multiple empty input values with the same name (Thank you @bluekeyes!)
|
|
17
|
+
* A bug fix around inputs that throw exceptions when calling `setSelectionRange()` (Thank you @gone!)
|
|
18
|
+
* A bug fix to pass through the proper event for the `htmx:configRequest` event
|
|
19
|
+
* A bug fix/improvement for the `preload` extension
|
|
20
|
+
* Many other small bug fixes
|
|
21
|
+
|
|
3
22
|
## [1.8.4] - 2022-11-05
|
|
4
23
|
|
|
5
24
|
* Fix the _exact same_ regression in `revealed` logic as in 1.8.2
|
package/README.md
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
[](https://bundlephobia.com/result?p=htmx.org)
|
|
8
8
|
[](https://bundlephobia.com/result?p=htmx.org)
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
## introduction
|
|
12
11
|
|
|
13
12
|
htmx allows you to access [AJAX](https://htmx.org/docs#ajax), [CSS Transitions](https://htmx.org/docs#css_transitions),
|
|
@@ -34,7 +33,7 @@ By removing these arbitrary constraints htmx completes HTML as a
|
|
|
34
33
|
## quick start
|
|
35
34
|
|
|
36
35
|
```html
|
|
37
|
-
<script src="https://unpkg.com/htmx.org@1.8.
|
|
36
|
+
<script src="https://unpkg.com/htmx.org@1.8.6"></script>
|
|
38
37
|
<!-- have a button POST a click via AJAX -->
|
|
39
38
|
<button hx-post="/clicked" hx-swap="outerHTML">
|
|
40
39
|
Click Me
|
|
@@ -67,20 +66,24 @@ Note there is an old broken package called `htmx`. This is `htmx.org`.
|
|
|
67
66
|
* please write code, including tests, in ES5 for [IE 11 compatibility](https://stackoverflow.com/questions/39902809/support-for-es6-in-internet-explorer-11)
|
|
68
67
|
* please include test cases in [`/test`](https://github.com/bigskysoftware/htmx/tree/dev/test) and docs in [`/www`](https://github.com/bigskysoftware/htmx/tree/dev/www)
|
|
69
68
|
* if you are adding a feature, consider doing it as an [extension](https://htmx.org/extensions) instead to
|
|
70
|
-
keep the core htmx code tidy
|
|
69
|
+
keep the core htmx code tidy
|
|
71
70
|
* development pull requests should be against the `dev` branch, docs fixes can be made directly against `master`
|
|
72
71
|
* No time? Then [become a sponsor](https://github.com/sponsors/bigskysoftware#sponsors)
|
|
73
72
|
|
|
74
73
|
### hacking guide
|
|
75
74
|
|
|
76
75
|
To develop htmx locally, you will need to install the development dependencies.
|
|
77
|
-
|
|
76
|
+
|
|
77
|
+
__Requires Python 2 and Node 15.__
|
|
78
|
+
|
|
79
|
+
Run:
|
|
78
80
|
|
|
79
81
|
```
|
|
80
82
|
npm install
|
|
81
83
|
```
|
|
82
84
|
|
|
83
85
|
Then, run a web server in the root.
|
|
86
|
+
|
|
84
87
|
This is easiest with Python:
|
|
85
88
|
|
|
86
89
|
```
|
package/dist/ext/class-tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(){
|
|
1
|
+
(function () {
|
|
2
2
|
|
|
3
3
|
function splitOnWhitespace(trigger) {
|
|
4
4
|
return trigger.split(/\s+/);
|
|
@@ -20,15 +20,29 @@
|
|
|
20
20
|
delay = 100;
|
|
21
21
|
}
|
|
22
22
|
return {
|
|
23
|
-
operation:operation,
|
|
24
|
-
cssClass:cssClass,
|
|
25
|
-
delay:delay
|
|
23
|
+
operation: operation,
|
|
24
|
+
cssClass: cssClass,
|
|
25
|
+
delay: delay
|
|
26
26
|
}
|
|
27
27
|
} else {
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function performOperation(elt, classOperation, classList, currentRunTime) {
|
|
33
|
+
setTimeout(function () {
|
|
34
|
+
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
|
|
35
|
+
}, currentRunTime)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toggleOperation(elt, classOperation, classList, currentRunTime) {
|
|
39
|
+
setTimeout(function () {
|
|
40
|
+
setInterval(function () {
|
|
41
|
+
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
|
|
42
|
+
}, classOperation.delay);
|
|
43
|
+
}, currentRunTime)
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
function processClassList(elt, classList) {
|
|
33
47
|
var runs = classList.split("&");
|
|
34
48
|
for (var i = 0; i < runs.length; i++) {
|
|
@@ -41,17 +55,11 @@
|
|
|
41
55
|
var classOperation = parseClassOperation(trimmedValue);
|
|
42
56
|
if (classOperation) {
|
|
43
57
|
if (classOperation.operation === "toggle") {
|
|
44
|
-
|
|
45
|
-
setInterval(function () {
|
|
46
|
-
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
|
|
47
|
-
}, classOperation.delay);
|
|
48
|
-
}, currentRunTime);
|
|
58
|
+
toggleOperation(elt, classOperation, classList, currentRunTime);
|
|
49
59
|
currentRunTime = currentRunTime + classOperation.delay;
|
|
50
60
|
} else {
|
|
51
61
|
currentRunTime = currentRunTime + classOperation.delay;
|
|
52
|
-
|
|
53
|
-
elt.classList[classOperation.operation].call(elt.classList, classOperation.cssClass);
|
|
54
|
-
}, currentRunTime);
|
|
62
|
+
performOperation(elt, classOperation, classList, currentRunTime);
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
}
|
|
@@ -81,4 +89,4 @@
|
|
|
81
89
|
}
|
|
82
90
|
}
|
|
83
91
|
});
|
|
84
|
-
})();
|
|
92
|
+
})();
|
package/dist/ext/head-support.js
CHANGED
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
|
|
31
31
|
htmlDoc.innerHTML = headTag;
|
|
32
32
|
var newHeadTag = htmlDoc.querySelector("head");
|
|
33
|
+
var currentHead = document.head;
|
|
34
|
+
|
|
33
35
|
if (newHeadTag == null) {
|
|
34
36
|
return;
|
|
35
37
|
} else {
|
|
@@ -46,7 +48,6 @@
|
|
|
46
48
|
var mergeStrategy = api.getAttributeValue(newHeadTag, "hx-head") || defaultMergeStrategy;
|
|
47
49
|
|
|
48
50
|
// get the current head
|
|
49
|
-
var currentHead = document.head;
|
|
50
51
|
for (const currentHeadElt of currentHead.children) {
|
|
51
52
|
|
|
52
53
|
// If the current head element is in the map
|
package/dist/ext/preload.js
CHANGED
|
@@ -26,7 +26,9 @@ htmx.defineExtension("preload", {
|
|
|
26
26
|
// Called after a successful AJAX request, to mark the
|
|
27
27
|
// content as loaded (and prevent additional AJAX calls.)
|
|
28
28
|
var done = function(html) {
|
|
29
|
-
node.
|
|
29
|
+
if (!node.preloadAlways) {
|
|
30
|
+
node.preloadState = "DONE"
|
|
31
|
+
}
|
|
30
32
|
|
|
31
33
|
if (attr(node, "preload-images") == "true") {
|
|
32
34
|
document.createElement("div").innerHTML = html // create and populate a node to load linked resources, too.
|
|
@@ -81,6 +83,10 @@ htmx.defineExtension("preload", {
|
|
|
81
83
|
|
|
82
84
|
// Get event name from config.
|
|
83
85
|
var on = attr(node, "preload") || "mousedown"
|
|
86
|
+
const always = on.indexOf("always") !== -1
|
|
87
|
+
if (always) {
|
|
88
|
+
on = on.replace('always', '').trim()
|
|
89
|
+
}
|
|
84
90
|
|
|
85
91
|
// FALL THROUGH to here means we need to add an EventListener
|
|
86
92
|
|
|
@@ -121,6 +127,7 @@ htmx.defineExtension("preload", {
|
|
|
121
127
|
|
|
122
128
|
// Mark the node as ready to run.
|
|
123
129
|
node.preloadState = "PAUSE";
|
|
130
|
+
node.preloadAlways = always;
|
|
124
131
|
htmx.trigger(node, "preload:init") // This event can be used to load content immediately.
|
|
125
132
|
}
|
|
126
133
|
|
package/dist/ext/ws.js
CHANGED
|
@@ -4,7 +4,7 @@ WebSockets Extension
|
|
|
4
4
|
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
(function(){
|
|
7
|
+
(function () {
|
|
8
8
|
|
|
9
9
|
/** @type {import("../htmx").HtmxInternalApi} */
|
|
10
10
|
var api;
|
|
@@ -15,18 +15,18 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
|
|
15
15
|
* init is called once, when this extension is first registered.
|
|
16
16
|
* @param {import("../htmx").HtmxInternalApi} apiRef
|
|
17
17
|
*/
|
|
18
|
-
init: function(apiRef) {
|
|
18
|
+
init: function (apiRef) {
|
|
19
19
|
|
|
20
20
|
// Store reference to internal API
|
|
21
21
|
api = apiRef;
|
|
22
22
|
|
|
23
23
|
// Default function for creating new EventSource objects
|
|
24
|
-
if (htmx.createWebSocket
|
|
24
|
+
if (!htmx.createWebSocket) {
|
|
25
25
|
htmx.createWebSocket = createWebSocket;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// Default setting for reconnect delay
|
|
29
|
-
if (htmx.config.wsReconnectDelay
|
|
29
|
+
if (!htmx.config.wsReconnectDelay) {
|
|
30
30
|
htmx.config.wsReconnectDelay = "full-jitter";
|
|
31
31
|
}
|
|
32
32
|
},
|
|
@@ -37,30 +37,30 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
|
|
37
37
|
* @param {string} name
|
|
38
38
|
* @param {Event} evt
|
|
39
39
|
*/
|
|
40
|
-
onEvent: function(name, evt) {
|
|
40
|
+
onEvent: function (name, evt) {
|
|
41
41
|
|
|
42
42
|
switch (name) {
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// Try to close the socket when elements are removed
|
|
45
|
+
case "htmx:beforeCleanupElement":
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
var internalData = api.getInternalData(evt.target)
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
if (internalData.webSocket) {
|
|
50
|
+
internalData.webSocket.close();
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
// Try to create websockets when elements are processed
|
|
55
|
+
case "htmx:afterProcessNode":
|
|
56
|
+
var parent = evt.target;
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
|
|
59
|
+
ensureWebSocket(child)
|
|
60
|
+
});
|
|
61
|
+
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
|
|
62
|
+
ensureWebSocketSend(child)
|
|
63
|
+
});
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
});
|
|
@@ -85,23 +85,22 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
|
|
85
85
|
/**
|
|
86
86
|
* ensureWebSocket creates a new WebSocket on the designated element, using
|
|
87
87
|
* the element's "ws-connect" attribute.
|
|
88
|
-
* @param {HTMLElement}
|
|
89
|
-
* @param {number=} retryCount
|
|
88
|
+
* @param {HTMLElement} socketElt
|
|
90
89
|
* @returns
|
|
91
90
|
*/
|
|
92
|
-
function ensureWebSocket(
|
|
91
|
+
function ensureWebSocket(socketElt) {
|
|
93
92
|
|
|
94
93
|
// If the element containing the WebSocket connection no longer exists, then
|
|
95
94
|
// do not connect/reconnect the WebSocket.
|
|
96
|
-
if (!api.bodyContains(
|
|
95
|
+
if (!api.bodyContains(socketElt)) {
|
|
97
96
|
return;
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
// Get the source straight from the element's value
|
|
101
|
-
var wssSource = api.getAttributeValue(
|
|
100
|
+
var wssSource = api.getAttributeValue(socketElt, "ws-connect")
|
|
102
101
|
|
|
103
102
|
if (wssSource == null || wssSource === "") {
|
|
104
|
-
var legacySource = getLegacyWebsocketURL(
|
|
103
|
+
var legacySource = getLegacyWebsocketURL(socketElt);
|
|
105
104
|
if (legacySource == null) {
|
|
106
105
|
return;
|
|
107
106
|
} else {
|
|
@@ -109,58 +108,38 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
|
|
109
108
|
}
|
|
110
109
|
}
|
|
111
110
|
|
|
112
|
-
// Default value for retryCount
|
|
113
|
-
if (retryCount == undefined) {
|
|
114
|
-
retryCount = 0;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
111
|
// Guarantee that the wssSource value is a fully qualified URL
|
|
118
|
-
if (wssSource.indexOf("/")
|
|
119
|
-
var base_part = location.hostname + (location.port ? ':'+location.port: '');
|
|
120
|
-
if (location.protocol
|
|
112
|
+
if (wssSource.indexOf("/") === 0) {
|
|
113
|
+
var base_part = location.hostname + (location.port ? ':' + location.port : '');
|
|
114
|
+
if (location.protocol === 'https:') {
|
|
121
115
|
wssSource = "wss://" + base_part + wssSource;
|
|
122
|
-
} else if (location.protocol
|
|
116
|
+
} else if (location.protocol === 'http:') {
|
|
123
117
|
wssSource = "ws://" + base_part + wssSource;
|
|
124
118
|
}
|
|
125
119
|
}
|
|
126
120
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
var messageQueue = [];
|
|
132
|
-
|
|
133
|
-
socket.onopen = function (e) {
|
|
134
|
-
retryCount = 0;
|
|
135
|
-
handleQueuedMessages(messageQueue, socket);
|
|
136
|
-
}
|
|
121
|
+
var socketWrapper = createWebsocketWrapper(socketElt, function () {
|
|
122
|
+
return htmx.createWebSocket(wssSource)
|
|
123
|
+
});
|
|
137
124
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
var delay = getWebSocketReconnectDelay(retryCount);
|
|
142
|
-
setTimeout(function() {
|
|
143
|
-
ensureWebSocket(elt, retryCount+1);
|
|
144
|
-
}, delay);
|
|
125
|
+
socketWrapper.addEventListener('message', function (event) {
|
|
126
|
+
if (maybeCloseWebSocketSource(socketElt)) {
|
|
127
|
+
return;
|
|
145
128
|
}
|
|
146
|
-
};
|
|
147
129
|
|
|
148
|
-
|
|
149
|
-
api.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
socket.addEventListener('message', function (event) {
|
|
154
|
-
if (maybeCloseWebSocketSource(elt)) {
|
|
130
|
+
var response = event.data;
|
|
131
|
+
if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
|
|
132
|
+
message: response,
|
|
133
|
+
socketWrapper: socketWrapper.publicInterface
|
|
134
|
+
})) {
|
|
155
135
|
return;
|
|
156
136
|
}
|
|
157
137
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
response = extension.transformResponse(response, null, elt);
|
|
138
|
+
api.withExtensions(socketElt, function (extension) {
|
|
139
|
+
response = extension.transformResponse(response, null, socketElt);
|
|
161
140
|
});
|
|
162
141
|
|
|
163
|
-
var settleInfo = api.makeSettleInfo(
|
|
142
|
+
var settleInfo = api.makeSettleInfo(socketElt);
|
|
164
143
|
var fragment = api.makeFragment(response);
|
|
165
144
|
|
|
166
145
|
if (fragment.children.length) {
|
|
@@ -171,11 +150,154 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
|
|
171
150
|
}
|
|
172
151
|
|
|
173
152
|
api.settleImmediately(settleInfo.tasks);
|
|
153
|
+
api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
|
|
174
154
|
});
|
|
175
155
|
|
|
176
156
|
// Put the WebSocket into the HTML Element's custom data.
|
|
177
|
-
api.getInternalData(
|
|
178
|
-
|
|
157
|
+
api.getInternalData(socketElt).webSocket = socketWrapper;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @typedef {Object} WebSocketWrapper
|
|
162
|
+
* @property {WebSocket} socket
|
|
163
|
+
* @property {Array<{message: string, sendElt: Element}>} messageQueue
|
|
164
|
+
* @property {number} retryCount
|
|
165
|
+
* @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
|
|
166
|
+
* @property {(message: string, sendElt: Element) => void} send
|
|
167
|
+
* @property {(event: string, handler: Function) => void} addEventListener
|
|
168
|
+
* @property {() => void} handleQueuedMessages
|
|
169
|
+
* @property {() => void} init
|
|
170
|
+
* @property {() => void} close
|
|
171
|
+
*/
|
|
172
|
+
/**
|
|
173
|
+
*
|
|
174
|
+
* @param socketElt
|
|
175
|
+
* @param socketFunc
|
|
176
|
+
* @returns {WebSocketWrapper}
|
|
177
|
+
*/
|
|
178
|
+
function createWebsocketWrapper(socketElt, socketFunc) {
|
|
179
|
+
var wrapper = {
|
|
180
|
+
socket: null,
|
|
181
|
+
messageQueue: [],
|
|
182
|
+
retryCount: 0,
|
|
183
|
+
|
|
184
|
+
/** @type {Object<string, Function[]>} */
|
|
185
|
+
events: {},
|
|
186
|
+
|
|
187
|
+
addEventListener: function (event, handler) {
|
|
188
|
+
if (this.socket) {
|
|
189
|
+
this.socket.addEventListener(event, handler);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!this.events[event]) {
|
|
193
|
+
this.events[event] = [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.events[event].push(handler);
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
sendImmediately: function (message, sendElt) {
|
|
200
|
+
if (!this.socket) {
|
|
201
|
+
api.triggerErrorEvent()
|
|
202
|
+
}
|
|
203
|
+
if (sendElt && api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
|
|
204
|
+
message: message,
|
|
205
|
+
socketWrapper: this.publicInterface
|
|
206
|
+
})) {
|
|
207
|
+
this.socket.send(message);
|
|
208
|
+
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
|
|
209
|
+
message: message,
|
|
210
|
+
socketWrapper: this.publicInterface
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
send: function (message, sendElt) {
|
|
216
|
+
if (this.socket.readyState !== this.socket.OPEN) {
|
|
217
|
+
this.messageQueue.push({ message: message, sendElt: sendElt });
|
|
218
|
+
} else {
|
|
219
|
+
this.sendImmediately(message, sendElt);
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
handleQueuedMessages: function () {
|
|
224
|
+
while (this.messageQueue.length > 0) {
|
|
225
|
+
var queuedItem = this.messageQueue[0]
|
|
226
|
+
if (this.socket.readyState === this.socket.OPEN) {
|
|
227
|
+
this.sendImmediately(queuedItem.message, queuedItem.sendElt);
|
|
228
|
+
this.messageQueue.shift();
|
|
229
|
+
} else {
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
init: function () {
|
|
236
|
+
if (this.socket && this.socket.readyState === this.socket.OPEN) {
|
|
237
|
+
// Close discarded socket
|
|
238
|
+
this.socket.close()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Create a new WebSocket and event handlers
|
|
242
|
+
/** @type {WebSocket} */
|
|
243
|
+
var socket = socketFunc();
|
|
244
|
+
|
|
245
|
+
// The event.type detail is added for interface conformance with the
|
|
246
|
+
// other two lifecycle events (open and close) so a single handler method
|
|
247
|
+
// can handle them polymorphically, if required.
|
|
248
|
+
api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
|
|
249
|
+
|
|
250
|
+
this.socket = socket;
|
|
251
|
+
|
|
252
|
+
socket.onopen = function (e) {
|
|
253
|
+
wrapper.retryCount = 0;
|
|
254
|
+
api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
|
|
255
|
+
wrapper.handleQueuedMessages();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
socket.onclose = function (e) {
|
|
259
|
+
// If socket should not be connected, stop further attempts to establish connection
|
|
260
|
+
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
|
|
261
|
+
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
|
|
262
|
+
var delay = getWebSocketReconnectDelay(wrapper.retryCount);
|
|
263
|
+
setTimeout(function () {
|
|
264
|
+
wrapper.retryCount += 1;
|
|
265
|
+
wrapper.init();
|
|
266
|
+
}, delay);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Notify client code that connection has been closed. Client code can inspect `event` field
|
|
270
|
+
// to determine whether closure has been valid or abnormal
|
|
271
|
+
api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
socket.onerror = function (e) {
|
|
275
|
+
api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
|
|
276
|
+
maybeCloseWebSocketSource(socketElt);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
var events = this.events;
|
|
280
|
+
Object.keys(events).forEach(function (k) {
|
|
281
|
+
events[k].forEach(function (e) {
|
|
282
|
+
socket.addEventListener(k, e);
|
|
283
|
+
})
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
close: function () {
|
|
288
|
+
this.socket.close()
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
wrapper.init();
|
|
293
|
+
|
|
294
|
+
wrapper.publicInterface = {
|
|
295
|
+
send: wrapper.send.bind(wrapper),
|
|
296
|
+
sendImmediately: wrapper.sendImmediately.bind(wrapper),
|
|
297
|
+
queue: wrapper.messageQueue
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return wrapper;
|
|
179
301
|
}
|
|
180
302
|
|
|
181
303
|
/**
|
|
@@ -205,67 +327,65 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
|
|
205
327
|
/**
|
|
206
328
|
* processWebSocketSend adds event listeners to the <form> element so that
|
|
207
329
|
* messages can be sent to the WebSocket server when the form is submitted.
|
|
208
|
-
* @param {HTMLElement}
|
|
209
|
-
* @param {HTMLElement}
|
|
330
|
+
* @param {HTMLElement} socketElt
|
|
331
|
+
* @param {HTMLElement} sendElt
|
|
210
332
|
*/
|
|
211
|
-
function processWebSocketSend(
|
|
212
|
-
var nodeData = api.getInternalData(
|
|
213
|
-
|
|
214
|
-
triggerSpecs.forEach(function(ts) {
|
|
215
|
-
api.addTriggerHandler(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
333
|
+
function processWebSocketSend(socketElt, sendElt) {
|
|
334
|
+
var nodeData = api.getInternalData(sendElt);
|
|
335
|
+
var triggerSpecs = api.getTriggerSpecs(sendElt);
|
|
336
|
+
triggerSpecs.forEach(function (ts) {
|
|
337
|
+
api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
|
|
338
|
+
if (maybeCloseWebSocketSource(socketElt)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** @type {WebSocketWrapper} */
|
|
343
|
+
var socketWrapper = api.getInternalData(socketElt).webSocket;
|
|
344
|
+
var headers = api.getHeaders(sendElt, socketElt);
|
|
345
|
+
var results = api.getInputValues(sendElt, 'post');
|
|
220
346
|
var errors = results.errors;
|
|
221
347
|
var rawParameters = results.values;
|
|
222
|
-
var expressionVars = api.getExpressionVars(
|
|
348
|
+
var expressionVars = api.getExpressionVars(sendElt);
|
|
223
349
|
var allParameters = api.mergeObjects(rawParameters, expressionVars);
|
|
224
|
-
var filteredParameters = api.filterValues(allParameters,
|
|
225
|
-
|
|
350
|
+
var filteredParameters = api.filterValues(allParameters, sendElt);
|
|
351
|
+
|
|
352
|
+
var sendConfig = {
|
|
353
|
+
parameters: filteredParameters,
|
|
354
|
+
unfilteredParameters: allParameters,
|
|
355
|
+
headers: headers,
|
|
356
|
+
errors: errors,
|
|
357
|
+
|
|
358
|
+
triggeringEvent: evt,
|
|
359
|
+
messageBody: undefined,
|
|
360
|
+
socketWrapper: socketWrapper.publicInterface
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
226
367
|
if (errors && errors.length > 0) {
|
|
227
|
-
api.triggerEvent(
|
|
368
|
+
api.triggerEvent(elt, 'htmx:validation:halted', errors);
|
|
228
369
|
return;
|
|
229
370
|
}
|
|
230
|
-
|
|
231
|
-
|
|
371
|
+
|
|
372
|
+
var body = sendConfig.messageBody;
|
|
373
|
+
if (body === undefined) {
|
|
374
|
+
var toSend = Object.assign({}, sendConfig.parameters);
|
|
375
|
+
if (sendConfig.headers)
|
|
376
|
+
toSend['HEADERS'] = headers;
|
|
377
|
+
body = JSON.stringify(toSend);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
socketWrapper.send(body, elt);
|
|
381
|
+
|
|
382
|
+
if (api.shouldCancel(evt, elt)) {
|
|
232
383
|
evt.preventDefault();
|
|
233
384
|
}
|
|
234
385
|
});
|
|
235
386
|
});
|
|
236
387
|
}
|
|
237
388
|
|
|
238
|
-
/**
|
|
239
|
-
* webSocketSend provides a safe way to send messages through a WebSocket.
|
|
240
|
-
* It checks that the socket is in OPEN state and, otherwise, awaits for it.
|
|
241
|
-
* @param {WebSocket} socket
|
|
242
|
-
* @param {string} message
|
|
243
|
-
* @param {string[]} messageQueue
|
|
244
|
-
* @return {boolean}
|
|
245
|
-
*/
|
|
246
|
-
function webSocketSend(socket, message, messageQueue) {
|
|
247
|
-
if (socket.readyState != socket.OPEN) {
|
|
248
|
-
messageQueue.push(message);
|
|
249
|
-
} else {
|
|
250
|
-
socket.send(message);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* handleQueuedMessages sends messages awaiting in the message queue
|
|
256
|
-
*/
|
|
257
|
-
function handleQueuedMessages(messageQueue, socket) {
|
|
258
|
-
while (messageQueue.length > 0) {
|
|
259
|
-
var message = messageQueue[0]
|
|
260
|
-
if (socket.readyState == socket.OPEN) {
|
|
261
|
-
socket.send(message);
|
|
262
|
-
messageQueue.shift()
|
|
263
|
-
} else {
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
389
|
/**
|
|
270
390
|
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
|
|
271
391
|
* @param {number} retryCount // The number of retries that have already taken place
|
|
@@ -273,7 +393,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
|
|
273
393
|
*/
|
|
274
394
|
function getWebSocketReconnectDelay(retryCount) {
|
|
275
395
|
|
|
276
|
-
/** @type {"full-jitter" | (retryCount:number) => number} */
|
|
396
|
+
/** @type {"full-jitter" | ((retryCount:number) => number)} */
|
|
277
397
|
var delay = htmx.config.wsReconnectDelay;
|
|
278
398
|
if (typeof delay === 'function') {
|
|
279
399
|
return delay(retryCount);
|
|
@@ -296,7 +416,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
|
|
296
416
|
* @param {*} elt
|
|
297
417
|
* @returns
|
|
298
418
|
*/
|
|
299
|
-
|
|
419
|
+
function maybeCloseWebSocketSource(elt) {
|
|
300
420
|
if (!api.bodyContains(elt)) {
|
|
301
421
|
api.getInternalData(elt).webSocket.close();
|
|
302
422
|
return true;
|
|
@@ -311,8 +431,10 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
|
|
311
431
|
* @param {string} url
|
|
312
432
|
* @returns WebSocket
|
|
313
433
|
*/
|
|
314
|
-
|
|
315
|
-
|
|
434
|
+
function createWebSocket(url) {
|
|
435
|
+
var sock = new WebSocket(url, []);
|
|
436
|
+
sock.binaryType = htmx.config.wsBinaryType;
|
|
437
|
+
return sock;
|
|
316
438
|
}
|
|
317
439
|
|
|
318
440
|
/**
|
|
@@ -331,7 +453,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
|
|
331
453
|
}
|
|
332
454
|
|
|
333
455
|
// Search all child nodes that match the requested attribute
|
|
334
|
-
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function(node) {
|
|
456
|
+
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
|
|
335
457
|
result.push(node)
|
|
336
458
|
})
|
|
337
459
|
|