python-fasthtml 0.0.11__tar.gz → 0.0.14__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 (30) hide show
  1. {python-fasthtml-0.0.11/python_fasthtml.egg-info → python-fasthtml-0.0.14}/PKG-INFO +2 -1
  2. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/__init__.py +1 -1
  3. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/_modidx.py +4 -0
  4. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/all.py +3 -0
  5. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/components.py +35 -8
  6. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/core.py +17 -5
  7. python-fasthtml-0.0.14/fasthtml/js.py +48 -0
  8. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/xtend.py +21 -1
  9. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14/python_fasthtml.egg-info}/PKG-INFO +2 -1
  10. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/python_fasthtml.egg-info/requires.txt +1 -0
  11. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/settings.ini +2 -2
  12. python-fasthtml-0.0.11/fasthtml/js.py +0 -29
  13. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/LICENSE +0 -0
  14. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/MANIFEST.in +0 -0
  15. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/README.md +0 -0
  16. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/authmw.py +0 -0
  17. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/cli.py +0 -0
  18. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/components.pyi +0 -0
  19. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/fastapp.py +0 -0
  20. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/live_reload.py +0 -0
  21. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/oauth.py +0 -0
  22. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/starlette.py +0 -0
  23. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/fasthtml/xtend.pyi +0 -0
  24. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/python_fasthtml.egg-info/SOURCES.txt +0 -0
  25. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/python_fasthtml.egg-info/dependency_links.txt +0 -0
  26. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/python_fasthtml.egg-info/entry_points.txt +0 -0
  27. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/python_fasthtml.egg-info/not-zip-safe +0 -0
  28. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/python_fasthtml.egg-info/top_level.txt +0 -0
  29. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/setup.cfg +0 -0
  30. {python-fasthtml-0.0.11 → python-fasthtml-0.0.14}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-fasthtml
3
- Version: 0.0.11
3
+ Version: 0.0.14
4
4
  Summary: The fastest way to create an HTML app
5
5
  Home-page: https://github.com/AnswerDotAI/fasthtml
6
6
  Author: Jeremy Howard
@@ -24,6 +24,7 @@ Requires-Dist: uvicorn[standard]
24
24
  Requires-Dist: httpx
25
25
  Requires-Dist: fastlite>=0.0.6
26
26
  Requires-Dist: python-multipart
27
+ Requires-Dist: beautifulsoup4
27
28
  Provides-Extra: dev
28
29
  Requires-Dist: ipython; extra == "dev"
29
30
  Requires-Dist: lxml; extra == "dev"
@@ -1,4 +1,4 @@
1
- __version__ = "0.0.11"
1
+ __version__ = "0.0.14"
2
2
  from .core import *
3
3
  from .authmw import *
4
4
  from .components import *
@@ -13,6 +13,7 @@ d = { 'settings': { 'branch': 'main',
13
13
  'fasthtml.components.fill_dataclass': ('components.html#fill_dataclass', 'fasthtml/components.py'),
14
14
  'fasthtml.components.fill_form': ('components.html#fill_form', 'fasthtml/components.py'),
15
15
  'fasthtml.components.find_inputs': ('components.html#find_inputs', 'fasthtml/components.py'),
16
+ 'fasthtml.components.html2xt': ('components.html#html2xt', 'fasthtml/components.py'),
16
17
  'fasthtml.components.show': ('components.html#show', 'fasthtml/components.py'),
17
18
  'fasthtml.components.xt_html': ('components.html#xt_html', 'fasthtml/components.py'),
18
19
  'fasthtml.components.xt_hx': ('components.html#xt_hx', 'fasthtml/components.py')},
@@ -43,5 +44,8 @@ d = { 'settings': { 'branch': 'main',
43
44
  'fasthtml.xtend.Group': ('xtend.html#group', 'fasthtml/xtend.py'),
44
45
  'fasthtml.xtend.Hidden': ('xtend.html#hidden', 'fasthtml/xtend.py'),
45
46
  'fasthtml.xtend.Html': ('xtend.html#html', 'fasthtml/xtend.py'),
47
+ 'fasthtml.xtend.Script': ('xtend.html#script', 'fasthtml/xtend.py'),
46
48
  'fasthtml.xtend.Search': ('xtend.html#search', 'fasthtml/xtend.py'),
49
+ 'fasthtml.xtend.Style': ('xtend.html#style', 'fasthtml/xtend.py'),
50
+ 'fasthtml.xtend.jsd': ('xtend.html#jsd', 'fasthtml/xtend.py'),
47
51
  'fasthtml.xtend.set_pico_cls': ('xtend.html#set_pico_cls', 'fasthtml/xtend.py')}}}
@@ -1,7 +1,10 @@
1
+ import uvicorn
2
+
1
3
  from .starlette import *
2
4
  from fastcore.utils import *
3
5
  from fastcore.xml import *
4
6
  from sqlite_minutils import Database
5
7
  from fastlite import *
6
8
  from . import *
9
+ from .js import *
7
10
 
@@ -1,18 +1,20 @@
1
1
  # AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/01_components.ipynb.
2
2
 
3
3
  # %% auto 0
4
- __all__ = ['named', 'html_attrs', 'hx_attrs', 'show', 'xt_html', 'xt_hx', 'fill_form', 'fill_dataclass', 'find_inputs', 'Html',
5
- 'Head', 'Title', 'Meta', 'Link', 'Style', 'Body', 'Pre', 'Code', 'Div', 'Span', 'P', 'H1', 'H2', 'H3', 'H4',
6
- 'H5', 'H6', 'Strong', 'Em', 'B', 'I', 'U', 'S', 'Strike', 'Sub', 'Sup', 'Hr', 'Br', 'Img', 'Nav', 'Ul', 'Ol',
7
- 'Li', 'Dl', 'Dt', 'Dd', 'Table', 'Thead', 'Tbody', 'Tfoot', 'Tr', 'Th', 'Td', 'Caption', 'Col', 'Colgroup',
8
- 'Form', 'Input', 'Textarea', 'Button', 'Select', 'Option', 'Label', 'Fieldset', 'Legend', 'Details',
9
- 'Dialog', 'Summary', 'Main', 'Header', 'Footer', 'Section', 'Article', 'Aside', 'Figure', 'Figcaption',
10
- 'Mark', 'Small', 'Iframe', 'Object', 'Embed', 'Param', 'Video', 'Audio', 'Source', 'Canvas', 'Svg', 'Math',
11
- 'Script', 'Noscript', 'Template', 'Slot']
4
+ __all__ = ['named', 'html_attrs', 'hx_attrs', 'show', 'xt_html', 'xt_hx', 'fill_form', 'fill_dataclass', 'find_inputs', 'html2xt',
5
+ 'Html', 'Head', 'Title', 'Meta', 'Link', 'Style', 'Body', 'Pre', 'Code', 'Div', 'Span', 'P', 'H1', 'H2',
6
+ 'H3', 'H4', 'H5', 'H6', 'Strong', 'Em', 'B', 'I', 'U', 'S', 'Strike', 'Sub', 'Sup', 'Hr', 'Br', 'Img', 'Nav',
7
+ 'Ul', 'Ol', 'Li', 'Dl', 'Dt', 'Dd', 'Table', 'Thead', 'Tbody', 'Tfoot', 'Tr', 'Th', 'Td', 'Caption', 'Col',
8
+ 'Colgroup', 'Form', 'Input', 'Textarea', 'Button', 'Select', 'Option', 'Label', 'Fieldset', 'Legend',
9
+ 'Details', 'Dialog', 'Summary', 'Main', 'Header', 'Footer', 'Section', 'Article', 'Aside', 'Figure',
10
+ 'Figcaption', 'Mark', 'Small', 'Iframe', 'Object', 'Embed', 'Param', 'Video', 'Audio', 'Source', 'Canvas',
11
+ 'Svg', 'Math', 'Script', 'Noscript', 'Template', 'Slot']
12
12
 
13
13
  # %% ../nbs/01_components.ipynb 2
14
14
  from dataclasses import dataclass, asdict, is_dataclass, make_dataclass, replace, astuple, MISSING
15
15
 
16
+ from bs4 import BeautifulSoup
17
+
16
18
  from fastcore.utils import *
17
19
  from fastcore.xml import *
18
20
  from fastcore.meta import use_kwargs, delegates
@@ -105,3 +107,28 @@ def __getattr__(tag):
105
107
  if tag.startswith('_') or tag[0].islower(): raise AttributeError
106
108
  def _f(*c, target_id=None, **kwargs): return xt_hx(tag, *c, target_id=target_id, **kwargs)
107
109
  return _f
110
+
111
+ # %% ../nbs/01_components.ipynb 22
112
+ def html2xt(html):
113
+ rev_map = {'class': 'cls', 'for': 'fr'}
114
+
115
+ def _parse(elm, lvl=0):
116
+ if isinstance(elm, str): return repr(elm.strip()) if elm.strip() else ''
117
+ if isinstance(elm, list): return '\n'.join(_parse(o, lvl) for o in elm)
118
+ tag_name = elm.name.capitalize()
119
+ if tag_name=='[document]': return _parse(list(elm.children), lvl)
120
+ cts = elm.contents
121
+ cs = [repr(c.strip()) if isinstance(c, str) else _parse(c, lvl+1)
122
+ for c in cts if str(c).strip()]
123
+ attrs = []
124
+ for key, value in elm.attrs.items():
125
+ if isinstance(value,(tuple,list)): value = " ".join(value)
126
+ attrs.append(f'{rev_map.get(key, key)}={value!r}')
127
+ spc = " "*lvl*2
128
+ onlychild = not cts or (len(cts)==1 and isinstance(cts[0],str))
129
+ j = ', ' if onlychild else f',\n{spc}'
130
+ inner = j.join(filter(None, cs+attrs))
131
+ if onlychild: return f'{tag_name}({inner})'
132
+ return f'{tag_name}(\n{spc}{inner}\n{" "*(lvl-1)*2})'
133
+
134
+ return _parse(BeautifulSoup(html.strip(), 'html.parser'), 1)
@@ -2,6 +2,7 @@ import json,dateutil,uuid,inspect
2
2
 
3
3
  from fastcore.utils import *
4
4
  from fastcore.xml import *
5
+ from fasthtml.xtend import *
5
6
 
6
7
  from types import UnionType, SimpleNamespace as ns
7
8
  from typing import Optional, get_type_hints, get_args, get_origin, Union, Mapping, TypedDict, List
@@ -13,6 +14,7 @@ from functools import wraps, partialmethod
13
14
 
14
15
  from .starlette import *
15
16
 
17
+ __all__ = "is_typeddict is_namedtuple date snake2hyphens htmx_hdrs HtmxHeaders str2int HttpHeader form2dict RouteX RouterX FastHTML htmx_hdrs reg_re_param MiddlewareBase Beforeware get_key".split()
16
18
 
17
19
  empty = Parameter.empty
18
20
 
@@ -124,7 +126,6 @@ async def _find_p(req, arg:str, p):
124
126
  if res is empty or res is None: res = req.headers.get(snake2hyphens(arg), None)
125
127
  if res is empty or res is None: res = nested_idx(req.scope, 'session', arg) or None
126
128
  if res is empty or res is None:
127
- #import pdb; pdb.set_trace()
128
129
  frm = await req.form()
129
130
  res = frm.getlist(arg)
130
131
  if res:
@@ -141,12 +142,20 @@ async def _wrap_req(req, params):
141
142
  @dataclass
142
143
  class HttpHeader: k:str;v:str
143
144
 
145
+ def flat_xt(lst):
146
+ result = []
147
+ for item in lst:
148
+ if isinstance(item, (list,tuple)) and not isinstance(item, XT): result.extend(item)
149
+ else: result.append(item)
150
+ return result
151
+
144
152
  def _xt_resp(req, resp, hdrs, **bodykw):
145
153
  http_hdrs,resp = partition(resp, risinstance(HttpHeader))
146
154
  http_hdrs = {o.k:str(o.v) for o in http_hdrs}
147
155
  titles,bdy = partition(resp, lambda o: getattr(o, 'tag', '')=='title')
148
- if resp and 'hx-request' not in req.headers and isinstance(resp,tuple) and titles:
149
- resp = Html(Head(titles[0], *hdrs), Body(bdy, **bodykw))
156
+ if resp and 'hx-request' not in req.headers and not any(getattr(o, 'tag', '')=='html' for o in resp):
157
+ if not titles: titles = [Title('FastHTML page')]
158
+ resp = Html(Head(titles[0], *flat_xt(hdrs)), Body(bdy, **bodykw))
150
159
  return HTMLResponse(to_xml(resp), headers=http_hdrs)
151
160
 
152
161
  def _wrap_resp(req, resp, cls, hdrs, **bodykw):
@@ -208,6 +217,8 @@ class RouterX(Router):
208
217
  htmxscr = Script(
209
218
  src="https://unpkg.com/htmx.org@1.9.12", crossorigin="anonymous",
210
219
  integrity="sha384-ujb1lZYygJmzgSwoxRggbCHcjc0rB2XoQrxeTUQyRjrOnlCoYta87iKBWq3EsdM2")
220
+ surrsrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/surreal/surreal.js")
221
+ scopesrc = Script(src="https://cdn.jsdelivr.net/gh/gnat/css-scope-inline/script.js")
211
222
 
212
223
  def get_key(key=None, fname='.sesskey'):
213
224
  if key: return key
@@ -221,7 +232,7 @@ def _list(o): return [] if not o else o if isinstance(o, (tuple,list)) else [o]
221
232
 
222
233
  class FastHTML(Starlette):
223
234
  def __init__(self, debug=False, routes=None, middleware=None, exception_handlers=None,
224
- on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, before=None,
235
+ on_startup=None, on_shutdown=None, lifespan=None, hdrs=None, before=None, default_hdrs=True,
225
236
  secret_key=None, session_cookie='session_', max_age=365*24*3600, sess_path='/',
226
237
  same_site='lax', sess_https_only=False, sess_domain=None, key_fname='.sesskey', **bodykw):
227
238
  middleware,before = _list(middleware),_list(before)
@@ -231,7 +242,8 @@ class FastHTML(Starlette):
231
242
  https_only=sess_https_only, domain=sess_domain)
232
243
  middleware.append(sess)
233
244
  super().__init__(debug, routes, middleware, exception_handlers, on_startup, on_shutdown, lifespan=lifespan)
234
- hdrs = list([] if hdrs is None else hdrs) + [htmxscr]
245
+ hdrs = list([] if hdrs is None else hdrs)
246
+ if default_hdrs: hdrs = [htmxscr,surrsrc,scopesrc] + hdrs
235
247
  self.router = RouterX(routes, on_startup=on_startup, on_shutdown=on_shutdown, lifespan=lifespan, hdrs=hdrs,
236
248
  before=before, **bodykw)
237
249
 
@@ -0,0 +1,48 @@
1
+ from fastcore.utils import *
2
+ from fasthtml.xtend import Script,jsd,Style
3
+
4
+ def light_media(css): return Style('@media (prefers-color-scheme: light) {%s}' %css)
5
+ def dark_media(css): return Style('@media (prefers-color-scheme: dark) {%s}' %css)
6
+
7
+ def MarkdownJS(sel='.marked'):
8
+ src = """
9
+ import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
10
+ import { proc_htmx} from "https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js/fasthtml.js";
11
+ proc_htmx('%s', e => e.innerHTML = marked.parse(e.textContent));
12
+ """ % sel
13
+ return Script(src, type='module')
14
+
15
+
16
+ def HighlightJS(sel='pre code', langs='python', light='atom-one-light', dark='atom-one-dark'):
17
+ src = """
18
+ hljs.addPlugin(new CopyButtonPlugin());
19
+ hljs.configure({'cssSelector': '%s'});
20
+ htmx.onLoad(hljs.highlightAll);""" % sel
21
+ hjs = 'highlightjs','cdn-release', 'build'
22
+ hjc = 'arronhunt' ,'highlightjs-copy', 'dist'
23
+ if isinstance(langs, str): langs = [langs]
24
+ langjs = [jsd(*hjs, f'languages/{lang}.min.js') for lang in langs]
25
+ return [jsd(*hjs, f'styles/{dark}.css', typ='css', media="(prefers-color-scheme: dark)"),
26
+ jsd(*hjs, f'styles/{light}.css', typ='css', media="(prefers-color-scheme: light)"),
27
+ jsd(*hjs, f'highlight.min.js'),
28
+ jsd(*hjc, 'highlightjs-copy.min.js'),
29
+ jsd(*hjc, 'highlightjs-copy.min.css', typ='css'),
30
+ light_media('.hljs-copy-button {background-color: #2d2b57;}'),
31
+ *langjs, Script(src, type='module')]
32
+
33
+
34
+ def SortableJS(sel='.sortable', ghost_class='blue-background-class'):
35
+ src = """
36
+ import {Sortable} from 'https://cdn.jsdelivr.net/npm/sortablejs/+esm';
37
+ import {proc_htmx} from "https://cdn.jsdelivr.net/gh/answerdotai/fasthtml-js/fasthtml.js";
38
+ proc_htmx('%s', elm => {
39
+ const s = new Sortable(elm, {
40
+ animation: 150, ghostClass: '%s', filter: ".htmx-indicator",
41
+ onMove: e => !e.related.classList.contains('htmx-indicator'),
42
+ onEnd: () => s.option("disabled", true),
43
+ });
44
+ htmx.on("htmx:afterSwap", () => s.option("disabled", false), elm);
45
+ });
46
+ """ % (sel, ghost_class)
47
+ return Script(src, type='module')
48
+
@@ -2,7 +2,7 @@
2
2
 
3
3
  # %% auto 0
4
4
  __all__ = ['picocss', 'picolink', 'picocondcss', 'picocondlink', 'set_pico_cls', 'Html', 'A', 'AX', 'Checkbox', 'Card', 'Group',
5
- 'Search', 'Grid', 'DialogX', 'Hidden']
5
+ 'Search', 'Grid', 'DialogX', 'Hidden', 'Script', 'Style', 'jsd']
6
6
 
7
7
  # %% ../nbs/02_xtend.ipynb 2
8
8
  from dataclasses import dataclass, asdict
@@ -96,3 +96,23 @@ def DialogX(*c, open=None, header=None, footer=None, id=None, **kwargs):
96
96
  @delegates(xt_hx, keep=True)
97
97
  def Hidden(value:str="", **kwargs):
98
98
  return Input(type="hidden", value=value, **kwargs)
99
+
100
+ # %% ../nbs/02_xtend.ipynb 28
101
+ @delegates(xt_html, keep=True)
102
+ def Script(code:str="", **kwargs):
103
+ "A Script tag that doesn't escape its code"
104
+ return xt_html('script', NotStr(code), **kwargs)
105
+
106
+ # %% ../nbs/02_xtend.ipynb 29
107
+ @delegates(xt_html, keep=True)
108
+ def Style(css:str="", **kwargs):
109
+ "A Style tag that doesn't escape its code"
110
+ return xt_html('style', NotStr(css), **kwargs)
111
+
112
+ # %% ../nbs/02_xtend.ipynb 30
113
+ def jsd(org, repo, root, path, typ='script', ver=None, esm=False, **kwargs):
114
+ "jsdelivr `Script` or CSS `Link` tag, or URL"
115
+ ver = '@'+ver if ver else ''
116
+ s = f'https://cdn.jsdelivr.net/gh/{org}/{repo}{ver}/{root}/{path}'
117
+ if esm: s += '/+esm'
118
+ return Script(src=s, **kwargs) if typ=='script' else Link(rel='stylesheet', href=s, **kwargs) if typ=='css' else s
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-fasthtml
3
- Version: 0.0.11
3
+ Version: 0.0.14
4
4
  Summary: The fastest way to create an HTML app
5
5
  Home-page: https://github.com/AnswerDotAI/fasthtml
6
6
  Author: Jeremy Howard
@@ -24,6 +24,7 @@ Requires-Dist: uvicorn[standard]
24
24
  Requires-Dist: httpx
25
25
  Requires-Dist: fastlite>=0.0.6
26
26
  Requires-Dist: python-multipart
27
+ Requires-Dist: beautifulsoup4
27
28
  Provides-Extra: dev
28
29
  Requires-Dist: ipython; extra == "dev"
29
30
  Requires-Dist: lxml; extra == "dev"
@@ -7,6 +7,7 @@ uvicorn[standard]
7
7
  httpx
8
8
  fastlite>=0.0.6
9
9
  python-multipart
10
+ beautifulsoup4
10
11
 
11
12
  [dev]
12
13
  ipython
@@ -1,10 +1,10 @@
1
1
  [DEFAULT]
2
2
  repo = fasthtml
3
3
  lib_name = fasthtml
4
- version = 0.0.11
4
+ version = 0.0.14
5
5
  min_python = 3.10
6
6
  license = apache2
7
- requirements = fastcore>=1.5.45 python-dateutil starlette oauthlib itsdangerous uvicorn[standard] httpx fastlite>=0.0.6 python-multipart
7
+ requirements = fastcore>=1.5.45 python-dateutil starlette oauthlib itsdangerous uvicorn[standard] httpx fastlite>=0.0.6 python-multipart beautifulsoup4
8
8
  dev_requirements = ipython lxml
9
9
  black_formatting = False
10
10
  conda_user = fastai
@@ -1,29 +0,0 @@
1
- from fastcore.utils import *
2
- from fasthtml.components import Script
3
-
4
- def MarkdownJS(sel):
5
- src = """
6
- import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
7
- htmx.onLoad(elt => htmx.findAll(elt, "%s").forEach(e => e.innerHTML = marked.parse(e.textContent)));
8
- """ % sel
9
- return Script(NotStr(src), type='module')
10
-
11
- def SortableJS(sel='.sortable', ghost_class='blue-background-class'):
12
- src = """
13
- import {Sortable} from 'https://cdn.jsdelivr.net/npm/sortablejs/+esm';
14
-
15
- htmx.onLoad(content => {
16
- content.querySelectorAll("%s").forEach(elm => {
17
- const s = new Sortable(elm, {
18
- animation: 150,
19
- ghostClass: '%s',
20
- filter: ".htmx-indicator",
21
- onMove: e => !e.related.classList.contains('htmx-indicator'),
22
- onEnd: () => s.option("disabled", true),
23
- });
24
- htmx.on("htmx:afterSwap", () => s.option("disabled", false), elm);
25
- });
26
- })
27
- """ % (sel, ghost_class)
28
- return Script(NotStr(src), type='module')
29
-