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:
+38
@@ -100,6 +100,43 @@ function getValueFromProperty(sourceElement: EventTarget, property: string): any
|
||||
}
|
||||
}
|
||||
|
||||
interface PopupDismissOptions {
|
||||
// Clicks within host (or any extraInside root) do not dismiss.
|
||||
host: HTMLElement;
|
||||
isOpen: () => boolean;
|
||||
close: () => void;
|
||||
// Extra roots considered "inside" — e.g. a library popup appended to
|
||||
// document.body rather than nested under host. Evaluated per event so a
|
||||
// lazily-created popup element is picked up once it exists.
|
||||
extraInside?: () => Array<Element | null | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires the shared dismiss behaviour for an anchored popup: Escape closes it,
|
||||
* and a mousedown outside the host (and any extraInside roots) closes it. Only
|
||||
* acts while isOpen() is true. Returns a cleanup function that removes both
|
||||
* document listeners — call it from disconnectedCallback.
|
||||
*/
|
||||
function bindPopupDismiss(options: PopupDismissOptions): () => void {
|
||||
const isInside = (target: Node): boolean => {
|
||||
if (options.host.contains(target)) return true;
|
||||
const extras = options.extraInside ? options.extraInside() : [];
|
||||
return extras.some((root) => !!root && root.contains(target));
|
||||
};
|
||||
const onKeyDown = (event: KeyboardEvent): void => {
|
||||
if (event.key === "Escape" && options.isOpen()) options.close();
|
||||
};
|
||||
const onMouseDown = (event: MouseEvent): void => {
|
||||
if (options.isOpen() && !isInside(event.target as Node)) options.close();
|
||||
};
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
};
|
||||
}
|
||||
|
||||
type ElementHandlerConfig = [
|
||||
condition: () => boolean, // condition function
|
||||
targetElements: string[], // array of target element selectors
|
||||
@@ -207,4 +244,5 @@ export {
|
||||
disableElementsWhenValueNotEqual,
|
||||
disableElementsWhenTrue,
|
||||
getValueFromProperty,
|
||||
bindPopupDismiss,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user