Remove inline-handler → window.* contract (issue #28)
Finish the behavioural refactor from #28: no first-party JS lives on the global object solely to be reachable from a server-rendered inline on* attribute, and no inline Alpine blobs remain in the filter bar / year picker. - Filter-bar collapse: drop the inline onclick for a delegated click listener on the persistent <filter-bar> custom element (data-filter-bar-toggle). The inner #filter-bar body is htmx-swapped while connectedCallback does not re-run, so delegation on the host preserves the swap-survival the inline handler had. - YearPicker: convert the Alpine x-data/x-on/x-ref/_pickerInstance f-string into a <year-picker> custom element with typed props (YearPickerProps). Behavior moves to ts/elements/year-picker.ts; ts/year_picker.ts and _YEAR_PICKER_MEDIA are removed. The builder lives in primitives.py (next to YearPicker) to avoid a circular import; registration stays in custom_elements.py for codegen. - Add bindPopupDismiss (ts/utils.ts): shared Escape + outside-click dismiss with a cleanup return and an extraInside hook for popups mounted on document.body. Adopted by date-range-picker.ts (1:1) and year-picker.ts (Datepicker popup is body-mounted, passed as an extra inside root). Follow-up #49 tracks unifying popup/dismiss/positioning across the remaining dropdown/search-select/Flowbite cases. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -189,6 +189,18 @@ register_element("filter-bar", "FilterBar", FilterBarProps)
|
||||
_FilterBarElement = custom_element_builder("filter-bar")
|
||||
|
||||
|
||||
class YearPickerProps(TypedDict):
|
||||
selected_year: str # "" for the all-time/empty state
|
||||
available_years: str # csv, e.g. "2019,2020"
|
||||
url_template: str # contains the literal __year__ placeholder
|
||||
|
||||
|
||||
# The <year-picker> builder lives in primitives.py (next to YearPicker, which
|
||||
# uses it) because custom_elements imports from primitives — registering here
|
||||
# would be a circular import. Registration is codegen-only, so it belongs here.
|
||||
register_element("year-picker", "YearPicker", YearPickerProps)
|
||||
|
||||
|
||||
def SelectionFields(
|
||||
*,
|
||||
source: str,
|
||||
|
||||
@@ -563,10 +563,8 @@ def _filter_collapse_button() -> Node:
|
||||
("type", "button"),
|
||||
# Slider handles are positioned in percentages, so initializing
|
||||
# them while the body is hidden is safe — no re-init on reveal.
|
||||
(
|
||||
"onclick",
|
||||
"document.getElementById('filter-bar-body').classList.toggle('hidden')",
|
||||
),
|
||||
# Click is wired by filter-bar.ts (no inline handler).
|
||||
("data-filter-bar-toggle", ""),
|
||||
(
|
||||
"class",
|
||||
"flex items-center gap-2 text-sm font-medium text-body "
|
||||
|
||||
@@ -550,14 +550,19 @@ def StaticScript(filename: str) -> SafeText:
|
||||
return mark_safe(f'<script src="{static("js/" + filename)}"></script>')
|
||||
|
||||
|
||||
# Media for the Flowbite-datepicker year picker: the vendored UMD bundle
|
||||
# (classic script) plus the ts/year_picker.ts glue (compiled module). Declared
|
||||
# on the YearPicker node so Page() loads both wherever a YearPicker appears. The
|
||||
# UMD bundle is a classic script (runs during parse) while year_picker.js is a
|
||||
# deferred module (runs after parse), so Datepicker is defined by the time the
|
||||
# module executes regardless of tag order.
|
||||
_YEAR_PICKER_MEDIA = Media(
|
||||
js_external=("datepicker.umd.js",), js=("dist/year_picker.js",)
|
||||
# The <year-picker> custom element wraps the Flowbite-datepicker year grid.
|
||||
# The builder auto-attaches dist/elements/year-picker.js; the vendored UMD
|
||||
# bundle (classic script, runs during parse) is merged in via with_media so
|
||||
# Datepicker is defined by the time the deferred element module executes.
|
||||
_YearPicker = custom_element_builder("year-picker")
|
||||
_DATEPICKER_MEDIA = Media(js_external=("datepicker.umd.js",))
|
||||
|
||||
# The down-chevron rendered inside the YearPicker button. Trusted static SVG.
|
||||
_YEAR_PICKER_CHEVRON = Safe(
|
||||
'<svg class="w-4 h-4 ms-2 rtl:rotate-180" aria-hidden="true" '
|
||||
'xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">'
|
||||
'<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" '
|
||||
'stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/></svg>'
|
||||
)
|
||||
|
||||
|
||||
@@ -574,8 +579,10 @@ def YearPicker(
|
||||
placeholder, substituted with the chosen year in JS (keeps this component
|
||||
decoupled from the project's URL names).
|
||||
|
||||
The Flowbite-datepicker UMD bundle is declared as ``media`` on the returned
|
||||
node, so ``Page()`` loads it automatically.
|
||||
Behavior lives in ``ts/elements/year-picker.ts``; this renders the light
|
||||
DOM (toggle button + hidden datepicker input). The element module and the
|
||||
Flowbite UMD bundle are declared as ``media`` on the node, so ``Page()``
|
||||
loads both automatically.
|
||||
"""
|
||||
label = str(year) if year is not None else "Choose a year"
|
||||
selected = str(year) if year is not None else ""
|
||||
@@ -586,26 +593,40 @@ def YearPicker(
|
||||
"hover:bg-neutral-tertiary-medium focus:ring-4 focus:ring-brand-medium"
|
||||
)
|
||||
years_csv = ",".join(str(y) for y in available_years)
|
||||
return Safe(
|
||||
f"""<div class="relative inline-block" x-data="{{ pickerOpen: false }}"
|
||||
@keydown.escape.window="pickerOpen = false">
|
||||
<button type="button"
|
||||
x-on:click="pickerOpen = !pickerOpen; $refs.pickerInput._pickerInstance && ($refs.pickerInput._pickerInstance.active ? $refs.pickerInput._pickerInstance.hide() : $refs.pickerInput._pickerInstance.show())"
|
||||
class="inline-flex items-center rounded-base px-4 py-2 text-sm font-medium {classes}">
|
||||
{label}
|
||||
<svg class="w-4 h-4 ms-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
||||
</svg>
|
||||
</button>
|
||||
<input type="text" x-ref="pickerInput" id="year-picker-input"
|
||||
class="absolute opacity-0 pointer-events-none"
|
||||
style="width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); border: 0;"
|
||||
data-available-years="{years_csv}"
|
||||
data-selected-year="{selected}"
|
||||
data-url-template="{url_template}">
|
||||
</div>""",
|
||||
media=_YEAR_PICKER_MEDIA,
|
||||
)
|
||||
return _YearPicker(
|
||||
attributes=[
|
||||
("selected-year", selected),
|
||||
("available-years", years_csv),
|
||||
("url-template", url_template),
|
||||
("class", "relative inline-block"),
|
||||
],
|
||||
children=[
|
||||
Element(
|
||||
"button",
|
||||
attributes=[
|
||||
("type", "button"),
|
||||
("data-year-picker-toggle", ""),
|
||||
(
|
||||
"class",
|
||||
"inline-flex items-center rounded-base px-4 py-2 "
|
||||
f"text-sm font-medium {classes}",
|
||||
),
|
||||
],
|
||||
children=[label, _YEAR_PICKER_CHEVRON],
|
||||
),
|
||||
Input(
|
||||
attributes=[
|
||||
("id", "year-picker-input"),
|
||||
("class", "absolute opacity-0 pointer-events-none"),
|
||||
(
|
||||
"style",
|
||||
"width: 1px; height: 1px; padding: 0; margin: -1px; "
|
||||
"overflow: hidden; clip: rect(0,0,0,0); border: 0;",
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).with_media(_DATEPICKER_MEDIA)
|
||||
|
||||
|
||||
# Form-field rendering. The element classes (label/error/checkbox-row + the
|
||||
|
||||
Reference in New Issue
Block a user