djhtmx 1.2.7__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.
Files changed (41) hide show
  1. {djhtmx-1.2.7 → djhtmx-1.2.9}/CHANGELOG.md +14 -0
  2. {djhtmx-1.2.7 → djhtmx-1.2.9}/PKG-INFO +1 -1
  3. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/__init__.py +1 -1
  4. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/command_queue.py +11 -1
  5. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/component.py +51 -9
  6. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/consumer.py +2 -1
  7. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/repo.py +11 -1
  8. djhtmx-1.2.9/src/djhtmx/static/htmx/2.0.4/ext/idiomorph-ext.min.js +1 -0
  9. djhtmx-1.2.9/src/djhtmx/static/htmx/django.js +222 -0
  10. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/testing.py +10 -2
  11. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/urls.py +15 -1
  12. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/utils.py +17 -14
  13. djhtmx-1.2.7/src/djhtmx/static/htmx/django.js +0 -198
  14. {djhtmx-1.2.7 → djhtmx-1.2.9}/.gitignore +0 -0
  15. {djhtmx-1.2.7 → djhtmx-1.2.9}/LICENSE +0 -0
  16. {djhtmx-1.2.7 → djhtmx-1.2.9}/MANIFEST.in +0 -0
  17. {djhtmx-1.2.7 → djhtmx-1.2.9}/README.md +0 -0
  18. {djhtmx-1.2.7 → djhtmx-1.2.9}/pyproject.toml +0 -0
  19. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/apps.py +0 -0
  20. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/commands.py +0 -0
  21. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/context.py +0 -0
  22. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/exceptions.py +0 -0
  23. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/global_events.py +0 -0
  24. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/introspection.py +0 -0
  25. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/json.py +0 -0
  26. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/management/commands/htmx.py +0 -0
  27. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/middleware.py +0 -0
  28. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/query.py +0 -0
  29. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/settings.py +0 -0
  30. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/ext/ws.js +0 -0
  31. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.amd.js +0 -0
  32. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.cjs.js +0 -0
  33. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.esm.d.ts +0 -0
  34. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.esm.js +0 -0
  35. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.js +0 -0
  36. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/static/htmx/2.0.4/htmx.min.js +0 -0
  37. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/templates/htmx/headers.html +0 -0
  38. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/templates/htmx/lazy.html +0 -0
  39. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/templatetags/__init__.py +0 -0
  40. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/templatetags/htmx.py +0 -0
  41. {djhtmx-1.2.7 → djhtmx-1.2.9}/src/djhtmx/tracing.py +0 -0
@@ -7,6 +7,20 @@ 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
+
19
+ ## [1.2.8] - 2026-01-06
20
+
21
+ ### Added
22
+ - Added idiomorph extension (idiomorph-ext.min.js) for HTMX 2.0.4
23
+
10
24
  ## [1.2.7] - 2026-01-05
11
25
 
12
26
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: djhtmx
3
- Version: 1.2.7
3
+ Version: 1.2.9
4
4
  Summary: Interactive UI Components for Django using HTMX
5
5
  Project-URL: Homepage, https://github.com/edelvalle/djhtmx
6
6
  Project-URL: Documentation, https://github.com/edelvalle/djhtmx#readme
@@ -1,4 +1,4 @@
1
1
  from .middleware import middleware
2
2
 
3
- __version__ = "1.2.7"
3
+ __version__ = "1.2.9"
4
4
  __all__ = ("middleware",)
@@ -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 Focus() | Redirect() | DispatchDOMEvent() | Open() | ReplaceURL() | PushURL():
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, target_: str, component_: type[HtmxComponent], parent_id: str | None = None, **state
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_, state=state, oob=f"beforeend: {target_}", parent_id=parent_id
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, target_: str, component_: type[HtmxComponent], parent_id: str | None = None, **state
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_, state=state, oob=f"afterbegin: {target_}", parent_id=parent_id
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, target_: str, component_: type[HtmxComponent], parent_id: str | None = None, **state
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_, state=state, oob=f"afterend: {target_}", parent_id=parent_id
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, target_: str, component_: type[HtmxComponent], parent_id: str | None = None, **state
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_, state=state, oob=f"beforebegin: {target_}", parent_id=parent_id
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(RenderFunction, _compose(loader.get_template(template).render, mark_safe))
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 | Redirect | Open | Focus | DispatchDOMEvent | SendHtml | PushURL | ReplaceURL
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 @@
1
+ var Idiomorph=function(){"use strict";const e=()=>{};const n={morphStyle:"outerHTML",callbacks:{beforeNodeAdded:e,afterNodeAdded:e,beforeNodeMorphed:e,afterNodeMorphed:e,beforeNodeRemoved:e,afterNodeRemoved:e,beforeAttributeUpdated:e},head:{style:"merge",shouldPreserve:e=>e.getAttribute("im-preserve")==="true",shouldReAppend:e=>e.getAttribute("im-re-append")==="true",shouldRemove:e,afterHeadMorphed:e},restoreFocus:true};function t(t,e,n={}){t=d(t);const r=f(e);const i=c(t,r,n);const o=a(i,()=>{return u(i,t,r,e=>{if(e.morphStyle==="innerHTML"){s(e,t,r);return Array.from(t.childNodes)}else{return l(e,t,r)}})});i.pantry.remove();return o}function l(e,t,n){const r=f(t);s(e,r,n,t,t.nextSibling);return Array.from(r.childNodes)}function a(e,t){if(!e.config.restoreFocus)return t();let n=document.activeElement;if(!(n instanceof HTMLInputElement||n instanceof HTMLTextAreaElement)){return t()}const{id:r,selectionStart:i,selectionEnd:o}=n;const l=t();if(r&&r!==document.activeElement?.getAttribute("id")){n=e.target.querySelector(`[id="${r}"]`);n?.focus()}if(n&&!n.selectionEnd&&o){n.setSelectionRange(i,o)}return l}const s=function(){function e(e,t,n,r=null,i=null){if(t instanceof HTMLTemplateElement&&n instanceof HTMLTemplateElement){t=t.content;n=n.content}r||=t.firstChild;for(const o of n.childNodes){if(r&&r!=i){const a=f(e,o,r,i);if(a){if(a!==r){h(e,r,a)}b(a,o,e);r=a.nextSibling;continue}}if(o instanceof Element){const s=o.getAttribute("id");if(e.persistentIds.has(s)){const u=p(t,s,r,e);b(u,o,e);r=u.nextSibling;continue}}const l=d(t,o,r,e);if(l){r=l.nextSibling}}while(r&&r!=i){const c=r;r=r.nextSibling;m(e,c)}}function d(e,t,n,r){if(r.callbacks.beforeNodeAdded(t)===false)return null;if(r.idMap.has(t)){const i=document.createElement(t.tagName);e.insertBefore(i,n);b(i,t,r);r.callbacks.afterNodeAdded(i);return i}else{const o=document.importNode(t,true);e.insertBefore(o,n);r.callbacks.afterNodeAdded(o);return o}}const f=function(){function e(e,t,n,r){let i=null;let o=t.nextSibling;let l=0;let a=n;while(a&&a!=r){if(u(a,t)){if(s(e,a,t)){return a}if(i===null){if(!e.idMap.has(a)){i=a}}}if(i===null&&o&&u(a,o)){l++;o=o.nextSibling;if(l>=2){i=undefined}}if(e.activeElementAndParents.includes(a))break;a=a.nextSibling}return i||null}function s(e,t,n){let r=e.idMap.get(t);let i=e.idMap.get(n);if(!i||!r)return false;for(const o of r){if(i.has(o)){return true}}return false}function u(e,t){const n=e;const r=t;return n.nodeType===r.nodeType&&n.tagName===r.tagName&&(!n.getAttribute?.("id")||n.getAttribute?.("id")===r.getAttribute?.("id"))}return e}();function m(e,t){if(e.idMap.has(t)){l(e.pantry,t,null)}else{if(e.callbacks.beforeNodeRemoved(t)===false)return;t.parentNode?.removeChild(t);e.callbacks.afterNodeRemoved(t)}}function h(t,e,n){let r=e;while(r&&r!==n){let e=r;r=r.nextSibling;m(t,e)}return r}function p(e,t,n,r){const i=r.target.getAttribute?.("id")===t&&r.target||r.target.querySelector(`[id="${t}"]`)||r.pantry.querySelector(`[id="${t}"]`);o(i,r);l(e,i,n);return i}function o(t,n){const r=t.getAttribute("id");while(t=t.parentNode){let e=n.idMap.get(t);if(e){e.delete(r);if(!e.size){n.idMap.delete(t)}}}}function l(t,n,r){if(t.moveBefore){try{t.moveBefore(n,r)}catch(e){t.insertBefore(n,r)}}else{t.insertBefore(n,r)}}return e}();const b=function(){function e(e,t,n){if(n.ignoreActive&&e===document.activeElement){return null}if(n.callbacks.beforeNodeMorphed(e,t)===false){return e}if(e instanceof HTMLHeadElement&&n.head.ignore){}else if(e instanceof HTMLHeadElement&&n.head.style!=="morph"){m(e,t,n)}else{r(e,t,n);if(!f(e,n)){s(n,e,t)}}n.callbacks.afterNodeMorphed(e,t);return e}function r(e,t,n){let r=t.nodeType;if(r===1){const i=e;const o=t;const l=i.attributes;const a=o.attributes;for(const s of a){if(d(s.name,i,"update",n)){continue}if(i.getAttribute(s.name)!==s.value){i.setAttribute(s.name,s.value)}}for(let e=l.length-1;0<=e;e--){const u=l[e];if(!u)continue;if(!o.hasAttribute(u.name)){if(d(u.name,i,"remove",n)){continue}i.removeAttribute(u.name)}}if(!f(i,n)){c(i,o,n)}}if(r===8||r===3){if(e.nodeValue!==t.nodeValue){e.nodeValue=t.nodeValue}}}function c(n,r,i){if(n instanceof HTMLInputElement&&r instanceof HTMLInputElement&&r.type!=="file"){let e=r.value;let t=n.value;o(n,r,"checked",i);o(n,r,"disabled",i);if(!r.hasAttribute("value")){if(!d("value",n,"remove",i)){n.value="";n.removeAttribute("value")}}else if(t!==e){if(!d("value",n,"update",i)){n.setAttribute("value",e);n.value=e}}}else if(n instanceof HTMLOptionElement&&r instanceof HTMLOptionElement){o(n,r,"selected",i)}else if(n instanceof HTMLTextAreaElement&&r instanceof HTMLTextAreaElement){let e=r.value;let t=n.value;if(d("value",n,"update",i)){return}if(e!==t){n.value=e}if(n.firstChild&&n.firstChild.nodeValue!==e){n.firstChild.nodeValue=e}}}function o(e,t,n,r){const i=t[n],o=e[n];if(i!==o){const l=d(n,e,"update",r);if(!l){e[n]=t[n]}if(i){if(!l){e.setAttribute(n,"")}}else{if(!d(n,e,"remove",r)){e.removeAttribute(n)}}}}function d(e,t,n,r){if(e==="value"&&r.ignoreActiveValue&&t===document.activeElement){return true}return r.callbacks.beforeAttributeUpdated(e,t,n)===false}function f(e,t){return!!t.ignoreActiveValue&&e===document.activeElement&&e!==document.body}return e}();function u(t,e,n,r){if(t.head.block){const i=e.querySelector("head");const o=n.querySelector("head");if(i&&o){const l=m(i,o,t);return Promise.all(l).then(()=>{const e=Object.assign(t,{head:{block:false,ignore:true}});return r(e)})}}return r(t)}function m(e,t,r){let i=[];let o=[];let l=[];let a=[];let s=new Map;for(const n of t.children){s.set(n.outerHTML,n)}for(const c of e.children){let e=s.has(c.outerHTML);let t=r.head.shouldReAppend(c);let n=r.head.shouldPreserve(c);if(e||n){if(t){o.push(c)}else{s.delete(c.outerHTML);l.push(c)}}else{if(r.head.style==="append"){if(t){o.push(c);a.push(c)}}else{if(r.head.shouldRemove(c)!==false){o.push(c)}}}}a.push(...s.values());let u=[];for(const d of a){let n=document.createRange().createContextualFragment(d.outerHTML).firstChild;if(r.callbacks.beforeNodeAdded(n)!==false){if("href"in n&&n.href||"src"in n&&n.src){let t;let e=new Promise(function(e){t=e});n.addEventListener("load",function(){t()});u.push(e)}e.appendChild(n);r.callbacks.afterNodeAdded(n);i.push(n)}}for(const f of o){if(r.callbacks.beforeNodeRemoved(f)!==false){e.removeChild(f);r.callbacks.afterNodeRemoved(f)}}r.head.afterHeadMorphed(e,{added:i,kept:l,removed:o});return u}const c=function(){function e(e,t,n){const{persistentIds:r,idMap:i}=f(e,t);const o=a(n);const l=o.morphStyle||"outerHTML";if(!["innerHTML","outerHTML"].includes(l)){throw`Do not understand how to morph style ${l}`}return{target:e,newContent:t,config:o,morphStyle:l,ignoreActive:o.ignoreActive,ignoreActiveValue:o.ignoreActiveValue,restoreFocus:o.restoreFocus,idMap:i,persistentIds:r,pantry:s(),activeElementAndParents:u(e),callbacks:o.callbacks,head:o.head}}function a(e){let t=Object.assign({},n);Object.assign(t,e);t.callbacks=Object.assign({},n.callbacks,e.callbacks);t.head=Object.assign({},n.head,e.head);return t}function s(){const e=document.createElement("div");e.hidden=true;document.body.insertAdjacentElement("afterend",e);return e}function u(e){let t=[];let n=document.activeElement;if(n?.tagName!=="BODY"&&e.contains(n)){while(n){t.push(n);if(n===e)break;n=n.parentElement}}return t}function c(e){let t=Array.from(e.querySelectorAll("[id]"));if(e.getAttribute?.("id")){t.push(e)}return t}function d(n,e,r,t){for(const i of t){const o=i.getAttribute("id");if(e.has(o)){let t=i;while(t){let e=n.get(t);if(e==null){e=new Set;n.set(t,e)}e.add(o);if(t===r)break;t=t.parentElement}}}}function f(e,t){const n=c(e);const r=c(t);const i=m(n,r);let o=new Map;d(o,i,e,n);const l=t.__idiomorphRoot||t;d(o,i,l,r);return{persistentIds:i,idMap:o}}function m(e,t){let n=new Set;let r=new Map;for(const{id:o,tagName:l}of e){if(r.has(o)){n.add(o)}else{r.set(o,l)}}let i=new Set;for(const{id:o,tagName:l}of t){if(i.has(o)){n.add(o)}else if(r.get(o)===l){i.add(o)}}for(const o of n){i.delete(o)}return i}return e}();const{normalizeElement:d,normalizeParent:f}=function(){const i=new WeakSet;function e(e){if(e instanceof Document){return e.documentElement}else{return e}}function r(e){if(e==null){return document.createElement("div")}else if(typeof e==="string"){return r(l(e))}else if(i.has(e)){return e}else if(e instanceof Node){if(e.parentNode){return new o(e)}else{const t=document.createElement("div");t.append(e);return t}}else{const t=document.createElement("div");for(const n of[...e]){t.append(n)}return t}}class o{constructor(e){this.originalNode=e;this.realParentNode=e.parentNode;this.previousSibling=e.previousSibling;this.nextSibling=e.nextSibling}get childNodes(){const e=[];let t=this.previousSibling?this.previousSibling.nextSibling:this.realParentNode.firstChild;while(t&&t!=this.nextSibling){e.push(t);t=t.nextSibling}return e}querySelectorAll(r){return this.childNodes.reduce((t,e)=>{if(e instanceof Element){if(e.matches(r))t.push(e);const n=e.querySelectorAll(r);for(let e=0;e<n.length;e++){t.push(n[e])}}return t},[])}insertBefore(e,t){return this.realParentNode.insertBefore(e,t)}moveBefore(e,t){return this.realParentNode.moveBefore(e,t)}get __idiomorphRoot(){return this.originalNode}}function l(n){let r=new DOMParser;let e=n.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim,"");if(e.match(/<\/html>/)||e.match(/<\/head>/)||e.match(/<\/body>/)){let t=r.parseFromString(n,"text/html");if(e.match(/<\/html>/)){i.add(t);return t}else{let e=t.firstChild;if(e){i.add(e)}return e}}else{let e=r.parseFromString("<body><template>"+n+"</template></body>","text/html");let t=e.body.querySelector("template").content;i.add(t);return t}}return{normalizeElement:e,normalizeParent:r}}();return{morph:t,defaults:n}}();(function(){function i(e){if(e==="morph"||e==="morph:outerHTML"){return{morphStyle:"outerHTML"}}else if(e==="morph:innerHTML"){return{morphStyle:"innerHTML"}}else if(e.startsWith("morph:")){return Function("return ("+e.slice(6)+")")()}}htmx.defineExtension("morph",{isInlineSwap:function(e){let t=i(e);return t?.morphStyle==="outerHTML"||t?.morphStyle==null},handleSwap:function(e,t,n){let r=i(e);if(r){return Idiomorph.morph(t,n.children,r)}}})})();
@@ -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 Destroy, DispatchDOMEvent, Focus, HtmxComponent, Open, Redirect
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 REGISTRY, Destroy, DispatchDOMEvent, Focus, Open, Redirect, Triggers
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
- # Import htmx module and all its submodules recursively
145
- _import_modules_recursively(f"{app_config.name}.htmx")
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