htmx.org 1.6.1 → 1.7.0
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 +26 -0
- package/README.md +16 -11
- package/dist/ext/alpine-morph.js +16 -0
- package/dist/ext/loading-states.js +165 -0
- package/dist/ext/restored.js +15 -0
- package/dist/ext/sse.js +318 -0
- package/dist/ext/ws.js +295 -0
- package/dist/htmx.js +486 -110
- package/dist/htmx.min.js +1 -1
- package/dist/htmx.min.js.gz +0 -0
- package/package.json +9 -7
- package/src/htmx.d.ts +339 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.0] - 2022-02-2
|
|
4
|
+
|
|
5
|
+
* The new [`hx-sync`](/attributes/hx-sync) attribute allows you to synchronize multiple element requests on a single
|
|
6
|
+
element using various strategies (e.g. replace)
|
|
7
|
+
* You can also now abort an element making a request by sending it the `htmx:abort` event
|
|
8
|
+
* [Server Sent Events](/extensions/server-sent-events) and [Web Sockets](/extensions/web-sockets) are now available as
|
|
9
|
+
extensions, in addition to the normal core support. In htmx 2.0, the current `hx-sse` and `hx-ws` attributes will be
|
|
10
|
+
moved entirely out to these new extensions. By moving these features to extensions we will be able to add functionality
|
|
11
|
+
to both of them without compromising the core file size of htmx. You are encouraged to move over to the new
|
|
12
|
+
extensions, but `hx-sse` and `hx-ws` will continue to work indefinitely in htmx 1.x.
|
|
13
|
+
* You can now mask out [attribute inheritance](/docs#inheritance) via the [`hx-disinherit`](/attributes/hx-disinherit) attribute.
|
|
14
|
+
* The `HX-Push` header can now have the `false` value, which will prevent a history snapshot from occuring.
|
|
15
|
+
* Many new extensions, with a big thanks to all the contributors!
|
|
16
|
+
* A new [`alpine-morph`](/extensions/alpine-morph) allows you to use Alpine's swapping engine, which preserves Alpine
|
|
17
|
+
* A [restored](/extensions/restored) extension was added that will trigger a `restore` event on all elements in the DOM
|
|
18
|
+
on history restoration.
|
|
19
|
+
* A [loading-states](/extensions/loading-states) extension was added that allows you to easily manage loading states
|
|
20
|
+
while a request is in flight, including disabling elements, and adding and removing CSS classes.
|
|
21
|
+
* The `this` symbol now resolves properly for the [`hx-include`](/attributes/hx-include) and [`hx-indicator`](/attributes/hx-indicator)
|
|
22
|
+
attributes
|
|
23
|
+
* When an object is included via the [`hx-vals`](/attributes/hx-vals) attribute, it will be converted to JSON (rather
|
|
24
|
+
than rendering as the string `[Object object]"`)
|
|
25
|
+
* You can now pass a swap style in to the `htmx.ajax()` function call.
|
|
26
|
+
* Poll events now contain a `target` attribute, allowing you to filter a poll on the element that is polling.
|
|
27
|
+
* Two new Out Of Band-related events were added: `htmx:oobBeforeSwap` & `htmx:oobAfterSwap`
|
|
28
|
+
|
|
3
29
|
## [1.6.1] - 2021-11-22
|
|
4
30
|
|
|
5
31
|
* A new `HX-Retarget` header allows you to change the default target of returned content
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ By removing these arbitrary constraints htmx completes HTML as a
|
|
|
35
35
|
|
|
36
36
|
```html
|
|
37
37
|
<!-- Load from unpkg -->
|
|
38
|
-
<script src="https://unpkg.com/htmx.org@1.
|
|
38
|
+
<script src="https://unpkg.com/htmx.org@1.7.0" ></script>
|
|
39
39
|
<!-- have a button POST a click via AJAX -->
|
|
40
40
|
<button hx-post="/clicked" hx-swap="outerHTML">
|
|
41
41
|
Click Me
|
|
@@ -74,30 +74,35 @@ keep the core htmx code tidy
|
|
|
74
74
|
|
|
75
75
|
### hacking guide
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
To develop htmx locally, you will need to install the development dependencies.
|
|
78
|
+
Use node 15 and run:
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
```
|
|
81
|
+
npm install
|
|
82
|
+
```
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
Then, run a web server in the root.
|
|
85
|
+
This is easiest with Python:
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
```
|
|
88
|
+
python3 -m http.server
|
|
89
|
+
```
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
You can then run the test suite by navigating to:
|
|
87
92
|
|
|
88
93
|
<http://0.0.0.0:8000/test/>
|
|
89
94
|
|
|
90
|
-
|
|
95
|
+
At this point you can modify `/src/htmx.js` to add features, and then add tests in the appropriate area under `/test`.
|
|
91
96
|
|
|
92
97
|
* `/test/index.html` - the root test page from which all other tests are included
|
|
93
|
-
* `/test/
|
|
98
|
+
* `/test/attributes` - attribute specific tests
|
|
94
99
|
* `/test/core` - core functionality tests
|
|
95
|
-
* `/test/core/regressions.js` -
|
|
100
|
+
* `/test/core/regressions.js` - regression tests
|
|
96
101
|
* `/test/ext` - extension tests
|
|
97
102
|
* `/test/manual` - manual tests that cannot be automated
|
|
98
103
|
|
|
99
104
|
htmx uses the [mocha](https://mochajs.org/) testing framework, the [chai](https://www.chaijs.com/) assertion framework
|
|
100
|
-
and [sinon](https://sinonjs.org/releases/
|
|
105
|
+
and [sinon](https://sinonjs.org/releases/v9/fake-xhr-and-server/) to mock out AJAX requests. They are all OK.
|
|
101
106
|
|
|
102
107
|
## haiku
|
|
103
108
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
htmx.defineExtension('alpine-morph', {
|
|
2
|
+
isInlineSwap: function (swapStyle) {
|
|
3
|
+
return swapStyle === 'morph';
|
|
4
|
+
},
|
|
5
|
+
handleSwap: function (swapStyle, target, fragment) {
|
|
6
|
+
if (swapStyle === 'morph') {
|
|
7
|
+
if (fragment.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
8
|
+
Alpine.morph(target, fragment.firstElementChild);
|
|
9
|
+
return [target];
|
|
10
|
+
} else {
|
|
11
|
+
Alpine.morph(target, fragment.outerHTML);
|
|
12
|
+
return [target];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
;(function () {
|
|
2
|
+
let loadingStatesUndoQueue = []
|
|
3
|
+
|
|
4
|
+
function loadingStateContainer(target) {
|
|
5
|
+
return htmx.closest(target, '[data-loading-states]') || document.body
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function mayProcessUndoCallback(target, callback) {
|
|
9
|
+
if (document.body.contains(target)) {
|
|
10
|
+
callback()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mayProcessLoadingStateByPath(elt, requestPath) {
|
|
15
|
+
const pathElt = htmx.closest(elt, '[data-loading-path]')
|
|
16
|
+
if (!pathElt) {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return pathElt.getAttribute('data-loading-path') === requestPath
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function queueLoadingState(sourceElt, targetElt, doCallback, undoCallback) {
|
|
24
|
+
const delayElt = htmx.closest(sourceElt, '[data-loading-delay]')
|
|
25
|
+
if (delayElt) {
|
|
26
|
+
const delayInMilliseconds =
|
|
27
|
+
delayElt.getAttribute('data-loading-delay') || 200
|
|
28
|
+
const timeout = setTimeout(() => {
|
|
29
|
+
doCallback()
|
|
30
|
+
|
|
31
|
+
loadingStatesUndoQueue.push(() => {
|
|
32
|
+
mayProcessUndoCallback(targetElt, () => undoCallback())
|
|
33
|
+
})
|
|
34
|
+
}, delayInMilliseconds)
|
|
35
|
+
|
|
36
|
+
loadingStatesUndoQueue.push(() => {
|
|
37
|
+
mayProcessUndoCallback(targetElt, () => clearTimeout(timeout))
|
|
38
|
+
})
|
|
39
|
+
} else {
|
|
40
|
+
doCallback()
|
|
41
|
+
loadingStatesUndoQueue.push(() => {
|
|
42
|
+
mayProcessUndoCallback(targetElt, () => undoCallback())
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getLoadingStateElts(loadingScope, type, path) {
|
|
48
|
+
return Array.from(htmx.findAll(loadingScope, `[${type}]`)).filter(
|
|
49
|
+
(elt) => mayProcessLoadingStateByPath(elt, path)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getLoadingTarget(elt) {
|
|
54
|
+
if (elt.getAttribute('data-loading-target')) {
|
|
55
|
+
return Array.from(
|
|
56
|
+
htmx.findAll(elt.getAttribute('data-loading-target'))
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
return [elt]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
htmx.defineExtension('loading-states', {
|
|
63
|
+
onEvent: function (name, evt) {
|
|
64
|
+
if (name === 'htmx:beforeRequest') {
|
|
65
|
+
const container = loadingStateContainer(evt.target)
|
|
66
|
+
|
|
67
|
+
const loadingStateTypes = [
|
|
68
|
+
'data-loading',
|
|
69
|
+
'data-loading-class',
|
|
70
|
+
'data-loading-class-remove',
|
|
71
|
+
'data-loading-disable',
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
let loadingStateEltsByType = {}
|
|
75
|
+
|
|
76
|
+
loadingStateTypes.forEach((type) => {
|
|
77
|
+
loadingStateEltsByType[type] = getLoadingStateElts(
|
|
78
|
+
container,
|
|
79
|
+
type,
|
|
80
|
+
evt.detail.pathInfo.path
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
loadingStateEltsByType['data-loading'].forEach((sourceElt) => {
|
|
85
|
+
getLoadingTarget(sourceElt).forEach((targetElt) => {
|
|
86
|
+
queueLoadingState(
|
|
87
|
+
sourceElt,
|
|
88
|
+
targetElt,
|
|
89
|
+
() =>
|
|
90
|
+
(targetElt.style.display =
|
|
91
|
+
sourceElt.getAttribute('data-loading') ||
|
|
92
|
+
'inline-block'),
|
|
93
|
+
() => (targetElt.style.display = 'none')
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
loadingStateEltsByType['data-loading-class'].forEach(
|
|
99
|
+
(sourceElt) => {
|
|
100
|
+
const classNames = sourceElt
|
|
101
|
+
.getAttribute('data-loading-class')
|
|
102
|
+
.split(' ')
|
|
103
|
+
|
|
104
|
+
getLoadingTarget(sourceElt).forEach((targetElt) => {
|
|
105
|
+
queueLoadingState(
|
|
106
|
+
sourceElt,
|
|
107
|
+
targetElt,
|
|
108
|
+
() =>
|
|
109
|
+
classNames.forEach((className) =>
|
|
110
|
+
targetElt.classList.add(className)
|
|
111
|
+
),
|
|
112
|
+
() =>
|
|
113
|
+
classNames.forEach((className) =>
|
|
114
|
+
targetElt.classList.remove(className)
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
loadingStateEltsByType['data-loading-class-remove'].forEach(
|
|
122
|
+
(sourceElt) => {
|
|
123
|
+
const classNames = sourceElt
|
|
124
|
+
.getAttribute('data-loading-class-remove')
|
|
125
|
+
.split(' ')
|
|
126
|
+
|
|
127
|
+
getLoadingTarget(sourceElt).forEach((targetElt) => {
|
|
128
|
+
queueLoadingState(
|
|
129
|
+
sourceElt,
|
|
130
|
+
targetElt,
|
|
131
|
+
() =>
|
|
132
|
+
classNames.forEach((className) =>
|
|
133
|
+
targetElt.classList.remove(className)
|
|
134
|
+
),
|
|
135
|
+
() =>
|
|
136
|
+
classNames.forEach((className) =>
|
|
137
|
+
targetElt.classList.add(className)
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
loadingStateEltsByType['data-loading-disable'].forEach(
|
|
145
|
+
(sourceElt) => {
|
|
146
|
+
getLoadingTarget(sourceElt).forEach((targetElt) => {
|
|
147
|
+
queueLoadingState(
|
|
148
|
+
sourceElt,
|
|
149
|
+
targetElt,
|
|
150
|
+
() => (targetElt.disabled = true),
|
|
151
|
+
() => (targetElt.disabled = false)
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (name === 'htmx:afterOnLoad') {
|
|
159
|
+
while (loadingStatesUndoQueue.length > 0) {
|
|
160
|
+
loadingStatesUndoQueue.shift()()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
})()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
htmx.defineExtension('restored', {
|
|
2
|
+
onEvent : function(name, evt) {
|
|
3
|
+
if (name === 'htmx:restored'){
|
|
4
|
+
var restoredElts = evt.detail.document.querySelectorAll(
|
|
5
|
+
"[hx-trigger='restored'],[data-hx-trigger='restored']"
|
|
6
|
+
);
|
|
7
|
+
// need a better way to do this, would prefer to just trigger from evt.detail.elt
|
|
8
|
+
var foundElt = Array.from(restoredElts).find(
|
|
9
|
+
(x) => (x.outerHTML === evt.detail.elt.outerHTML)
|
|
10
|
+
);
|
|
11
|
+
var restoredEvent = evt.detail.triggerEvent(foundElt, 'restored');
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
})
|
package/dist/ext/sse.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Server Sent Events Extension
|
|
3
|
+
============================
|
|
4
|
+
This extension adds support for Server Sent Events to htmx. See /www/extensions/sse.md for usage instructions.
|
|
5
|
+
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
(function(){
|
|
9
|
+
|
|
10
|
+
/** @type {import("../htmx").HtmxInternalApi} */
|
|
11
|
+
var api;
|
|
12
|
+
|
|
13
|
+
htmx.defineExtension("sse", {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Init saves the provided reference to the internal HTMX API.
|
|
17
|
+
*
|
|
18
|
+
* @param {import("../htmx").HtmxInternalApi} api
|
|
19
|
+
* @returns void
|
|
20
|
+
*/
|
|
21
|
+
init: function(apiRef) {
|
|
22
|
+
// store a reference to the internal API.
|
|
23
|
+
api = apiRef;
|
|
24
|
+
|
|
25
|
+
// set a function in the public API for creating new EventSource objects
|
|
26
|
+
if (htmx.createEventSource == undefined) {
|
|
27
|
+
htmx.createEventSource = createEventSource;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* onEvent handles all events passed to this extension.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} name
|
|
35
|
+
* @param {Event} evt
|
|
36
|
+
* @returns void
|
|
37
|
+
*/
|
|
38
|
+
onEvent: function(name, evt) {
|
|
39
|
+
|
|
40
|
+
switch (name) {
|
|
41
|
+
|
|
42
|
+
// Try to remove remove an EventSource when elements are removed
|
|
43
|
+
case "htmx:beforeCleanupElement":
|
|
44
|
+
var internalData = api.getInternalData(evt.target)
|
|
45
|
+
if (internalData.sseEventSource) {
|
|
46
|
+
internalData.sseEventSource.close();
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
|
|
50
|
+
// Try to create EventSources when elements are processed
|
|
51
|
+
case "htmx:afterProcessNode":
|
|
52
|
+
createEventSourceOnElement(evt.target);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
///////////////////////////////////////////////
|
|
58
|
+
// HELPER FUNCTIONS
|
|
59
|
+
///////////////////////////////////////////////
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* createEventSource is the default method for creating new EventSource objects.
|
|
64
|
+
* it is hoisted into htmx.config.createEventSource to be overridden by the user, if needed.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} url
|
|
67
|
+
* @returns EventSource
|
|
68
|
+
*/
|
|
69
|
+
function createEventSource(url) {
|
|
70
|
+
return new EventSource(url, {withCredentials:true});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function splitOnWhitespace(trigger) {
|
|
74
|
+
return trigger.trim().split(/\s+/);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getLegacySSEURL(elt) {
|
|
78
|
+
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
|
79
|
+
if (legacySSEValue) {
|
|
80
|
+
var values = splitOnWhitespace(legacySSEValue);
|
|
81
|
+
for (var i = 0; i < values.length; i++) {
|
|
82
|
+
var value = values[i].split(/:(.+)/);
|
|
83
|
+
if (value[0] === "connect") {
|
|
84
|
+
return value[1];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getLegacySSESwaps(elt) {
|
|
91
|
+
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
|
92
|
+
var returnArr = [];
|
|
93
|
+
if (legacySSEValue) {
|
|
94
|
+
var values = splitOnWhitespace(legacySSEValue);
|
|
95
|
+
for (var i = 0; i < values.length; i++) {
|
|
96
|
+
var value = values[i].split(/:(.+)/);
|
|
97
|
+
if (value[0] === "swap") {
|
|
98
|
+
returnArr.push(value[1]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return returnArr;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* createEventSourceOnElement creates a new EventSource connection on the provided element.
|
|
107
|
+
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
|
108
|
+
* is created and stored in the element's internalData.
|
|
109
|
+
* @param {HTMLElement} elt
|
|
110
|
+
* @param {number} retryCount
|
|
111
|
+
* @returns {EventSource | null}
|
|
112
|
+
*/
|
|
113
|
+
function createEventSourceOnElement(elt, retryCount) {
|
|
114
|
+
|
|
115
|
+
if (elt == null) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
var internalData = api.getInternalData(elt);
|
|
120
|
+
|
|
121
|
+
// get URL from element's attribute
|
|
122
|
+
var sseURL = api.getAttributeValue(elt, "sse-connect");
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if (sseURL == undefined) {
|
|
126
|
+
var legacyURL = getLegacySSEURL(elt)
|
|
127
|
+
if (legacyURL) {
|
|
128
|
+
sseURL = legacyURL;
|
|
129
|
+
} else {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Connect to the EventSource
|
|
135
|
+
var source = htmx.createEventSource(sseURL);
|
|
136
|
+
internalData.sseEventSource = source;
|
|
137
|
+
|
|
138
|
+
// Create event handlers
|
|
139
|
+
source.onerror = function (err) {
|
|
140
|
+
|
|
141
|
+
// Log an error event
|
|
142
|
+
api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source});
|
|
143
|
+
|
|
144
|
+
// If parent no longer exists in the document, then clean up this EventSource
|
|
145
|
+
if (maybeCloseSSESource(elt)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Otherwise, try to reconnect the EventSource
|
|
150
|
+
if (source.readyState === EventSource.CLOSED) {
|
|
151
|
+
retryCount = retryCount || 0;
|
|
152
|
+
var timeout = Math.random() * (2 ^ retryCount) * 500;
|
|
153
|
+
window.setTimeout(function() {
|
|
154
|
+
createEventSourceOnElement(elt, Math.min(7, retryCount+1));
|
|
155
|
+
}, timeout);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// Add message handlers for every `sse-swap` attribute
|
|
160
|
+
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
|
|
161
|
+
|
|
162
|
+
var sseSwapAttr = api.getAttributeValue(child, "sse-swap");
|
|
163
|
+
if (sseSwapAttr) {
|
|
164
|
+
var sseEventNames = sseSwapAttr.split(",");
|
|
165
|
+
} else {
|
|
166
|
+
var sseEventNames = getLegacySSESwaps(child);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (var i = 0 ; i < sseEventNames.length ; i++) {
|
|
170
|
+
var sseEventName = sseEventNames[i].trim();
|
|
171
|
+
var listener = function(event) {
|
|
172
|
+
|
|
173
|
+
// If the parent is missing then close SSE and remove listener
|
|
174
|
+
if (maybeCloseSSESource(elt)) {
|
|
175
|
+
source.removeEventListener(sseEventName, listener);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// swap the response into the DOM and trigger a notification
|
|
180
|
+
swap(child, event.data);
|
|
181
|
+
api.triggerEvent(elt, "htmx:sseMessage", event);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Register the new listener
|
|
185
|
+
api.getInternalData(elt).sseEventListener = listener;
|
|
186
|
+
source.addEventListener(sseEventName, listener);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Add message handlers for every `hx-trigger="sse:*"` attribute
|
|
191
|
+
queryAttributeOnThisOrChildren(elt, "hx-trigger").forEach(function(child) {
|
|
192
|
+
|
|
193
|
+
var sseEventName = api.getAttributeValue(child, "hx-trigger");
|
|
194
|
+
if (sseEventName == null) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Only process hx-triggers for events with the "sse:" prefix
|
|
199
|
+
if (sseEventName.slice(0, 4) != "sse:") {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
var listener = function(event) {
|
|
204
|
+
|
|
205
|
+
// If parent is missing, then close SSE and remove listener
|
|
206
|
+
if (maybeCloseSSESource(elt)) {
|
|
207
|
+
source.removeEventListener(sseEventName, listener);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Trigger events to be handled by the rest of htmx
|
|
212
|
+
htmx.trigger(child, sseEventName, event);
|
|
213
|
+
htmx.trigger(child, "htmx:sseMessage", event);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Register the new listener
|
|
217
|
+
api.getInternalData(elt).sseEventListener = listener;
|
|
218
|
+
source.addEventListener(sseEventName.slice(4), listener);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* maybeCloseSSESource confirms that the parent element still exists.
|
|
224
|
+
* If not, then any associated SSE source is closed and the function returns true.
|
|
225
|
+
*
|
|
226
|
+
* @param {HTMLElement} elt
|
|
227
|
+
* @returns boolean
|
|
228
|
+
*/
|
|
229
|
+
function maybeCloseSSESource(elt) {
|
|
230
|
+
if (!api.bodyContains(elt)) {
|
|
231
|
+
var source = api.getInternalData(elt).sseEventSource;
|
|
232
|
+
if (source != undefined) {
|
|
233
|
+
source.close();
|
|
234
|
+
// source = null
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
|
|
243
|
+
*
|
|
244
|
+
* @param {HTMLElement} elt
|
|
245
|
+
* @param {string} attributeName
|
|
246
|
+
*/
|
|
247
|
+
function queryAttributeOnThisOrChildren(elt, attributeName) {
|
|
248
|
+
|
|
249
|
+
var result = [];
|
|
250
|
+
|
|
251
|
+
// If the parent element also contains the requested attribute, then add it to the results too.
|
|
252
|
+
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) {
|
|
253
|
+
result.push(elt);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Search all child nodes that match the requested attribute
|
|
257
|
+
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) {
|
|
258
|
+
result.push(node);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param {HTMLElement} elt
|
|
266
|
+
* @param {string} content
|
|
267
|
+
*/
|
|
268
|
+
function swap(elt, content) {
|
|
269
|
+
|
|
270
|
+
api.withExtensions(elt, function(extension) {
|
|
271
|
+
content = extension.transformResponse(content, null, elt);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
var swapSpec = api.getSwapSpecification(elt);
|
|
275
|
+
var target = api.getTarget(elt);
|
|
276
|
+
var settleInfo = api.makeSettleInfo(elt);
|
|
277
|
+
|
|
278
|
+
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
|
|
279
|
+
|
|
280
|
+
settleInfo.elts.forEach(function (elt) {
|
|
281
|
+
if (elt.classList) {
|
|
282
|
+
elt.classList.add(htmx.config.settlingClass);
|
|
283
|
+
}
|
|
284
|
+
api.triggerEvent(elt, 'htmx:beforeSettle');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Handle settle tasks (with delay if requested)
|
|
288
|
+
if (swapSpec.settleDelay > 0) {
|
|
289
|
+
setTimeout(doSettle(settleInfo), swapSpec.settleDelay);
|
|
290
|
+
} else {
|
|
291
|
+
doSettle(settleInfo)();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* doSettle mirrors much of the functionality in htmx that
|
|
297
|
+
* settles elements after their content has been swapped.
|
|
298
|
+
* TODO: this should be published by htmx, and not duplicated here
|
|
299
|
+
* @param {import("../htmx").HtmxSettleInfo} settleInfo
|
|
300
|
+
* @returns () => void
|
|
301
|
+
*/
|
|
302
|
+
function doSettle(settleInfo) {
|
|
303
|
+
|
|
304
|
+
return function() {
|
|
305
|
+
settleInfo.tasks.forEach(function (task) {
|
|
306
|
+
task.call();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
settleInfo.elts.forEach(function (elt) {
|
|
310
|
+
if (elt.classList) {
|
|
311
|
+
elt.classList.remove(htmx.config.settlingClass);
|
|
312
|
+
}
|
|
313
|
+
api.triggerEvent(elt, 'htmx:afterSettle');
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
})();
|