hyperflask 0.2.3__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.
Files changed (121) hide show
  1. {hyperflask-0.2.3 → hyperflask-0.2.4}/PKG-INFO +1 -1
  2. {hyperflask-0.2.3 → hyperflask-0.2.4}/pyproject.toml +1 -1
  3. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/app.py +9 -4
  4. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/layouts/email.mjml +3 -3
  5. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/layouts/web.html +1 -4
  6. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/page_helpers.py +3 -11
  7. hyperflask-0.2.4/src/hyperflask/static/app.js +14 -0
  8. hyperflask-0.2.4/src/hyperflask/static/htmx-exts.js +107 -0
  9. hyperflask-0.2.4/src/hyperflask/ui/flash.html +17 -0
  10. hyperflask-0.2.4/src/hyperflask/ui/sse.html +14 -0
  11. hyperflask-0.2.4/src/hyperflask/utils/htmx.py +64 -0
  12. hyperflask-0.2.3/src/hyperflask/reactivity.py +0 -52
  13. hyperflask-0.2.3/src/hyperflask/static/app.js +0 -47
  14. hyperflask-0.2.3/src/hyperflask/static/reactive.js +0 -39
  15. hyperflask-0.2.3/src/hyperflask/ui/flash.html +0 -8
  16. hyperflask-0.2.3/src/hyperflask/ui/sse.html +0 -6
  17. hyperflask-0.2.3/src/hyperflask/utils/htmx.py +0 -27
  18. hyperflask-0.2.3/src/hyperflask/utils/s3.py +0 -4
  19. {hyperflask-0.2.3 → hyperflask-0.2.4}/README.md +0 -0
  20. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/__init__.py +0 -0
  21. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/__main__.py +0 -0
  22. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/cli.py +0 -0
  23. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/components/__init__.py +0 -0
  24. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/components/alpine.py +0 -0
  25. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/components/core.py +0 -0
  26. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/components/jsx.py +0 -0
  27. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/components/webcomponent.py +0 -0
  28. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/factory.py +0 -0
  29. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/forms/__init__.py +0 -0
  30. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/forms/fields.py +0 -0
  31. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/forms/widgets.py +0 -0
  32. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/model.py +0 -0
  33. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/static/alpine.js +0 -0
  34. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/static/app.css +0 -0
  35. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/static/react.js +0 -0
  36. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/static/stimulus.js +0 -0
  37. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/static/translations.js +0 -0
  38. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Accordion.html +0 -0
  39. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/AccordionPanel.html +0 -0
  40. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Alert.html +0 -0
  41. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Avatar.html +0 -0
  42. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/AvatarGroup.html +0 -0
  43. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Badge.html +0 -0
  44. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Breadcrumbs.html +0 -0
  45. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/BreadcrumbsItem.html +0 -0
  46. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Button.html +0 -0
  47. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Card.html +0 -0
  48. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/CardActions.html +0 -0
  49. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/CardTitle.html +0 -0
  50. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/ChatBubble.html +0 -0
  51. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Checkbox.html +0 -0
  52. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/CollapsibleSubMenu.html +0 -0
  53. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Countdown.html +0 -0
  54. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Divider.html +0 -0
  55. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Drawer.html +0 -0
  56. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/DrawerContent.html +0 -0
  57. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/DrawerSide.html +0 -0
  58. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Dropdown.html +0 -0
  59. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/DropdownItem.html +0 -0
  60. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Fieldset.html +0 -0
  61. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/FieldsetLegend.html +0 -0
  62. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/FileInput.html +0 -0
  63. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Filter.html +0 -0
  64. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/FilterItem.html +0 -0
  65. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Footer.html +0 -0
  66. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/FooterLink.html +0 -0
  67. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/FooterNav.html +0 -0
  68. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Indicator.html +0 -0
  69. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Input.html +0 -0
  70. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/InputLabel.html +0 -0
  71. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Join.html +0 -0
  72. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Kbd.html +0 -0
  73. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Label.html +0 -0
  74. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/LabeledCheckbox.html +0 -0
  75. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/LabeledToggle.html +0 -0
  76. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Link.html +0 -0
  77. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/List.html +0 -0
  78. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/ListItem.html +0 -0
  79. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Loading.html +0 -0
  80. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Menu.html +0 -0
  81. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/MenuItem.html +0 -0
  82. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/MenuTitle.html +0 -0
  83. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Modal.html +0 -0
  84. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/ModalAction.html +0 -0
  85. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/ModalTitle.html +0 -0
  86. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Navbar.html +0 -0
  87. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Pagination.html +0 -0
  88. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/PaginationNext.html +0 -0
  89. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/PaginationPage.html +0 -0
  90. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/PaginationPrev.html +0 -0
  91. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Progress.html +0 -0
  92. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/README.md +0 -0
  93. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Radio.html +0 -0
  94. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Range.html +0 -0
  95. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Rating.html +0 -0
  96. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Select.html +0 -0
  97. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Stack.html +0 -0
  98. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Stat.html +0 -0
  99. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Stats.html +0 -0
  100. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Step.html +0 -0
  101. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Steps.html +0 -0
  102. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/SubMenu.html +0 -0
  103. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Swap.html +0 -0
  104. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Tab.html +0 -0
  105. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Table.html +0 -0
  106. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Tabs.html +0 -0
  107. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Textarea.html +0 -0
  108. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Toast.html +0 -0
  109. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Toggle.html +0 -0
  110. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/daisyui/Tooltip.html +0 -0
  111. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/forms.html +0 -0
  112. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/icons.html +0 -0
  113. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/layout.html +0 -0
  114. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/ui/modal.html +0 -0
  115. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/utils/__init__.py +0 -0
  116. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/utils/html.py +0 -0
  117. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/utils/markdown.py +0 -0
  118. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/utils/models.py +0 -0
  119. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/utils/page_actions.py +0 -0
  120. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/utils/request.py +0 -0
  121. {hyperflask-0.2.3 → hyperflask-0.2.4}/src/hyperflask/utils/tokens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperflask
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Full stack web framework
5
5
  Author: Maxime Bouroumeau-Fuseau
6
6
  Author-email: Maxime Bouroumeau-Fuseau <maxime.bouroumeau@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperflask"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Full stack web framework"
5
5
  authors = [
6
6
  {"name" = "Maxime Bouroumeau-Fuseau", email = "maxime.bouroumeau@gmail.com"}
@@ -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.jinja_env.globals.update(page_action_url=page_action_url)
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", "#c8c8c8")}}" width="600px"{% endblock %}>
25
- <mj-section full-width="full-width" background-color="{{config.get("EMAIL_BACKGROUND_ACCENT_COLOR", "#a6a6a6")}}" padding-bottom="0">
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="#4d4d4d" font-weight="bold" align="center" font-size="16px" letter-spacing="1px">
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
- <{FlashMessages}/>
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 g, current_app
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
- def decorator(func=None, name=None):
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
- stream=stream,
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 %}
@@ -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,8 +0,0 @@
1
-
2
- {% macro FlashMessages() -%}
3
- <{Toast id="flash-messages"}>
4
- {% for category, message in get_flashed_messages(True) %}
5
- <{Alert color=category}>{{message}}</{Alert}>
6
- {% endfor %}
7
- </{Toast}>
8
- {%- endmacro %}
@@ -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)
@@ -1,4 +0,0 @@
1
-
2
-
3
- def create_upload_url():
4
- pass
File without changes