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:
2026-06-20 15:54:00 +02:00
parent 82416e149d
commit b3fa7fac96
8 changed files with 201 additions and 103 deletions
+7 -15
View File
@@ -20,6 +20,7 @@
* NB: class strings below are emitted verbatim so the Tailwind scanner picks
* them up — keep them as plain literals.
*/
import { bindPopupDismiss } from "../utils.js";
type Anchor = "" | "start" | "end";
@@ -517,22 +518,13 @@ function createCalendarState(
closePopup();
});
const onKeyDown = (event: KeyboardEvent): void => {
if (event.key === "Escape" && state.open) closePopup();
};
const onMouseDown = (event: MouseEvent): void => {
if (state.open && !picker.contains(event.target as Node)) closePopup();
};
document.addEventListener("keydown", onKeyDown);
document.addEventListener("mousedown", onMouseDown);
const cleanup = bindPopupDismiss({
host: picker,
isOpen: () => state.open,
close: closePopup,
});
return {
state,
cleanup() {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("mousedown", onMouseDown);
},
};
return { state, cleanup };
}
function initPicker(picker: HTMLElement): () => void {