djhtmx 1.2.8__tar.gz → 1.2.9__tar.gz
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.
- {djhtmx-1.2.8 → djhtmx-1.2.9}/CHANGELOG.md +9 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/PKG-INFO +1 -1
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/__init__.py +1 -1
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/command_queue.py +11 -1
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/component.py +51 -9
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/consumer.py +2 -1
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/repo.py +11 -1
- djhtmx-1.2.9/src/djhtmx/static/htmx/django.js +222 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/testing.py +10 -2
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/urls.py +15 -1
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/utils.py +17 -14
- djhtmx-1.2.8/src/djhtmx/static/htmx/django.js +0 -198
- {djhtmx-1.2.8 → djhtmx-1.2.9}/.gitignore +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/LICENSE +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/MANIFEST.in +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/README.md +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/pyproject.toml +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/apps.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/commands.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/context.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/exceptions.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/global_events.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/introspection.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/json.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/management/commands/htmx.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/middleware.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/query.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/settings.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/ext/idiomorph-ext.min.js +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/ext/ws.js +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.amd.js +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.cjs.js +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.esm.d.ts +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.esm.js +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.js +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.min.js +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/templates/htmx/headers.html +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/templates/htmx/lazy.html +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/templatetags/__init__.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/templatetags/htmx.py +0 -0
- {djhtmx-1.2.8 → djhtmx-1.2.9}/src/djhtmx/tracing.py +0 -0
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.9] - 2026-01-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **ScrollIntoView Command**: New command that scrolls elements into view with configurable behavior (`auto`, `smooth`, `instant`) and block alignment (`start`, `center`, `end`, `nearest`). Includes WebSocket and HTTP trigger support with strict Python typing via Literal types.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **Import Error Handling**: Import errors during HTMX module autodiscovery are no longer suppressed. This change improves error visibility and helps catch import issues in user code earlier. Previously, import errors were silently caught, which could hide configuration or dependency problems.
|
|
17
|
+
- Refactored `django.js` to use modern JavaScript patterns (for...of loops, const declarations) for improved code quality and maintainability
|
|
18
|
+
|
|
10
19
|
## [1.2.8] - 2026-01-06
|
|
11
20
|
|
|
12
21
|
### Added
|
|
@@ -16,6 +16,7 @@ from .component import (
|
|
|
16
16
|
Open,
|
|
17
17
|
Redirect,
|
|
18
18
|
Render,
|
|
19
|
+
ScrollIntoView,
|
|
19
20
|
Signal,
|
|
20
21
|
SkipRender,
|
|
21
22
|
)
|
|
@@ -86,6 +87,7 @@ class CommandQueue:
|
|
|
86
87
|
| Emit()
|
|
87
88
|
| SkipRender()
|
|
88
89
|
| Focus()
|
|
90
|
+
| ScrollIntoView()
|
|
89
91
|
| Redirect()
|
|
90
92
|
| DispatchDOMEvent()
|
|
91
93
|
| Open()
|
|
@@ -139,7 +141,15 @@ class CommandQueue:
|
|
|
139
141
|
return 6, component.id, timestamp
|
|
140
142
|
else:
|
|
141
143
|
return 7, component.id, timestamp
|
|
142
|
-
case
|
|
144
|
+
case (
|
|
145
|
+
Focus()
|
|
146
|
+
| ScrollIntoView()
|
|
147
|
+
| Redirect()
|
|
148
|
+
| DispatchDOMEvent()
|
|
149
|
+
| Open()
|
|
150
|
+
| ReplaceURL()
|
|
151
|
+
| PushURL()
|
|
152
|
+
):
|
|
143
153
|
return 8, "", 0
|
|
144
154
|
case _ as unreachable:
|
|
145
155
|
assert_never(unreachable)
|
|
@@ -89,6 +89,16 @@ class Focus:
|
|
|
89
89
|
command: Literal["focus"] = "focus"
|
|
90
90
|
|
|
91
91
|
|
|
92
|
+
@dataclass(slots=True)
|
|
93
|
+
class ScrollIntoView:
|
|
94
|
+
"Scrolls the browser element that matches `selector` into view"
|
|
95
|
+
|
|
96
|
+
selector: str
|
|
97
|
+
behavior: Literal["auto", "smooth", "instant"] = "smooth"
|
|
98
|
+
block: Literal["start", "center", "end", "nearest"] = "center"
|
|
99
|
+
command: Literal["scroll_into_view"] = "scroll_into_view"
|
|
100
|
+
|
|
101
|
+
|
|
92
102
|
@dataclass(slots=True)
|
|
93
103
|
class Execute:
|
|
94
104
|
component_id: str
|
|
@@ -126,34 +136,62 @@ class BuildAndRender:
|
|
|
126
136
|
|
|
127
137
|
@classmethod
|
|
128
138
|
def append(
|
|
129
|
-
cls,
|
|
139
|
+
cls,
|
|
140
|
+
target_: str,
|
|
141
|
+
component_: type[HtmxComponent],
|
|
142
|
+
parent_id: str | None = None,
|
|
143
|
+
**state,
|
|
130
144
|
):
|
|
131
145
|
return cls(
|
|
132
|
-
component=component_,
|
|
146
|
+
component=component_,
|
|
147
|
+
state=state,
|
|
148
|
+
oob=f"beforeend: {target_}",
|
|
149
|
+
parent_id=parent_id,
|
|
133
150
|
)
|
|
134
151
|
|
|
135
152
|
@classmethod
|
|
136
153
|
def prepend(
|
|
137
|
-
cls,
|
|
154
|
+
cls,
|
|
155
|
+
target_: str,
|
|
156
|
+
component_: type[HtmxComponent],
|
|
157
|
+
parent_id: str | None = None,
|
|
158
|
+
**state,
|
|
138
159
|
):
|
|
139
160
|
return cls(
|
|
140
|
-
component=component_,
|
|
161
|
+
component=component_,
|
|
162
|
+
state=state,
|
|
163
|
+
oob=f"afterbegin: {target_}",
|
|
164
|
+
parent_id=parent_id,
|
|
141
165
|
)
|
|
142
166
|
|
|
143
167
|
@classmethod
|
|
144
168
|
def after(
|
|
145
|
-
cls,
|
|
169
|
+
cls,
|
|
170
|
+
target_: str,
|
|
171
|
+
component_: type[HtmxComponent],
|
|
172
|
+
parent_id: str | None = None,
|
|
173
|
+
**state,
|
|
146
174
|
):
|
|
147
175
|
return cls(
|
|
148
|
-
component=component_,
|
|
176
|
+
component=component_,
|
|
177
|
+
state=state,
|
|
178
|
+
oob=f"afterend: {target_}",
|
|
179
|
+
parent_id=parent_id,
|
|
149
180
|
)
|
|
150
181
|
|
|
151
182
|
@classmethod
|
|
152
183
|
def before(
|
|
153
|
-
cls,
|
|
184
|
+
cls,
|
|
185
|
+
target_: str,
|
|
186
|
+
component_: type[HtmxComponent],
|
|
187
|
+
parent_id: str | None = None,
|
|
188
|
+
**state,
|
|
154
189
|
):
|
|
155
190
|
return cls(
|
|
156
|
-
component=component_,
|
|
191
|
+
component=component_,
|
|
192
|
+
state=state,
|
|
193
|
+
oob=f"beforebegin: {target_}",
|
|
194
|
+
parent_id=parent_id,
|
|
157
195
|
)
|
|
158
196
|
|
|
159
197
|
@classmethod
|
|
@@ -191,6 +229,7 @@ Command = (
|
|
|
191
229
|
Destroy
|
|
192
230
|
| Redirect
|
|
193
231
|
| Focus
|
|
232
|
+
| ScrollIntoView
|
|
194
233
|
| DispatchDOMEvent
|
|
195
234
|
| SkipRender
|
|
196
235
|
| BuildAndRender
|
|
@@ -249,7 +288,10 @@ def get_template(template: str) -> RenderFunction: # pragma: no cover
|
|
|
249
288
|
return cast(RenderFunction, _compose(loader.get_template(template).render, mark_safe))
|
|
250
289
|
else:
|
|
251
290
|
if (render := RENDER_FUNC.get(template)) is None:
|
|
252
|
-
render = cast(
|
|
291
|
+
render = cast(
|
|
292
|
+
RenderFunction,
|
|
293
|
+
_compose(loader.get_template(template).render, mark_safe),
|
|
294
|
+
)
|
|
253
295
|
RENDER_FUNC[template] = render
|
|
254
296
|
return render
|
|
255
297
|
|
|
@@ -6,7 +6,7 @@ from pydantic import BaseModel, TypeAdapter
|
|
|
6
6
|
|
|
7
7
|
from . import json
|
|
8
8
|
from .commands import PushURL, ReplaceURL, SendHtml
|
|
9
|
-
from .component import Command, Destroy, DispatchDOMEvent, Focus, Open, Redirect
|
|
9
|
+
from .component import Command, Destroy, DispatchDOMEvent, Focus, Open, Redirect, ScrollIntoView
|
|
10
10
|
from .introspection import parse_request_data
|
|
11
11
|
from .repo import Repository
|
|
12
12
|
from .utils import get_params
|
|
@@ -58,6 +58,7 @@ class Consumer(AsyncJsonWebsocketConsumer):
|
|
|
58
58
|
Destroy()
|
|
59
59
|
| Redirect()
|
|
60
60
|
| Focus()
|
|
61
|
+
| ScrollIntoView()
|
|
61
62
|
| DispatchDOMEvent()
|
|
62
63
|
| PushURL()
|
|
63
64
|
| Open()
|
|
@@ -35,6 +35,7 @@ from .component import (
|
|
|
35
35
|
Open,
|
|
36
36
|
Redirect,
|
|
37
37
|
Render,
|
|
38
|
+
ScrollIntoView,
|
|
38
39
|
Signal,
|
|
39
40
|
SkipRender,
|
|
40
41
|
_get_query_patchers,
|
|
@@ -56,7 +57,15 @@ logger = logging.getLogger(__name__)
|
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
ProcessedCommand = (
|
|
59
|
-
Destroy
|
|
60
|
+
Destroy
|
|
61
|
+
| Redirect
|
|
62
|
+
| Open
|
|
63
|
+
| Focus
|
|
64
|
+
| ScrollIntoView
|
|
65
|
+
| DispatchDOMEvent
|
|
66
|
+
| SendHtml
|
|
67
|
+
| PushURL
|
|
68
|
+
| ReplaceURL
|
|
60
69
|
)
|
|
61
70
|
|
|
62
71
|
|
|
@@ -296,6 +305,7 @@ class Repository:
|
|
|
296
305
|
| PushURL()
|
|
297
306
|
| Redirect()
|
|
298
307
|
| Focus()
|
|
308
|
+
| ScrollIntoView()
|
|
299
309
|
| DispatchDOMEvent() as command
|
|
300
310
|
):
|
|
301
311
|
commands.processing_component_id = ""
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// WebSocket Management
|
|
3
|
+
const sentComponents = new Set();
|
|
4
|
+
|
|
5
|
+
function sendRemovedComponents(event) {
|
|
6
|
+
const removedComponents = Array.from(sentComponents).filter(
|
|
7
|
+
(id) => !document.getElementById(id),
|
|
8
|
+
);
|
|
9
|
+
for (const id of removedComponents) {
|
|
10
|
+
sentComponents.delete(id);
|
|
11
|
+
}
|
|
12
|
+
if (removedComponents.length) {
|
|
13
|
+
event.detail.socketWrapper.send(
|
|
14
|
+
JSON.stringify({
|
|
15
|
+
type: "removed",
|
|
16
|
+
component_ids: removedComponents,
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sendAddedComponents(event) {
|
|
23
|
+
const states = [];
|
|
24
|
+
const subscriptions = new Map();
|
|
25
|
+
const ids = new Set();
|
|
26
|
+
|
|
27
|
+
for (const element of Array.from(
|
|
28
|
+
document.querySelectorAll("[data-hx-state]"),
|
|
29
|
+
).filter((el) => !sentComponents.has(el.id))) {
|
|
30
|
+
const hxSubscriptions = element.dataset.hxSubscriptions;
|
|
31
|
+
if (hxSubscriptions !== undefined) {
|
|
32
|
+
subscriptions[element.id] = element.dataset.hxSubscriptions;
|
|
33
|
+
}
|
|
34
|
+
states.push(element.dataset.hxState);
|
|
35
|
+
ids.add(element.id);
|
|
36
|
+
}
|
|
37
|
+
for (const id of ids) {
|
|
38
|
+
sentComponents.add(id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (ids.size) {
|
|
42
|
+
event.detail.socketWrapper.send(
|
|
43
|
+
JSON.stringify({
|
|
44
|
+
type: "added",
|
|
45
|
+
states,
|
|
46
|
+
subscriptions,
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeHtmxIndicator() {
|
|
53
|
+
// remove indicator
|
|
54
|
+
for (const el of document.querySelectorAll(".htmx-request")) {
|
|
55
|
+
el.classList.remove("htmx-request");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
document.addEventListener("htmx:wsOpen", (event) => {
|
|
60
|
+
console.log("OPEN", event);
|
|
61
|
+
sentComponents.clear();
|
|
62
|
+
removeHtmxIndicator();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
document.addEventListener("htmx:wsClose", (event) => {
|
|
66
|
+
console.log("CLOSE", event);
|
|
67
|
+
sentComponents.clear();
|
|
68
|
+
removeHtmxIndicator();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
document.addEventListener("htmx:wsConfigSend", (event) => {
|
|
72
|
+
// add indicator
|
|
73
|
+
const indicatorSelector = event.detail.elt
|
|
74
|
+
.closest("[hx-indicator]")
|
|
75
|
+
?.getAttribute("hx-indicator");
|
|
76
|
+
if (indicatorSelector) {
|
|
77
|
+
for (const el of document.querySelectorAll(indicatorSelector)) {
|
|
78
|
+
el.classList.add("htmx-request");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// send current state
|
|
83
|
+
sendRemovedComponents(event);
|
|
84
|
+
sendAddedComponents(event);
|
|
85
|
+
|
|
86
|
+
// enrich event message
|
|
87
|
+
event.detail.headers["HX-Component-Id"] =
|
|
88
|
+
event.detail.elt.closest("[data-hx-state]").id;
|
|
89
|
+
event.detail.headers["HX-Component-Handler"] =
|
|
90
|
+
event.detail.elt.getAttribute("ws-send");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
document.addEventListener("htmx:wsBeforeMessage", (event) => {
|
|
94
|
+
removeHtmxIndicator();
|
|
95
|
+
|
|
96
|
+
// process message
|
|
97
|
+
if (event.detail.message.startsWith("{")) {
|
|
98
|
+
const commandData = JSON.parse(event.detail.message);
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
const { command } = commandData;
|
|
101
|
+
switch (command) {
|
|
102
|
+
case "destroy": {
|
|
103
|
+
const { component_id } = commandData;
|
|
104
|
+
document.getElementById(component_id)?.remove();
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "focus": {
|
|
108
|
+
const { selector } = commandData;
|
|
109
|
+
document.querySelector(selector)?.focus();
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "scroll_into_view": {
|
|
113
|
+
const { selector, behavior = "smooth", block = "center" } = commandData;
|
|
114
|
+
document.querySelector(selector)?.scrollIntoView({
|
|
115
|
+
behavior,
|
|
116
|
+
block,
|
|
117
|
+
});
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case "redirect": {
|
|
121
|
+
const { url } = commandData;
|
|
122
|
+
location.assign(url);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "dispatch_event": {
|
|
126
|
+
const { target, detail, buubles, cancelable, composed } = commandData;
|
|
127
|
+
document.querySelector(target)?.dispatchEvent(
|
|
128
|
+
new CustomEvent(event, {
|
|
129
|
+
detail,
|
|
130
|
+
buubles,
|
|
131
|
+
cancelable,
|
|
132
|
+
composed,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "send_state": {
|
|
138
|
+
const { component_id, state } = commandData;
|
|
139
|
+
const component = document.getElementById(component_id);
|
|
140
|
+
if (component) {
|
|
141
|
+
component.dataset.hxState = state;
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case "push_url": {
|
|
146
|
+
const { url } = commandData;
|
|
147
|
+
history.pushState({}, document.title, url);
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
default:
|
|
152
|
+
console.error("Can't process command:", event.detail.message);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
document.addEventListener("hxDispatchDOMEvent", (event) => {
|
|
159
|
+
for (const {
|
|
160
|
+
event: eventName,
|
|
161
|
+
target,
|
|
162
|
+
detail,
|
|
163
|
+
bubbles,
|
|
164
|
+
cancelable,
|
|
165
|
+
composed,
|
|
166
|
+
} of event.detail.value) {
|
|
167
|
+
const el = document.querySelector(target);
|
|
168
|
+
if (el) {
|
|
169
|
+
// This setTimeout basically queues the dispatch of the event
|
|
170
|
+
// to avoid dispatching events within events handlers.
|
|
171
|
+
setTimeout(
|
|
172
|
+
() =>
|
|
173
|
+
el.dispatchEvent(
|
|
174
|
+
new CustomEvent(eventName, {
|
|
175
|
+
detail,
|
|
176
|
+
bubbles,
|
|
177
|
+
cancelable,
|
|
178
|
+
composed,
|
|
179
|
+
}),
|
|
180
|
+
),
|
|
181
|
+
0,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
document.addEventListener("hxFocus", (event) => {
|
|
188
|
+
for (const selector of event.detail.value) {
|
|
189
|
+
document.querySelector(selector).focus();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
document.addEventListener("hxScrollIntoView", (event) => {
|
|
194
|
+
for (const item of event.detail.value) {
|
|
195
|
+
const selector = typeof item === "string" ? item : item.selector;
|
|
196
|
+
const behavior = typeof item === "object" ? item.behavior || "smooth" : "smooth";
|
|
197
|
+
const block = typeof item === "object" ? item.block || "center" : "center";
|
|
198
|
+
document.querySelector(selector)?.scrollIntoView({
|
|
199
|
+
behavior,
|
|
200
|
+
block,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
document.addEventListener("hxOpenURL", (event) => {
|
|
206
|
+
for (const { url, name, target, rel } of event.detail.value) {
|
|
207
|
+
const link = document.createElement("a");
|
|
208
|
+
link.href = url;
|
|
209
|
+
link.target = target || "_blank";
|
|
210
|
+
if (name) {
|
|
211
|
+
link.download = name;
|
|
212
|
+
}
|
|
213
|
+
if (rel) {
|
|
214
|
+
link.rel = rel;
|
|
215
|
+
}
|
|
216
|
+
link.click();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
})();
|
|
220
|
+
// Local Variables:
|
|
221
|
+
// js-indent-level: 4
|
|
222
|
+
// End:
|
|
@@ -13,7 +13,15 @@ from pygments.lexers import HtmlLexer
|
|
|
13
13
|
|
|
14
14
|
from . import json
|
|
15
15
|
from .commands import PushURL, ReplaceURL, SendHtml
|
|
16
|
-
from .component import
|
|
16
|
+
from .component import (
|
|
17
|
+
Destroy,
|
|
18
|
+
DispatchDOMEvent,
|
|
19
|
+
Focus,
|
|
20
|
+
HtmxComponent,
|
|
21
|
+
Open,
|
|
22
|
+
Redirect,
|
|
23
|
+
ScrollIntoView,
|
|
24
|
+
)
|
|
17
25
|
from .introspection import parse_request_data
|
|
18
26
|
from .repo import Repository, Session, signer
|
|
19
27
|
from .utils import get_params
|
|
@@ -187,7 +195,7 @@ class Htmx:
|
|
|
187
195
|
self.path = parsed_url.path
|
|
188
196
|
self.query_string = parsed_url.query
|
|
189
197
|
|
|
190
|
-
case Focus() | DispatchDOMEvent():
|
|
198
|
+
case Focus() | ScrollIntoView() | DispatchDOMEvent():
|
|
191
199
|
pass
|
|
192
200
|
|
|
193
201
|
if navigate_to_url:
|
|
@@ -11,7 +11,16 @@ from django.utils.html import format_html
|
|
|
11
11
|
from django.views.decorators.csrf import csrf_exempt
|
|
12
12
|
|
|
13
13
|
from .commands import PushURL, ReplaceURL, SendHtml
|
|
14
|
-
from .component import
|
|
14
|
+
from .component import (
|
|
15
|
+
REGISTRY,
|
|
16
|
+
Destroy,
|
|
17
|
+
DispatchDOMEvent,
|
|
18
|
+
Focus,
|
|
19
|
+
Open,
|
|
20
|
+
Redirect,
|
|
21
|
+
ScrollIntoView,
|
|
22
|
+
Triggers,
|
|
23
|
+
)
|
|
15
24
|
from .consumer import Consumer
|
|
16
25
|
from .introspection import parse_request_data
|
|
17
26
|
from .repo import Repository
|
|
@@ -53,6 +62,11 @@ def endpoint(request: HttpRequest, component_name: str, component_id: str, event
|
|
|
53
62
|
headers["HX-Redirect"] = url
|
|
54
63
|
case Focus(selector):
|
|
55
64
|
triggers.after_settle("hxFocus", selector)
|
|
65
|
+
case ScrollIntoView(selector, behavior, block):
|
|
66
|
+
triggers.after_settle(
|
|
67
|
+
"hxScrollIntoView",
|
|
68
|
+
{"selector": selector, "behavior": behavior, "block": block},
|
|
69
|
+
)
|
|
56
70
|
case Open(url, name, target, rel):
|
|
57
71
|
triggers.after_settle(
|
|
58
72
|
"hxOpenURL",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import contextlib
|
|
2
1
|
import importlib
|
|
2
|
+
import logging
|
|
3
3
|
import pkgutil
|
|
4
4
|
import typing as t
|
|
5
5
|
from urllib.parse import urlparse
|
|
@@ -129,17 +129,20 @@ def autodiscover_htmx_modules():
|
|
|
129
129
|
- htmx.py files (like standard autodiscover_modules("htmx"))
|
|
130
130
|
- All Python files under htmx/ directories in apps (recursively)
|
|
131
131
|
"""
|
|
132
|
-
|
|
133
|
-
def _import_modules_recursively(module_name):
|
|
134
|
-
"""Recursively import a module and all its submodules."""
|
|
135
|
-
with contextlib.suppress(ImportError):
|
|
136
|
-
module = importlib.import_module(module_name)
|
|
137
|
-
|
|
138
|
-
# If this is a package, recursively import all modules in it
|
|
139
|
-
if hasattr(module, "__path__"):
|
|
140
|
-
for _finder, submodule_name, _is_pkg in pkgutil.iter_modules(module.__path__):
|
|
141
|
-
_import_modules_recursively(f"{module_name}.{submodule_name}")
|
|
142
|
-
|
|
143
132
|
for app_config in apps.get_app_configs():
|
|
144
|
-
|
|
145
|
-
|
|
133
|
+
module_name = f"{app_config.module.__name__}.htmx"
|
|
134
|
+
try:
|
|
135
|
+
module = importlib.import_module(module_name)
|
|
136
|
+
except ImportError:
|
|
137
|
+
logger.warning("Could not import %s", module_name)
|
|
138
|
+
continue
|
|
139
|
+
if hasattr(module, "__path__"):
|
|
140
|
+
# If it's a package, recursively walk it importing all modules and packages.
|
|
141
|
+
for info in pkgutil.walk_packages(module.__path__, prefix=module_name + "."):
|
|
142
|
+
if not info.ispkg:
|
|
143
|
+
# `walk_packages` only imports packages, not modules; we need to import them
|
|
144
|
+
# all.
|
|
145
|
+
importlib.import_module(info.name)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
logger = logging.getLogger(__name__)
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
(function () {
|
|
2
|
-
// WebSocket Management
|
|
3
|
-
let sentComponents = new Set();
|
|
4
|
-
|
|
5
|
-
function sendRemovedComponents(event) {
|
|
6
|
-
let removedComponents = Array.from(sentComponents).filter(
|
|
7
|
-
(id) => !document.getElementById(id),
|
|
8
|
-
);
|
|
9
|
-
removedComponents.forEach((id) => sentComponents.delete(id));
|
|
10
|
-
if (removedComponents.length) {
|
|
11
|
-
event.detail.socketWrapper.send(
|
|
12
|
-
JSON.stringify({
|
|
13
|
-
type: "removed",
|
|
14
|
-
component_ids: removedComponents,
|
|
15
|
-
}),
|
|
16
|
-
);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function sendAddedComponents(event) {
|
|
21
|
-
let states = [];
|
|
22
|
-
let subscriptions = new Map();
|
|
23
|
-
let ids = new Set();
|
|
24
|
-
|
|
25
|
-
Array.from(document.querySelectorAll("[data-hx-state]"))
|
|
26
|
-
.filter((el) => !sentComponents.has(el.id))
|
|
27
|
-
.forEach((element) => {
|
|
28
|
-
let hxSubscriptions = element.dataset.hxSubscriptions;
|
|
29
|
-
if (hxSubscriptions !== undefined) {
|
|
30
|
-
subscriptions[element.id] = element.dataset.hxSubscriptions;
|
|
31
|
-
}
|
|
32
|
-
states.push(element.dataset.hxState);
|
|
33
|
-
ids.add(element.id);
|
|
34
|
-
});
|
|
35
|
-
ids.forEach((id) => sentComponents.add(id));
|
|
36
|
-
|
|
37
|
-
if (ids.size) {
|
|
38
|
-
event.detail.socketWrapper.send(
|
|
39
|
-
JSON.stringify({
|
|
40
|
-
type: "added",
|
|
41
|
-
states,
|
|
42
|
-
subscriptions,
|
|
43
|
-
}),
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function removeHtmxIndicator() {
|
|
49
|
-
// remove indicator
|
|
50
|
-
document
|
|
51
|
-
.querySelectorAll(".htmx-request")
|
|
52
|
-
.forEach((el) => el.classList.remove("htmx-request"));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
document.addEventListener("htmx:wsOpen", (event) => {
|
|
56
|
-
console.log("OPEN", event);
|
|
57
|
-
sentComponents.clear();
|
|
58
|
-
removeHtmxIndicator();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
document.addEventListener("htmx:wsClose", (event) => {
|
|
62
|
-
console.log("CLOSE", event);
|
|
63
|
-
sentComponents.clear();
|
|
64
|
-
removeHtmxIndicator();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
document.addEventListener("htmx:wsConfigSend", (event) => {
|
|
68
|
-
// add indicator
|
|
69
|
-
let indicatorSelector = event.detail.elt
|
|
70
|
-
.closest("[hx-indicator]")
|
|
71
|
-
?.getAttribute("hx-indicator");
|
|
72
|
-
if (indicatorSelector) {
|
|
73
|
-
document
|
|
74
|
-
.querySelectorAll(indicatorSelector)
|
|
75
|
-
.forEach((el) => el.classList.add("htmx-request"));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// send current state
|
|
79
|
-
sendRemovedComponents(event);
|
|
80
|
-
sendAddedComponents(event);
|
|
81
|
-
|
|
82
|
-
// enrich event message
|
|
83
|
-
event.detail.headers["HX-Component-Id"] =
|
|
84
|
-
event.detail.elt.closest("[data-hx-state]").id;
|
|
85
|
-
event.detail.headers["HX-Component-Handler"] =
|
|
86
|
-
event.detail.elt.getAttribute("ws-send");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
document.addEventListener("htmx:wsBeforeMessage", (event) => {
|
|
90
|
-
removeHtmxIndicator();
|
|
91
|
-
|
|
92
|
-
// process message
|
|
93
|
-
if (event.detail.message.startsWith("{")) {
|
|
94
|
-
let commandData = JSON.parse(event.detail.message);
|
|
95
|
-
event.preventDefault();
|
|
96
|
-
let { command } = commandData;
|
|
97
|
-
switch (command) {
|
|
98
|
-
case "destroy": {
|
|
99
|
-
let { component_id } = commandData;
|
|
100
|
-
document.getElementById(component_id)?.remove();
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
case "focus": {
|
|
104
|
-
let { selector } = commandData;
|
|
105
|
-
document.querySelector(selector)?.focus();
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
case "redirect": {
|
|
109
|
-
let { url } = commandData;
|
|
110
|
-
location.assign(url);
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
case "dispatch_event": {
|
|
114
|
-
let = { target, detail, buubles, cancelable, composed } =
|
|
115
|
-
commandData;
|
|
116
|
-
document.querySelector(target)?.dispatchEvent(
|
|
117
|
-
new CustomEvent(event, {
|
|
118
|
-
detail,
|
|
119
|
-
buubles,
|
|
120
|
-
cancelable,
|
|
121
|
-
composed,
|
|
122
|
-
}),
|
|
123
|
-
);
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
case "send_state": {
|
|
127
|
-
let { component_id, state } = commandData;
|
|
128
|
-
let component = document.getElementById(component_id);
|
|
129
|
-
if (component) {
|
|
130
|
-
component.dataset.hxState = state;
|
|
131
|
-
}
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
case "push_url": {
|
|
135
|
-
let { url } = commandData;
|
|
136
|
-
history.pushState({}, document.title, url);
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
default:
|
|
141
|
-
console.error(
|
|
142
|
-
"Can't process command:",
|
|
143
|
-
event.detail.message,
|
|
144
|
-
);
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
document.addEventListener("hxDispatchDOMEvent", (event) => {
|
|
151
|
-
event.detail.value.map(
|
|
152
|
-
({ event, target, detail, bubbles, cancelable, composed }) => {
|
|
153
|
-
let el = document.querySelector(target);
|
|
154
|
-
if (typeof el != "undefined" && el != null) {
|
|
155
|
-
// This setTimeout basically queues the dispatch of the event
|
|
156
|
-
// to avoid dispatching events within events handlers.
|
|
157
|
-
setTimeout(
|
|
158
|
-
() =>
|
|
159
|
-
el.dispatchEvent(
|
|
160
|
-
new CustomEvent(event, {
|
|
161
|
-
detail,
|
|
162
|
-
bubbles,
|
|
163
|
-
cancelable,
|
|
164
|
-
composed,
|
|
165
|
-
}),
|
|
166
|
-
),
|
|
167
|
-
0,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
document.addEventListener("hxFocus", (event) => {
|
|
175
|
-
event.detail.value.map((selector) => {
|
|
176
|
-
document.querySelector(selector).focus();
|
|
177
|
-
});
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
document.addEventListener("hxOpenURL", (event) => {
|
|
181
|
-
event.detail.value.map(({ url, name, target, rel }) => {
|
|
182
|
-
const link = document.createElement('a');
|
|
183
|
-
link.href = url;
|
|
184
|
-
link.target = !!target ? target : '_blank';
|
|
185
|
-
if (name) {
|
|
186
|
-
link.download = name;
|
|
187
|
-
}
|
|
188
|
-
if (rel) {
|
|
189
|
-
link.rel = rel;
|
|
190
|
-
}
|
|
191
|
-
link.click();
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
})();
|
|
196
|
-
// Local Variables:
|
|
197
|
-
// js-indent-level: 4
|
|
198
|
-
// End:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|