hyperflask 0.2.2__tar.gz → 0.2.4__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.2.2 → hyperflask-0.2.4}/PKG-INFO +1 -1
- {hyperflask-0.2.2 → hyperflask-0.2.4}/pyproject.toml +1 -1
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/app.py +9 -4
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/layouts/email.mjml +3 -3
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/layouts/web.html +1 -4
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/page_helpers.py +3 -11
- hyperflask-0.2.4/src/hyperflask/static/app.js +14 -0
- hyperflask-0.2.4/src/hyperflask/static/htmx-exts.js +107 -0
- hyperflask-0.2.4/src/hyperflask/ui/flash.html +17 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/forms.html +1 -1
- hyperflask-0.2.4/src/hyperflask/ui/sse.html +14 -0
- hyperflask-0.2.4/src/hyperflask/utils/htmx.py +64 -0
- hyperflask-0.2.2/src/hyperflask/reactivity.py +0 -52
- hyperflask-0.2.2/src/hyperflask/static/app.js +0 -47
- hyperflask-0.2.2/src/hyperflask/static/reactive.js +0 -39
- hyperflask-0.2.2/src/hyperflask/ui/flash.html +0 -8
- hyperflask-0.2.2/src/hyperflask/ui/sse.html +0 -6
- hyperflask-0.2.2/src/hyperflask/utils/htmx.py +0 -27
- hyperflask-0.2.2/src/hyperflask/utils/s3.py +0 -4
- {hyperflask-0.2.2 → hyperflask-0.2.4}/README.md +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/__init__.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/__main__.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/cli.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/components/__init__.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/components/alpine.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/components/core.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/components/jsx.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/components/webcomponent.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/factory.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/forms/__init__.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/forms/fields.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/forms/widgets.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/model.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/static/alpine.js +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/static/app.css +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/static/react.js +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/static/stimulus.js +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/static/translations.js +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Alert.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Badge.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Button.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Card.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Divider.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Filter.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Footer.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Input.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Join.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Label.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Link.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/List.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Loading.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Menu.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Modal.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Progress.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/README.md +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Radio.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Range.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Rating.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Select.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Stack.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Stat.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Stats.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Step.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Steps.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Swap.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Tab.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Table.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Toast.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/icons.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/layout.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/ui/modal.html +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/utils/__init__.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/utils/html.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/utils/markdown.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/utils/models.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/utils/page_actions.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/utils/request.py +0 -0
- {hyperflask-0.2.2 → hyperflask-0.2.4}/src/hyperflask/utils/tokens.py +0 -0
|
@@ -28,7 +28,7 @@ from flask_debugtoolbar import DebugToolbarExtension
|
|
|
28
28
|
# from hyperflask
|
|
29
29
|
from .forms import Form
|
|
30
30
|
from .utils.page_actions import page_action_url
|
|
31
|
-
from .utils.htmx import htmx_oob
|
|
31
|
+
from .utils.htmx import htmx_oob, respond_with_flash_messages
|
|
32
32
|
from .utils.markdown import jinja_markdown, MarkdownExtension
|
|
33
33
|
from .utils.html import sanitize_html, nl2br
|
|
34
34
|
from .utils.models import File as SQLFileType
|
|
@@ -69,6 +69,8 @@ class Hyperflask(Flask):
|
|
|
69
69
|
"OTEL_INSTRUMENT": instrument,
|
|
70
70
|
"DRAMATIQ_BROKER": "dramatiq.brokers.redis:RedisBroker",
|
|
71
71
|
"DEBUG_TB_INTERCEPT_REDIRECTS": False,
|
|
72
|
+
"SESSION_COOKIE_HTTPONLY": True,
|
|
73
|
+
"SESSION_COOKIE_SAMESITE": "Lax",
|
|
72
74
|
# Hyperflask specific
|
|
73
75
|
"LAYOUT": "layouts/default.html",
|
|
74
76
|
"STATIC_MODE": static_mode,
|
|
@@ -76,7 +78,9 @@ class Hyperflask(Flask):
|
|
|
76
78
|
"HTMX_EXT": ["hf-modal"],
|
|
77
79
|
"HTMX_BOOST_SITE": False,
|
|
78
80
|
"MARKDOWN_OPTIONS": {"extensions": ["fenced_code", "nl2br", "attr_list", "admonition", "codehilite"]},
|
|
79
|
-
"MARKDOWN_SANITIZER_CONFIG": {}
|
|
81
|
+
"MARKDOWN_SANITIZER_CONFIG": {},
|
|
82
|
+
"FLASH_TOAST_OOB": True,
|
|
83
|
+
"FLASH_TOAST_REMOVE_AFTER": None
|
|
80
84
|
})
|
|
81
85
|
if config:
|
|
82
86
|
self.config.update(config)
|
|
@@ -95,6 +99,7 @@ class Hyperflask(Flask):
|
|
|
95
99
|
self.jinja_env.add_extension(WtformExtension)
|
|
96
100
|
self.jinja_env.add_extension(MarkdownExtension)
|
|
97
101
|
self.jinja_env.filters.update(markdown=jinja_markdown, sanitize=sanitize_html, nl2br=nl2br)
|
|
102
|
+
self.jinja_env.globals.update(page_action_url=page_action_url, app=self)
|
|
98
103
|
self.jinja_env.form_base_cls = Form
|
|
99
104
|
self.jinja_env.default_layout = self.config["LAYOUT"]
|
|
100
105
|
self.forms = self.jinja_env.forms
|
|
@@ -148,7 +153,6 @@ class Hyperflask(Flask):
|
|
|
148
153
|
})
|
|
149
154
|
self.assets.bundle(
|
|
150
155
|
{"@hyperflask": ["app.js"],
|
|
151
|
-
"@hyperflask/reactive": ["reactive.js"],
|
|
152
156
|
"@hyperflask/alpine": ["alpine.js"]},
|
|
153
157
|
from_package="hyperflask",
|
|
154
158
|
assets_folder="static"
|
|
@@ -175,7 +179,8 @@ class Hyperflask(Flask):
|
|
|
175
179
|
for macro in self.macros.create_from_directory(os.path.join(self.root_path, forms_folder)):
|
|
176
180
|
self.forms.register(self.macros.resolve_template(macro), macro)
|
|
177
181
|
|
|
178
|
-
self.
|
|
182
|
+
if self.config["FLASH_TOAST_OOB"]:
|
|
183
|
+
self.after_request(respond_with_flash_messages)
|
|
179
184
|
|
|
180
185
|
self.collections.register_freezer_generator(self.freezer)
|
|
181
186
|
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
</mj-style>
|
|
22
22
|
{% endblock %}
|
|
23
23
|
</mj-head>
|
|
24
|
-
<mj-body {% block body_tags %}background-color="{{config.get("EMAIL_BACKGROUND_COLOR", "#
|
|
25
|
-
<mj-section full-width="full-width" background-color="{{config.get("EMAIL_BACKGROUND_ACCENT_COLOR", "#
|
|
24
|
+
<mj-body {% block body_tags %}background-color="{{config.get("EMAIL_BACKGROUND_COLOR", "#ebebebff")}}" width="600px"{% endblock %}>
|
|
25
|
+
<mj-section full-width="full-width" background-color="{{config.get("EMAIL_BACKGROUND_ACCENT_COLOR", "#d1d1d1ff")}}" padding-bottom="0">
|
|
26
26
|
<mj-column width="100%">
|
|
27
|
-
<mj-text color="#
|
|
27
|
+
<mj-text color="#474747ff" font-weight="bold" align="center" font-size="16px" letter-spacing="1px">
|
|
28
28
|
{% block brand %}
|
|
29
29
|
{{config.get("SITE_TITLE", "")}}
|
|
30
30
|
{% endblock %}
|
|
@@ -6,9 +6,6 @@
|
|
|
6
6
|
<meta charset="utf-8">
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
8
|
{% endblock %}
|
|
9
|
-
{% if config["PWA"] -%}
|
|
10
|
-
<link rel="manifest" href="{{url_for("pwa_manifest")}}" />
|
|
11
|
-
{%- endif %}
|
|
12
9
|
{% block page_title %}<title>{% if page_title is defined %}{{ page_title }}{% if config.get("SITE_TITLE") %} | {% endif %}{% endif %}{% if config.get("SITE_TITLE") %}{{config["SITE_TITLE"]}}{% endif %}</title>{% endblock %}
|
|
13
10
|
{% asset_tags %}
|
|
14
11
|
{% block head_assets %}{% endblock %}
|
|
@@ -25,7 +22,7 @@
|
|
|
25
22
|
{% block footer %}{% endblock %}
|
|
26
23
|
{% block flash_messages %}
|
|
27
24
|
{% autoescape false %} {# Jinja bug #}
|
|
28
|
-
<{
|
|
25
|
+
<{FlashMessagesToast}/>
|
|
29
26
|
{% endautoescape %}
|
|
30
27
|
{% endblock %}
|
|
31
28
|
{% block body_end %}{% endblock %}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from flask_file_routes import DEFAULT_HELPERS
|
|
2
|
-
from flask import
|
|
2
|
+
from flask import current_app
|
|
3
3
|
from .utils.page_actions import page_action
|
|
4
4
|
|
|
5
5
|
|
|
@@ -18,19 +18,11 @@ def form(page):
|
|
|
18
18
|
return form_helper
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def stream(page):
|
|
22
|
-
g.__page_stream__ = True
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
21
|
def action(page):
|
|
27
|
-
|
|
28
|
-
return page_action(func, name)
|
|
29
|
-
return decorator
|
|
22
|
+
return page_action
|
|
30
23
|
|
|
31
24
|
|
|
32
25
|
DEFAULT_HELPERS.update(csrf_protect=csrf_protect,
|
|
33
26
|
forms=forms,
|
|
34
27
|
form=form,
|
|
35
|
-
|
|
36
|
-
action=action)
|
|
28
|
+
action=action)
|
|
@@ -0,0 +1,14 @@
|
|
|
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'];
|
|
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
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
{% macro FlashMessages() -%}
|
|
3
|
+
{%- for category, message in get_flashed_messages(True) -%}
|
|
4
|
+
<{FlashMessage category=category}>{{message}}</{FlashMessage}>
|
|
5
|
+
{%- endfor -%}
|
|
6
|
+
{%- endmacro %}
|
|
7
|
+
|
|
8
|
+
{% macro FlashMessage(category="info") -%}
|
|
9
|
+
{%- set message = caller() -%}
|
|
10
|
+
<{Alert color=category class="flash-message" **kwargs }>{{message}}</{Alert}>
|
|
11
|
+
{%- endmacro %}
|
|
12
|
+
|
|
13
|
+
{% macro FlashMessagesToast() -%}
|
|
14
|
+
<{Toast id="flash-messages-toast" class="z-1000"}>
|
|
15
|
+
<{FlashMessages}/>
|
|
16
|
+
</{Toast}>
|
|
17
|
+
{%- endmacro %}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
{%- endmacro %}
|
|
26
26
|
|
|
27
27
|
{% macro FormField(field=None, label=None) -%}
|
|
28
|
-
{% if label
|
|
28
|
+
{% if label %}<{Label}>{{label}}</{Label}>{% elif field.label and label == None %}<{Label for=field.label.field_id}>{{field.label.text}}</{Label}>{% endif %}
|
|
29
29
|
{{field if field else caller()}}
|
|
30
30
|
{%- endmacro %}
|
|
31
31
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
{% macro MercureStream(topic=None, private=True, hx_swap="beforeend", type=None, auto_scroll=False) -%}
|
|
3
|
+
<div {% if topic %}hx-ext="sse" sse-connect="{{mercure_authentified_hub_url(topic) if private else mercure_hub_url(topic)}}"{% endif %}
|
|
4
|
+
sse-swap="{{mercure_topic(topic if topic and not type and app.extensions['mercure_sse'].type_is_topic else (type or 'message'))}}"
|
|
5
|
+
hx-swap="{{hx_swap}}" {% if auto_scroll %}hx-bottom-scroll-on-trigger="htmx:sseMessage"{% endif %} {{html_attrs(**kwargs)}}>
|
|
6
|
+
{{ caller() }}
|
|
7
|
+
</div>
|
|
8
|
+
{%- endmacro %}
|
|
9
|
+
|
|
10
|
+
{% macro MercureStreamContext(topics, private=True) -%}
|
|
11
|
+
<div hx-ext="sse" sse-connect="{{mercure_authentified_hub_url(topics) if private else mercure_hub_url(topic)}}" {{html_attrs(**kwargs)}}>
|
|
12
|
+
{{ caller() }}
|
|
13
|
+
</div>
|
|
14
|
+
{%- endmacro %}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from htmx_flask import request
|
|
2
|
+
from htmx_flask import make_response
|
|
3
|
+
from markupsafe import Markup
|
|
4
|
+
from flask import get_flashed_messages, current_app, json
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def htmx_redirect(url):
|
|
9
|
+
return make_response(redirect=url)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def htmx_oob(html, swap="outerHTML", target=None, wrap=True):
|
|
13
|
+
if not request.htmx:
|
|
14
|
+
return ""
|
|
15
|
+
if callable(html):
|
|
16
|
+
html = html()
|
|
17
|
+
swap_oob = f"{swap}:{target}" if target else swap
|
|
18
|
+
html = str(html).strip()
|
|
19
|
+
m = re.match(r'<([a-z\-0-9]+)', html, re.I)
|
|
20
|
+
if swap != "outerHTML" and wrap:
|
|
21
|
+
wrap_tag = wrap if wrap is not True else {"tr": "table", "li": "ul"}.get(m.group(1).lower() if m else None, "div")
|
|
22
|
+
html = f"<{wrap_tag} hx-swap-oob=\"{swap_oob}\">{html}</{wrap_tag}>"
|
|
23
|
+
elif m:
|
|
24
|
+
tag, html = html.split(" ", 1)
|
|
25
|
+
html = f"{tag} hx-swap-oob=\"{swap_oob}\" {html}"
|
|
26
|
+
if wrap and tag[1:].lower() in ("tr", "td", "th", "thead", "tbody", "tfoot", "colgroup", "caption", "col"):
|
|
27
|
+
html = f"<template>{html}</template>"
|
|
28
|
+
return Markup(html)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def respond_with_htmx_oob(response, html, **kwargs):
|
|
32
|
+
response.set_data(htmx_oob(html, **kwargs) + response.get_data(as_text=True))
|
|
33
|
+
return response
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def add_hx_trigger(response, event, data=None):
|
|
37
|
+
if "HX-Trigger" not in response.headers:
|
|
38
|
+
trigger = json.dumps({event: data}) if data is not None else event
|
|
39
|
+
response.headers["HX-Trigger"] = trigger
|
|
40
|
+
return response
|
|
41
|
+
|
|
42
|
+
trigger = response.headers["HX-Trigger"]
|
|
43
|
+
if trigger.startswith("{"):
|
|
44
|
+
trigger = json.loads(trigger)
|
|
45
|
+
else:
|
|
46
|
+
trigger = {trigger: None}
|
|
47
|
+
trigger[event] = data
|
|
48
|
+
response.headers["HX-Trigger"] = trigger
|
|
49
|
+
return response
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def respond_with_flash_messages(response, only_if_htmx=True, target="#flash-messages-toast"):
|
|
53
|
+
if only_if_htmx and not request.htmx:
|
|
54
|
+
return response
|
|
55
|
+
|
|
56
|
+
messages = []
|
|
57
|
+
for category, message in get_flashed_messages(with_categories=True):
|
|
58
|
+
props = {"category": category, "caller": lambda: message}
|
|
59
|
+
if current_app.config["FLASH_TOAST_REMOVE_AFTER"]:
|
|
60
|
+
props["remove-me"] = current_app.config["FLASH_TOAST_REMOVE_AFTER"]
|
|
61
|
+
messages.append(htmx_oob(current_app.macros.FlashMessage(**props), swap="beforestart", target=target))
|
|
62
|
+
|
|
63
|
+
response.set_data("\n".join(messages) + response.get_data(as_text=True))
|
|
64
|
+
return response
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
from jinja2.ext import Extension
|
|
2
|
-
from jinja_super_macros import html_tag
|
|
3
|
-
from flask import current_app
|
|
4
|
-
from flask_mercure_sse import mercure_hub_url
|
|
5
|
-
from markupsafe import Markup
|
|
6
|
-
from collections import namedtuple
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
ReactiveInfo = namedtuple("ReactiveInfo", ["topic", "events", "render", "html_attrs", "tagname"], defaults=[None, None, None, "hf-reactive"])
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def reactive(obj, attr=None):
|
|
13
|
-
if attr and hasattr(getattr(obj, attr), "__reactive__"):
|
|
14
|
-
obj = getattr(obj, attr)
|
|
15
|
-
attr = None
|
|
16
|
-
elif not hasattr(obj, "__reactive__"):
|
|
17
|
-
return getattr(obj, attr) if attr else obj
|
|
18
|
-
info = ReactiveInfo(*obj.__reactive__(attr))
|
|
19
|
-
return render_reactive(info, getattr(obj, attr) if attr else obj)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def render_reactive(info, default_value=""):
|
|
23
|
-
html_attrs = info.html_attrs or {}
|
|
24
|
-
html_attrs.setdefault("mercure-url", mercure_hub_url())
|
|
25
|
-
if info.topic:
|
|
26
|
-
html_attrs.setdefault("topic", info.topic)
|
|
27
|
-
if info.events:
|
|
28
|
-
html_attrs.setdefault("events", ",".join(info.events))
|
|
29
|
-
return html_tag(info.tagname, html_attrs) + (info.render() if info.render else default_value) + Markup(f"</{info.tagname}>")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class ReactiveExtension(Extension):
|
|
33
|
-
def __init__(self, environment):
|
|
34
|
-
super().__init__(environment)
|
|
35
|
-
environment.globals["reactive"] = reactive
|
|
36
|
-
|
|
37
|
-
def preprocess(self, source, name, filename=None):
|
|
38
|
-
pos = source.find("{!")
|
|
39
|
-
while pos >= 0:
|
|
40
|
-
end = source.find("!}", pos)
|
|
41
|
-
if end < 0:
|
|
42
|
-
raise Exception("Missing closing bracket for reactive")
|
|
43
|
-
value = source[pos + 2:end]
|
|
44
|
-
if "." in value:
|
|
45
|
-
obj, attr = value.rsplit(".", 1)
|
|
46
|
-
args = [obj, f'"{attr}"']
|
|
47
|
-
else:
|
|
48
|
-
args = [value]
|
|
49
|
-
replacement = "{{ reactive(" + ", ".join(args) + ") }}"
|
|
50
|
-
source = source[:pos] + replacement + source[end + 2:]
|
|
51
|
-
pos = source.find("{!", pos + len(replacement) + 2)
|
|
52
|
-
return source
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import htmx from "htmx.org";
|
|
2
|
-
import 'htmx-ext-sse';
|
|
3
|
-
import "./app.css";
|
|
4
|
-
|
|
5
|
-
(function() {
|
|
6
|
-
let triggerElement = false, enableTriggerElement;
|
|
7
|
-
htmx.defineExtension("hf-modal", {
|
|
8
|
-
getSelectors: function() {
|
|
9
|
-
return ['[hf-modal]'];
|
|
10
|
-
},
|
|
11
|
-
onEvent: function(name, evt) {
|
|
12
|
-
if (name === "htmx:beforeProcessNode" && evt.target.hasAttribute("hf-modal")) {
|
|
13
|
-
evt.target.setAttribute("hx-get", evt.target.getAttribute("hf-modal"));
|
|
14
|
-
evt.target.setAttribute("hx-target", "body");
|
|
15
|
-
evt.target.setAttribute("hx-swap", "beforeend");
|
|
16
|
-
} else if (name === "htmx:beforeRequest" && evt.target.hasAttribute("hf-modal")) {
|
|
17
|
-
triggerElement = evt.target;
|
|
18
|
-
enableTriggerElement = !evt.target.disabled;
|
|
19
|
-
evt.target.disabled = true;
|
|
20
|
-
} else if (name === "htmx:load" && triggerElement) {
|
|
21
|
-
if (evt.target.tagName === "DIALOG") {
|
|
22
|
-
evt.target.showModal();
|
|
23
|
-
const form = evt.target.querySelector("form[hf-modal-form]");
|
|
24
|
-
if (form) {
|
|
25
|
-
form.setAttribute("hx-target", "closest dialog");
|
|
26
|
-
form.setAttribute("hx-swap", "delete");
|
|
27
|
-
form.setAttribute("hx-disabled-elt", "this");
|
|
28
|
-
if (!form.hasAttribute("action")) {
|
|
29
|
-
form.setAttribute("action", triggerElement.getAttribute("hf-modal"));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (enableTriggerElement) {
|
|
34
|
-
triggerElement.disabled = false;
|
|
35
|
-
}
|
|
36
|
-
triggerElement = null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
})();
|
|
41
|
-
|
|
42
|
-
window.htmx = htmx;
|
|
43
|
-
export { htmx };
|
|
44
|
-
|
|
45
|
-
if (window.Alpine) {
|
|
46
|
-
window.Alpine.start()
|
|
47
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
class HyperflaskReactive extends HTMLElement {
|
|
3
|
-
connectedCallback() {
|
|
4
|
-
const swap = this.getAttribute('swap');
|
|
5
|
-
const prop = this.getAttribute('prop');
|
|
6
|
-
const escape = this.getAttribute('escape') !== null;
|
|
7
|
-
const url = new URL(this.getAttribute('mercure-url'));
|
|
8
|
-
url.searchParams.append('topic', this.getAttribute('topic'));
|
|
9
|
-
this.eventSource = new EventSource(url);
|
|
10
|
-
const listener = (event) => {
|
|
11
|
-
let data = event.data;
|
|
12
|
-
if (prop) {
|
|
13
|
-
data = JSON.parse(data)[prop];
|
|
14
|
-
}
|
|
15
|
-
if (swap && swap != 'innerHTML') {
|
|
16
|
-
this[escape ? 'insertAdjacentText' : 'insertAdjacentHTML'](swap, data);
|
|
17
|
-
} else {
|
|
18
|
-
this[escape ? 'innerText' : 'innerHTML'] = data;
|
|
19
|
-
window.htmx.process(this);
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
const events = this.getAttribute('events');
|
|
23
|
-
if (events) {
|
|
24
|
-
events.split(' ').forEach(event => {
|
|
25
|
-
this.eventSource.addEventListener(event, listener);
|
|
26
|
-
});
|
|
27
|
-
} else {
|
|
28
|
-
this.eventSource.onmessage = listener;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
disconnectedCallback() {
|
|
33
|
-
this.eventSource.close();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (!customElements.get('hf-reactive')) {
|
|
38
|
-
customElements.define('hf-reactive', HyperflaskReactive);
|
|
39
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
{% macro MercureStream(topic, private=True, hx_swap="beforeend", type="message") -%}
|
|
3
|
-
<div hx-ext="sse" sse-connect="{{mercure_authentified_hub_url(topic) if private else mercure_hub_url(topic)}}" sse-swap="{{type}}" hx-swap="{{hx_swap}}" {{html_attrs(**kwargs)}}>
|
|
4
|
-
{{ caller() }}
|
|
5
|
-
</div>
|
|
6
|
-
{%- endmacro %}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from htmx_flask import request
|
|
2
|
-
from htmx_flask import make_response
|
|
3
|
-
from markupsafe import Markup
|
|
4
|
-
import re
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def htmx_redirect(url):
|
|
8
|
-
return make_response(redirect=url)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def htmx_oob(html, swap="outerHTML", target=None, wrap=True):
|
|
12
|
-
if not request.htmx:
|
|
13
|
-
return ""
|
|
14
|
-
if callable(html):
|
|
15
|
-
html = html()
|
|
16
|
-
swap_oob = f"{swap}:{target}" if target else swap
|
|
17
|
-
html = str(html).strip()
|
|
18
|
-
m = re.match(r'<([a-z\-0-9]+)', html, re.I)
|
|
19
|
-
if swap != "outerHTML" and wrap:
|
|
20
|
-
wrap_tag = wrap if wrap is not True else {"tr": "table", "li": "ul"}.get(m.group(1).lower() if m else None, "div")
|
|
21
|
-
html = f"<{wrap_tag} hx-swap-oob=\"{swap_oob}\">{html}</{wrap_tag}>"
|
|
22
|
-
elif m:
|
|
23
|
-
tag, html = html.split(" ", 1)
|
|
24
|
-
html = f"{tag} hx-swap-oob=\"{swap_oob}\" {html}"
|
|
25
|
-
if wrap and tag[1:].lower() in ("tr", "td", "th", "thead", "tbody", "tfoot", "colgroup", "caption", "col"):
|
|
26
|
-
html = f"<template>{html}</template>"
|
|
27
|
-
return Markup(html)
|
|
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
|