hyperflask 0.6.0__tar.gz → 0.7.1__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.
- {hyperflask-0.6.0 → hyperflask-0.7.1}/PKG-INFO +3 -3
- {hyperflask-0.6.0 → hyperflask-0.7.1}/README.md +1 -1
- {hyperflask-0.6.0 → hyperflask-0.7.1}/pyproject.toml +2 -5
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/app.py +9 -6
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/components/core.py +20 -2
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/layouts/web.html +2 -2
- hyperflask-0.7.1/src/hyperflask/static/app.js +11 -0
- hyperflask-0.7.1/src/hyperflask/static/htmx-exts.js +186 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/forms.html +3 -2
- hyperflask-0.7.1/src/hyperflask/ui/sse.html +24 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/utils/htmx.py +9 -9
- hyperflask-0.6.0/src/hyperflask/static/app.js +0 -14
- hyperflask-0.6.0/src/hyperflask/static/htmx-exts.js +0 -128
- hyperflask-0.6.0/src/hyperflask/ui/sse.html +0 -22
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/__init__.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/__main__.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/actors.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/cli/__init__.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/cli/reloader.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/cli/runner.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/cli/scaffold.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/cli/worker.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/components/__init__.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/components/alpine.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/components/jsx.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/components/webcomponent.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/factory.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/forms/__init__.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/forms/fields.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/forms/widgets.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/layouts/email.mjml +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/models.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/page_helpers.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/security.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/static/alpine-csp.js +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/static/alpine.js +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/static/app.css +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/static/react.js +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/static/stimulus.js +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/static/translations.js +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Alert.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Badge.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Button.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Card.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Divider.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Filter.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Footer.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Input.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Join.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Label.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Link.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/List.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Loading.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Menu.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Modal.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Progress.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/README.md +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Radio.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Range.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Rating.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Select.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Stack.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Stat.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Stats.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Step.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Steps.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Swap.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Tab.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Table.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Toast.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/flash.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/icons.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/layout.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/modal.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/ui/skeleton.html +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/utils/__init__.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/utils/collections.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/utils/freezer.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/utils/html.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/utils/image.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/utils/markdown.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/utils/metadata.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/utils/page_actions.py +0 -0
- {hyperflask-0.6.0 → hyperflask-0.7.1}/src/hyperflask/utils/tokens.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperflask
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Full stack web framework
|
|
5
5
|
Author: Maxime Bouroumeau-Fuseau
|
|
6
6
|
Author-email: Maxime Bouroumeau-Fuseau <maxime.bouroumeau@gmail.com>
|
|
@@ -28,7 +28,7 @@ Requires-Dist: html-sanitizer~=2.6
|
|
|
28
28
|
Requires-Dist: htmx-flask~=0.1
|
|
29
29
|
Requires-Dist: jinja-layout~=0.5
|
|
30
30
|
Requires-Dist: jinja-wtforms~=0.2
|
|
31
|
-
Requires-Dist: periodiq~=0.
|
|
31
|
+
Requires-Dist: periodiq~=0.14
|
|
32
32
|
Requires-Dist: python-slugify~=8.0.4
|
|
33
33
|
Requires-Dist: watchdog>=6.0.0
|
|
34
34
|
Requires-Dist: pillow>=11.3.0
|
|
@@ -114,6 +114,6 @@ A good part of these extensions is developed as part of the Hyperflask project.
|
|
|
114
114
|
1. Create your project directory: `mkdir example-project && cd example-project`
|
|
115
115
|
2. Create and activate a virtualenv: `python -m venv .venv && source .venv/bin/activate`
|
|
116
116
|
3. `pip install hyperflask`
|
|
117
|
-
5. Run `npm install esbuild tailwindcss @tailwindcss/cli htmx.org
|
|
117
|
+
5. Run `npm install esbuild tailwindcss @tailwindcss/cli htmx.org@4 bootstrap-icons daisyui`
|
|
118
118
|
4. Run `mkdir pages && echo "Hello world" > pages/index.jpy`
|
|
119
119
|
6. Start a development server using `hyperflask dev`
|
|
@@ -76,6 +76,6 @@ A good part of these extensions is developed as part of the Hyperflask project.
|
|
|
76
76
|
1. Create your project directory: `mkdir example-project && cd example-project`
|
|
77
77
|
2. Create and activate a virtualenv: `python -m venv .venv && source .venv/bin/activate`
|
|
78
78
|
3. `pip install hyperflask`
|
|
79
|
-
5. Run `npm install esbuild tailwindcss @tailwindcss/cli htmx.org
|
|
79
|
+
5. Run `npm install esbuild tailwindcss @tailwindcss/cli htmx.org@4 bootstrap-icons daisyui`
|
|
80
80
|
4. Run `mkdir pages && echo "Hello world" > pages/index.jpy`
|
|
81
81
|
6. Start a development server using `hyperflask dev`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hyperflask"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.1"
|
|
4
4
|
description = "Full stack web framework"
|
|
5
5
|
authors = [
|
|
6
6
|
{"name" = "Maxime Bouroumeau-Fuseau", email = "maxime.bouroumeau@gmail.com"}
|
|
@@ -33,15 +33,12 @@ dependencies = [
|
|
|
33
33
|
"htmx-flask ~=0.1",
|
|
34
34
|
"jinja-layout ~=0.5",
|
|
35
35
|
"jinja-wtforms ~=0.2",
|
|
36
|
-
"periodiq ~=0.
|
|
36
|
+
"periodiq ~=0.14",
|
|
37
37
|
"python-slugify ~=8.0.4",
|
|
38
38
|
"watchdog>=6.0.0",
|
|
39
39
|
"pillow>=11.3.0",
|
|
40
40
|
]
|
|
41
41
|
|
|
42
|
-
[tool.uv.sources]
|
|
43
|
-
periodiq = { git = "https://gitlab.com/bersace/periodiq" }
|
|
44
|
-
|
|
45
42
|
[project.urls]
|
|
46
43
|
Repository = "https://github.com/hyperflask/hyperflask"
|
|
47
44
|
|
|
@@ -16,7 +16,7 @@ from flask_files import Files
|
|
|
16
16
|
from flask_sqlorm import FlaskSQLORM
|
|
17
17
|
from flask_mercure_sse import MercureSSE
|
|
18
18
|
from flask_mailman_templates import MailTemplates
|
|
19
|
-
from flask_suspense import Suspense, render_template, suspense_before_render_template,
|
|
19
|
+
from flask_suspense import Suspense, render_template, suspense_before_render_template, suspense_after_render_macros
|
|
20
20
|
#from flask_observability import Observability
|
|
21
21
|
# 3rd party extensions
|
|
22
22
|
from flask_wtf.csrf import CSRFProtect
|
|
@@ -82,7 +82,7 @@ class Hyperflask(Flask):
|
|
|
82
82
|
"ALPINE": False,
|
|
83
83
|
"STATIC_MODE": static_mode,
|
|
84
84
|
"ASSETS_INCLUDE_ALPINE": False,
|
|
85
|
-
"
|
|
85
|
+
"HTMX_CONFIG": {},
|
|
86
86
|
"HTMX_BOOST_SITE": False,
|
|
87
87
|
"MARKDOWN_OPTIONS": {"extensions": ["fenced_code", "nl2br", "attr_list", "admonition", "codehilite"]},
|
|
88
88
|
"MARKDOWN_SANITIZER_CONFIG": {},
|
|
@@ -111,6 +111,8 @@ class Hyperflask(Flask):
|
|
|
111
111
|
self.config["PAGES_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
|
|
112
112
|
self.config["COLLECTIONS_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
|
|
113
113
|
self.config["MAIL_TEMPLATES_MARKDOWN_OPTIONS"] = self.config["MARKDOWN_OPTIONS"]
|
|
114
|
+
# error responses are not swapped (only out of band swaps are processed)
|
|
115
|
+
self.config["HTMX_CONFIG"] = {"noSwap": [204, 304, "4xx", "5xx"], **self.config["HTMX_CONFIG"]}
|
|
114
116
|
|
|
115
117
|
if self.config['SERVER_SECURED']:
|
|
116
118
|
self.config.update({
|
|
@@ -244,10 +246,11 @@ class Hyperflask(Flask):
|
|
|
244
246
|
def on_suspense_before_render_template(sender, **kwargs):
|
|
245
247
|
csp_nonce() # we will need a nonce but the first call will be after the template started rendering
|
|
246
248
|
|
|
247
|
-
@
|
|
248
|
-
def
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
@suspense_after_render_macros.connect_via(self, weak=False)
|
|
250
|
+
def on_suspense_after_render_macros(sender, **kwargs):
|
|
251
|
+
# process the content inserted by suspense macros during htmx requests
|
|
252
|
+
# (during full page loads, htmx is not loaded yet and processes the whole page once loaded)
|
|
253
|
+
return f'<script nonce="{csp_nonce()}">window.htmx?.process(document.body)</script>'
|
|
251
254
|
|
|
252
255
|
@self.errorhandler(404)
|
|
253
256
|
def not_found(error):
|
|
@@ -100,13 +100,31 @@ class ComponentAdapter(BaseComponentAdapter):
|
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
class PropsWrapper:
|
|
103
|
+
# Not a Mapping subclass on purpose: methods like items() or values() would shadow props
|
|
104
|
+
# with the same name since jinja tries getattr() first. keys() and __getitem__() are enough for **props.
|
|
103
105
|
def __init__(self, props):
|
|
104
106
|
self._props = props
|
|
105
107
|
|
|
106
108
|
def __getattr__(self, name):
|
|
107
|
-
if name in self._props:
|
|
109
|
+
if not name.startswith("_") and name in self._props:
|
|
108
110
|
return self._props[name]
|
|
109
|
-
|
|
111
|
+
# AttributeError lets jinja fall back to __getitem__ which makes missing props undefined
|
|
112
|
+
raise AttributeError(f"No such prop: {name}")
|
|
113
|
+
|
|
114
|
+
def __getitem__(self, name):
|
|
115
|
+
return self._props[name]
|
|
116
|
+
|
|
117
|
+
def __contains__(self, name):
|
|
118
|
+
return name in self._props
|
|
119
|
+
|
|
120
|
+
def __iter__(self):
|
|
121
|
+
return iter(self._props)
|
|
122
|
+
|
|
123
|
+
def __len__(self):
|
|
124
|
+
return len(self._props)
|
|
125
|
+
|
|
126
|
+
def keys(self):
|
|
127
|
+
return self._props.keys()
|
|
110
128
|
|
|
111
129
|
def get(self, name, default=None):
|
|
112
130
|
return self._props.get(name, default)
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
{% block head_metas %}
|
|
6
6
|
{{ metadata_tags(page.get('metadata', {})) }}
|
|
7
7
|
{% endblock %}
|
|
8
|
+
<meta name="htmx-config" content='{{ config["HTMX_CONFIG"]|tojson }}'>
|
|
8
9
|
{% asset_tags(nonce=csp_nonce) %}
|
|
9
10
|
{% block head_assets %}{% endblock %}
|
|
10
11
|
{%- endblock %}
|
|
11
12
|
</head>
|
|
12
13
|
{% block body_tag -%}
|
|
13
14
|
<body
|
|
14
|
-
{% if config["
|
|
15
|
-
{% if config["HTMX_BOOST_SITE"] %}hx-boost="true"{% endif %}
|
|
15
|
+
{% if config["HTMX_BOOST_SITE"] %}hx-boost:inherited="true"{% endif %}
|
|
16
16
|
{% block body_tag_attrs %}{% endblock %}
|
|
17
17
|
>
|
|
18
18
|
{%- endblock %}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import htmx from "htmx.org";
|
|
2
|
+
|
|
3
|
+
function queryAll(root, selector) {
|
|
4
|
+
const elts = [...root.querySelectorAll(selector)];
|
|
5
|
+
if (root.matches(selector)) {
|
|
6
|
+
elts.unshift(root);
|
|
7
|
+
}
|
|
8
|
+
return elts;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
htmx.registerExtension("hf-modal", {
|
|
12
|
+
htmx_before_process(root) {
|
|
13
|
+
for (const elt of queryAll(root, "[hf-modal]")) {
|
|
14
|
+
elt.setAttribute("hx-get", elt.getAttribute("hf-modal"));
|
|
15
|
+
elt.setAttribute("hx-target", "body");
|
|
16
|
+
elt.setAttribute("hx-swap", "beforeend");
|
|
17
|
+
if (!elt.hasAttribute("hx-disable")) {
|
|
18
|
+
elt.setAttribute("hx-disable", "this");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
htmx_after_settle(target, detail) {
|
|
23
|
+
const triggerElement = detail.task.sourceElement;
|
|
24
|
+
if (detail.task.type !== "main" || !triggerElement?.hasAttribute("hf-modal")) return;
|
|
25
|
+
for (const dialog of detail.newContent) {
|
|
26
|
+
if (dialog.tagName !== "DIALOG") continue;
|
|
27
|
+
dialog.showModal();
|
|
28
|
+
const form = dialog.querySelector("form[hf-modal-form]");
|
|
29
|
+
if (form) {
|
|
30
|
+
form.setAttribute("hx-target", "closest dialog");
|
|
31
|
+
form.setAttribute("hx-swap", "delete");
|
|
32
|
+
form.setAttribute("hx-disable", "this");
|
|
33
|
+
if (!form.hasAttribute("action")) {
|
|
34
|
+
form.setAttribute("action", triggerElement.getAttribute("hf-modal"));
|
|
35
|
+
}
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
form.querySelector("input:not([type=hidden])")?.focus();
|
|
38
|
+
}, 100);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
htmx.registerExtension("hx-bottom-scroll-on-trigger", {
|
|
45
|
+
htmx_after_process(root) {
|
|
46
|
+
for (const elt of queryAll(root, "[hx-bottom-scroll-on-trigger]")) {
|
|
47
|
+
if (elt._hfBottomScroll) continue;
|
|
48
|
+
elt._hfBottomScroll = true;
|
|
49
|
+
elt.scrollTop = elt.scrollHeight - elt.clientHeight;
|
|
50
|
+
elt.classList.add("at-bottom");
|
|
51
|
+
elt.addEventListener("scroll", () => {
|
|
52
|
+
if (elt.scrollTop < elt.scrollHeight - elt.clientHeight - 20) {
|
|
53
|
+
elt.classList.remove("at-bottom");
|
|
54
|
+
} else {
|
|
55
|
+
elt.classList.add("at-bottom");
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
elt.addEventListener(elt.getAttribute("hx-bottom-scroll-on-trigger"), () => {
|
|
59
|
+
if (elt.classList.contains("at-bottom")) {
|
|
60
|
+
elt.scrollTop = elt.scrollHeight - elt.clientHeight;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Port of the htmx 1 remove-me extension: removes elements after a delay (eg. remove-me="5s")
|
|
68
|
+
htmx.registerExtension("remove-me", {
|
|
69
|
+
htmx_after_process(root) {
|
|
70
|
+
for (const elt of queryAll(root, "[remove-me],[data-remove-me]")) {
|
|
71
|
+
if (elt._hfRemoveMe) continue;
|
|
72
|
+
elt._hfRemoveMe = true;
|
|
73
|
+
const timing = elt.getAttribute("remove-me") || elt.getAttribute("data-remove-me");
|
|
74
|
+
if (timing) {
|
|
75
|
+
setTimeout(() => elt.remove(), htmx.parseInterval(timing));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
(function () {
|
|
82
|
+
function ensureLinksAreActive() {
|
|
83
|
+
const nodes = document.querySelectorAll('a[hx-active-url], [hx-active-url] a');
|
|
84
|
+
nodes.forEach((a) => {
|
|
85
|
+
let url;
|
|
86
|
+
for (const attr of ['hx-push-url', 'hx-replace-url', 'href', 'hx-get']) {
|
|
87
|
+
if (a.hasAttribute(attr)) {
|
|
88
|
+
if (attr !== "href" && a.getAttribute(attr) === "true") {
|
|
89
|
+
url = new URL(a.getAttribute('hx-get'), window.location.origin);
|
|
90
|
+
} else {
|
|
91
|
+
url = new URL(a.getAttribute(attr), window.location.origin);
|
|
92
|
+
}
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!url) return;
|
|
97
|
+
const activeClass = a.closest("[hx-active-url]")?.getAttribute("hx-active-url");
|
|
98
|
+
if (!activeClass) return;
|
|
99
|
+
if (url.pathname === window.location.pathname && url.search === window.location.search) {
|
|
100
|
+
a.classList.add(activeClass);
|
|
101
|
+
} else {
|
|
102
|
+
a.classList.remove(activeClass);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
htmx.registerExtension("hx-active-url", {
|
|
108
|
+
init() {
|
|
109
|
+
document.addEventListener('DOMContentLoaded', ensureLinksAreActive);
|
|
110
|
+
},
|
|
111
|
+
htmx_before_process(root) {
|
|
112
|
+
for (const elt of queryAll(root, "[hx-href]")) {
|
|
113
|
+
elt.setAttribute('href', elt.getAttribute('hx-href'));
|
|
114
|
+
elt.setAttribute('hx-push-url', elt.getAttribute('hx-href'));
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
htmx_after_history_push: ensureLinksAreActive,
|
|
118
|
+
htmx_after_history_replace: ensureLinksAreActive
|
|
119
|
+
});
|
|
120
|
+
})();
|
|
121
|
+
|
|
122
|
+
(function () {
|
|
123
|
+
// Swaps named SSE events into elements, like the htmx 2 sse-swap attribute.
|
|
124
|
+
// The htmx 4 sse extension only swaps unnamed messages into the connection element
|
|
125
|
+
// and dispatches named events as DOM events.
|
|
126
|
+
const SSE_CONNECT_SELECTOR = "[hx-sse\\:connect],[data-hx-sse\\:connect]";
|
|
127
|
+
const SSE_SWAP_ATTR = "hx-sse:swap";
|
|
128
|
+
let api;
|
|
129
|
+
|
|
130
|
+
htmx.registerExtension("hx-sse-swap", {
|
|
131
|
+
init(internalAPI) {
|
|
132
|
+
api = internalAPI;
|
|
133
|
+
},
|
|
134
|
+
htmx_sse_before_message(elt, detail) {
|
|
135
|
+
const swapElts = queryAll(elt, "[hx-sse\\:swap]").filter((e) => e.closest(SSE_CONNECT_SELECTOR) === elt);
|
|
136
|
+
if (!swapElts.length) return;
|
|
137
|
+
// unnamed messages are treated as "message" events so that the sse extension does not swap them
|
|
138
|
+
const event = detail.message.event ||= "message";
|
|
139
|
+
for (const swapElt of swapElts) {
|
|
140
|
+
if (!swapElt.getAttribute(SSE_SWAP_ATTR).split(/\s*,\s*/).includes(event)) continue;
|
|
141
|
+
const ctx = api.createRequestContext(swapElt, new CustomEvent(event));
|
|
142
|
+
ctx.text = detail.message.data;
|
|
143
|
+
ctx.swap += " swapEmpty:false";
|
|
144
|
+
detail.waitUntil(htmx.swap(ctx).catch((error) => api.triggerHtmxEvent(swapElt, "htmx:error", { error })));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
})();
|
|
149
|
+
|
|
150
|
+
(function () {
|
|
151
|
+
async function streamInto(ctx, stream) {
|
|
152
|
+
const reader = stream.getReader();
|
|
153
|
+
const decoder = new TextDecoder();
|
|
154
|
+
let text = "";
|
|
155
|
+
while (true) {
|
|
156
|
+
const { done, value } = await reader.read();
|
|
157
|
+
if (done || ctx.hfStreamDone) break;
|
|
158
|
+
text += decoder.decode(value, { stream: true });
|
|
159
|
+
ctx.target.innerHTML = text;
|
|
160
|
+
}
|
|
161
|
+
reader.cancel();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Progressively renders suspense responses (streamed with the x-suspense header)
|
|
165
|
+
// while htmx waits for the full response to perform the actual swap
|
|
166
|
+
htmx.registerExtension("hx-stream", {
|
|
167
|
+
htmx_before_request(elt, detail) {
|
|
168
|
+
const ctx = detail.ctx;
|
|
169
|
+
const fetch = ctx.fetch;
|
|
170
|
+
ctx.fetch = async (url, init) => {
|
|
171
|
+
const response = await fetch(url, init);
|
|
172
|
+
if (response.headers.get("x-suspense") !== "1" || !response.body || !(ctx.target instanceof Element)) {
|
|
173
|
+
return response;
|
|
174
|
+
}
|
|
175
|
+
const [body, stream] = response.body.tee();
|
|
176
|
+
streamInto(ctx, stream);
|
|
177
|
+
const teed = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
178
|
+
Object.defineProperty(teed, "url", { value: response.url });
|
|
179
|
+
return teed;
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
htmx_before_swap(elt, detail) {
|
|
183
|
+
detail.ctx.hfStreamDone = true;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
})();
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
{% macro HxForm(form=None, action=None) -%}
|
|
13
13
|
{% set inner = caller() %}
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
{% set on_after_request = "ctx.response.status < 400 && event.target.reset();" + kwargs.pop("hx_on__after_request", "") + kwargs.pop("hx-on::after:request", "") %}
|
|
15
|
+
{% set _ = kwargs.update({"hx-on::after:request": on_after_request}) %}
|
|
16
|
+
<{Form form hx_boost="false" hx_post=(action or request.url) **kwargs}>
|
|
16
17
|
{{inner}}
|
|
17
18
|
</{Form}>
|
|
18
19
|
{%- endmacro %}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
|
|
2
|
+
{% macro MercureStream(topic=None, private=True, hx_swap="beforeend", type=None, auto_scroll=False, with_subscriptions=False) -%}
|
|
3
|
+
{% set sse_url = mercure_authentified_hub_url(topic, with_subscriptions=with_subscriptions) if private else mercure_hub_url(topic, with_subscriptions=with_subscriptions) %}
|
|
4
|
+
<div {% if topic %}hx-sse:connect="{{sse_url}}"{% endif %}
|
|
5
|
+
hx-sse:swap="{{mercure_topic(topic if topic and not type and app.extensions['mercure_sse'].type_is_topic else (type or 'message'))}}"
|
|
6
|
+
{% if auto_scroll %}hx-bottom-scroll-on-trigger="htmx:after:swap"{% endif %} {{html_attrs(hx_swap=hx_swap, class_="mercure-stream", **kwargs)}}>
|
|
7
|
+
{{ caller() }}
|
|
8
|
+
</div>
|
|
9
|
+
{%- endmacro %}
|
|
10
|
+
|
|
11
|
+
{% macro MercureStreamContext(topics, private=True, with_subscriptions=False) -%}
|
|
12
|
+
{% set sse_url = mercure_authentified_hub_url(topics, with_subscriptions=with_subscriptions) if private else mercure_hub_url(topics, with_subscriptions=with_subscriptions) %}
|
|
13
|
+
<div hx-sse:connect="{{sse_url}}" {{html_attrs(class_="mercure-stream-context", **kwargs)}}>
|
|
14
|
+
{{ caller() }}
|
|
15
|
+
</div>
|
|
16
|
+
{%- endmacro %}
|
|
17
|
+
|
|
18
|
+
{% macro MercurePresence(hx_get, topic=None) -%}
|
|
19
|
+
{#- the sse extension uses hx-trigger to open the connection so it needs its own element -#}
|
|
20
|
+
{% if topic %}<div hx-sse:connect="{{mercure_authentified_hub_url(mercure_subscription_topic(topic))}}" class="mercure-stream-context">{% endif %}
|
|
21
|
+
<div hx-trigger="Subscription from:'closest .mercure-stream-context'" {{html_attrs(hx_get=hx_get, class_="mercure-presence", **kwargs)}}>
|
|
22
|
+
</div>
|
|
23
|
+
{% if topic %}</div>{% endif %}
|
|
24
|
+
{%- endmacro %}
|
|
@@ -14,15 +14,12 @@ def htmx_oob(html, swap="outerHTML", target=None, wrap=True):
|
|
|
14
14
|
html = html()
|
|
15
15
|
swap_oob = f"{swap}:{target}" if target else swap
|
|
16
16
|
html = str(html).strip()
|
|
17
|
-
m = re.match(r'<([a-z\-0-9]+)', html, re.I)
|
|
18
17
|
if swap != "outerHTML" and wrap:
|
|
19
|
-
|
|
18
|
+
# htmx inserts the content of the wrapper (template content can hold any element, eg. table rows)
|
|
19
|
+
wrap_tag = wrap if wrap is not True else "template"
|
|
20
20
|
html = f"<{wrap_tag} hx-swap-oob=\"{swap_oob}\">{html}</{wrap_tag}>"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
html = f"{tag} hx-swap-oob=\"{swap_oob}\" {html}"
|
|
24
|
-
if wrap and tag[1:].lower() in ("tr", "td", "th", "thead", "tbody", "tfoot", "colgroup", "caption", "col"):
|
|
25
|
-
html = f"<template>{html}</template>"
|
|
21
|
+
else:
|
|
22
|
+
html = re.sub(r'^<([a-z\-0-9]+)', lambda m: f'<{m.group(1)} hx-swap-oob="{swap_oob}"', html, count=1, flags=re.I)
|
|
26
23
|
return Markup(html)
|
|
27
24
|
|
|
28
25
|
|
|
@@ -43,20 +40,23 @@ def add_hx_trigger(response, event, data=None):
|
|
|
43
40
|
else:
|
|
44
41
|
trigger = {trigger: None}
|
|
45
42
|
trigger[event] = data
|
|
46
|
-
response.headers["HX-Trigger"] = trigger
|
|
43
|
+
response.headers["HX-Trigger"] = json.dumps(trigger)
|
|
47
44
|
return response
|
|
48
45
|
|
|
49
46
|
|
|
50
47
|
def respond_with_flash_messages(response, only_if_htmx=True, target="#flash-messages-toast"):
|
|
51
48
|
if only_if_htmx and not request.htmx:
|
|
52
49
|
return response
|
|
50
|
+
if response.is_streamed:
|
|
51
|
+
# do not buffer streams (eg. sse connections made by htmx)
|
|
52
|
+
return response
|
|
53
53
|
|
|
54
54
|
messages = []
|
|
55
55
|
for category, message in get_flashed_messages(with_categories=True):
|
|
56
56
|
props = {"category": category, "caller": lambda: message}
|
|
57
57
|
if current_app.config["FLASH_TOAST_REMOVE_AFTER"]:
|
|
58
58
|
props["remove-me"] = current_app.config["FLASH_TOAST_REMOVE_AFTER"]
|
|
59
|
-
messages.append(htmx_oob(current_app.macros.FlashMessage(**props), swap="
|
|
59
|
+
messages.append(htmx_oob(current_app.macros.FlashMessage(**props), swap="afterbegin", target=target))
|
|
60
60
|
|
|
61
61
|
response.set_data("\n".join(messages) + response.get_data(as_text=True))
|
|
62
62
|
return response
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import htmx from "htmx.org";
|
|
2
|
-
import "htmx-ext-sse";
|
|
3
|
-
import "./htmx-exts";
|
|
4
|
-
import "./app.css";
|
|
5
|
-
|
|
6
|
-
const DEFAULT_HTMX_EXTS = ['hf-modal', 'hx-bottom-scroll-on-trigger', 'hx-active-url', 'hx-stream'];
|
|
7
|
-
document.body.setAttribute('hx-ext', [document.body.getAttribute('hx-ext'), ...DEFAULT_HTMX_EXTS].filter(Boolean).join(","))
|
|
8
|
-
|
|
9
|
-
window.htmx = htmx;
|
|
10
|
-
export { htmx };
|
|
11
|
-
|
|
12
|
-
if (window.Alpine) {
|
|
13
|
-
window.Alpine.start()
|
|
14
|
-
}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import htmx from "htmx.org";
|
|
2
|
-
|
|
3
|
-
(function () {
|
|
4
|
-
let triggerElement = false, enableTriggerElement;
|
|
5
|
-
htmx.defineExtension("hf-modal", {
|
|
6
|
-
getSelectors() {
|
|
7
|
-
return ['[hf-modal]'];
|
|
8
|
-
},
|
|
9
|
-
onEvent(name, evt) {
|
|
10
|
-
if (name === "htmx:beforeProcessNode" && evt.target.hasAttribute("hf-modal")) {
|
|
11
|
-
evt.target.setAttribute("hx-get", evt.target.getAttribute("hf-modal"));
|
|
12
|
-
evt.target.setAttribute("hx-target", "body");
|
|
13
|
-
evt.target.setAttribute("hx-swap", "beforeend");
|
|
14
|
-
} else if (name === "htmx:beforeRequest" && evt.target.hasAttribute("hf-modal")) {
|
|
15
|
-
triggerElement = evt.target;
|
|
16
|
-
enableTriggerElement = !evt.target.disabled;
|
|
17
|
-
evt.target.disabled = true;
|
|
18
|
-
} else if (name === "htmx:load" && triggerElement) {
|
|
19
|
-
if (evt.target.tagName === "DIALOG") {
|
|
20
|
-
evt.target.showModal();
|
|
21
|
-
const form = evt.target.querySelector("form[hf-modal-form]");
|
|
22
|
-
if (form) {
|
|
23
|
-
form.setAttribute("hx-target", "closest dialog");
|
|
24
|
-
form.setAttribute("hx-swap", "delete");
|
|
25
|
-
form.setAttribute("hx-disabled-elt", "this");
|
|
26
|
-
if (!form.hasAttribute("action")) {
|
|
27
|
-
form.setAttribute("action", triggerElement.getAttribute("hf-modal"));
|
|
28
|
-
}
|
|
29
|
-
setTimeout(() => {
|
|
30
|
-
form.querySelector("input:not([type=hidden])")?.focus();
|
|
31
|
-
}, 100);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
if (enableTriggerElement) {
|
|
35
|
-
triggerElement.disabled = false;
|
|
36
|
-
}
|
|
37
|
-
triggerElement = null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
})();
|
|
42
|
-
|
|
43
|
-
htmx.defineExtension("hx-bottom-scroll-on-trigger", {
|
|
44
|
-
getSelectors() {
|
|
45
|
-
return ['[hx-bottom-scroll-on-trigger]'];
|
|
46
|
-
},
|
|
47
|
-
onEvent(name, evt) {
|
|
48
|
-
if (name === "htmx:afterProcessNode" && evt.target.hasAttribute("hx-bottom-scroll-on-trigger")) {
|
|
49
|
-
const elt = evt.target;
|
|
50
|
-
elt.scrollTop = elt.scrollHeight - elt.clientHeight;
|
|
51
|
-
elt.classList.add("at-bottom");
|
|
52
|
-
elt.addEventListener("scroll", (e) => {
|
|
53
|
-
if (e.target.scrollTop < e.target.scrollHeight - e.target.clientHeight - 20) {
|
|
54
|
-
e.target.classList.remove("at-bottom");
|
|
55
|
-
} else {
|
|
56
|
-
e.target.classList.add("at-bottom");
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const eventName = evt.target.getAttribute("hx-bottom-scroll-on-trigger");
|
|
63
|
-
if (name === eventName && evt.target.classList.contains("at-bottom")) {
|
|
64
|
-
evt.target.scrollTop = evt.target.scrollHeight - evt.target.clientHeight;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
htmx.defineExtension("hx-active-url", {
|
|
70
|
-
init() {
|
|
71
|
-
function ensureLinksAreActive() {
|
|
72
|
-
const nodes = document.querySelectorAll('a[hx-active-url], [hx-active-url] a');
|
|
73
|
-
nodes.forEach((a) => {
|
|
74
|
-
let url;
|
|
75
|
-
for (const attr of ['hx-push-url', 'hx-replace-url', 'href', 'hx-get']) {
|
|
76
|
-
if (a.hasAttribute(attr)) {
|
|
77
|
-
if (attr !== "href" && a.getAttribute(attr) === "true") {
|
|
78
|
-
url = new URL(a.getAttribute('hx-get'), window.location.origin);
|
|
79
|
-
} else {
|
|
80
|
-
url = new URL(a.getAttribute(attr), window.location.origin);
|
|
81
|
-
}
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
if (!url) return;
|
|
86
|
-
const activeClass = a.closest("[hx-active-url]")?.getAttribute("hx-active-url");
|
|
87
|
-
if (!activeClass) return;
|
|
88
|
-
if (url.pathname === window.location.pathname && url.search === window.location.search) {
|
|
89
|
-
a.classList.add(activeClass);
|
|
90
|
-
} else {
|
|
91
|
-
a.classList.remove(activeClass);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
document.addEventListener('DOMContentLoaded', ensureLinksAreActive);
|
|
96
|
-
document.addEventListener("htmx:pushedIntoHistory", ensureLinksAreActive);
|
|
97
|
-
},
|
|
98
|
-
getSelectors() {
|
|
99
|
-
return ['[hx-href]'];
|
|
100
|
-
},
|
|
101
|
-
onEvent(name, evt) {
|
|
102
|
-
if (name === "htmx:beforeProcessNode" && evt.target.hasAttribute("hx-href")) {
|
|
103
|
-
evt.target.setAttribute('href', evt.target.getAttribute('hx-href'));
|
|
104
|
-
evt.target.setAttribute('hx-push-url', evt.target.getAttribute('hx-href'));
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
htmx.defineExtension("hx-stream", {
|
|
110
|
-
onEvent: function (name, evt) {
|
|
111
|
-
if (name === "htmx:beforeRequest") {
|
|
112
|
-
const target = evt.detail.target;
|
|
113
|
-
const xhr = evt.detail.xhr;
|
|
114
|
-
let stream = false;
|
|
115
|
-
xhr.addEventListener('readystatechange', function () {
|
|
116
|
-
if (xhr.readyState == xhr.HEADERS_RECEIVED && xhr.getResponseHeader('x-suspense') === '1') {
|
|
117
|
-
stream = true;
|
|
118
|
-
target.innerHTML = xhr.responseText;
|
|
119
|
-
} else if (xhr.readyState === xhr.LOADING && stream) {
|
|
120
|
-
target.innerHTML = xhr.responseText;
|
|
121
|
-
} else if (xhr.readyState === xhr.DONE) {
|
|
122
|
-
stream = false;
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
return true;
|
|
127
|
-
}
|
|
128
|
-
});
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
{% macro MercureStream(topic=None, private=True, hx_swap="beforeend", type=None, auto_scroll=False, with_subscriptions=False) -%}
|
|
3
|
-
{% set sse_url = mercure_authentified_hub_url(topic, with_subscriptions=with_subscriptions) if private else mercure_hub_url(topic, with_subscriptions=with_subscriptions) %}
|
|
4
|
-
<div {% if topic %}hx-ext="sse" sse-connect="{{sse_url}}"{% endif %}
|
|
5
|
-
sse-swap="{{mercure_topic(topic if topic and not type and app.extensions['mercure_sse'].type_is_topic else (type or 'message'))}}"
|
|
6
|
-
{% if auto_scroll %}hx-bottom-scroll-on-trigger="htmx:sseMessage"{% endif %} {{html_attrs(hx_swap=hx_swap, class_="mercure-stream", **kwargs)}}>
|
|
7
|
-
{{ caller() }}
|
|
8
|
-
</div>
|
|
9
|
-
{%- endmacro %}
|
|
10
|
-
|
|
11
|
-
{% macro MercureStreamContext(topics, private=True, with_subscriptions=False) -%}
|
|
12
|
-
{% set sse_url = mercure_authentified_hub_url(topics, with_subscriptions=with_subscriptions) if private else mercure_hub_url(topics, with_subscriptions=with_subscriptions) %}
|
|
13
|
-
<div hx-ext="sse" sse-connect="{{sse_url}}" {{html_attrs(class_="mercure-stream-context", **kwargs)}}>
|
|
14
|
-
{{ caller() }}
|
|
15
|
-
</div>
|
|
16
|
-
{%- endmacro %}
|
|
17
|
-
|
|
18
|
-
{% macro MercurePresence(hx_get, topic=None) -%}
|
|
19
|
-
<div {% if topic %}hx-ext="sse" sse-connect="{{mercure_authentified_hub_url(mercure_subscription_topic(topic))}}"{% endif %}
|
|
20
|
-
hx-trigger="sse:Subscription" {{html_attrs(hx_get=hx_get, class_="mercure-presence", **kwargs)}}>
|
|
21
|
-
</div>
|
|
22
|
-
{%- endmacro %}
|
|
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
|
|
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
|
|
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
|
|
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
|