declare const htmx: any; /** * Runs initializeElement once for each element matching selector, on initial * page load and inside every htmx-swapped fragment (a port of FastHTML's * proc_htmx). htmx fires htmx:load for the initial document and for each * swapped-in element, so a single registration covers both; the WeakSet * guarantees once-per-element initialization, replacing the old * DOMContentLoaded + htmx:afterSwap + per-element guard-flag pattern. */ function onSwap(selector: string, initializeElement: (element: Element) => void) { const initialized = new WeakSet(); htmx.onLoad((swappedElement: Element) => { const elements: Element[] = Array.from(htmx.findAll(swappedElement, selector)); if (swappedElement.matches && swappedElement.matches(selector)) { elements.unshift(swappedElement); } for (const element of elements) { if (initialized.has(element)) continue; initialized.add(element); initializeElement(element); } }); } /** Formats Date to a UTC string accepted by the datetime-local input field. */ function toISOUTCString(date: Date): string { function stringAndPad(number: number) { return number.toString().padStart(2, "0"); } const year = date.getFullYear(); const month = stringAndPad(date.getMonth() + 1); const day = stringAndPad(date.getDate()); const hours = stringAndPad(date.getHours()); const minutes = stringAndPad(date.getMinutes()); return `${year}-${month}-${day}T${hours}:${minutes}`; } /** * Mirrors each source element's value onto its target live as the user types, * until the user edits the target directly — at which point that target is * "dirty" and the manual value wins (no more mirroring into it). Each syncData * entry maps a source selector and property onto a target selector and property. */ function syncSelectInputUntilChanged(syncData: Array<{ source: string; target: string; source_value: string; target_value: string }>, parentSelector: string | Document = document) { const parentElement = parentSelector === document ? document : document.querySelector(parentSelector as string); if (!parentElement) { console.error(`The parent selector "${parentSelector}" is not valid.`); return; } // One delegated "input" listener drives both directions per syncItem. "input" // (not "change") makes the mirror live as the user types. A target the user // edits is marked dirty so the mirror stops clobbering it — programmatically // setting target.value does NOT fire "input", so our own writes never mark a // target dirty; only real user edits do. const dirtyTargets = new Set(); parentElement.addEventListener("input", function (event) { const eventTarget = event.target as HTMLElement; syncData.forEach((syncItem, index) => { // User edited the target directly → stop mirroring into it. if (eventTarget.matches(syncItem.target)) { dirtyTargets.add(index); return; } // Source changed → mirror into the target unless the user took it over. if (eventTarget.matches(syncItem.source) && !dirtyTargets.has(index)) { const valueToSync = getValueFromProperty(eventTarget, syncItem.source_value); const targetElement = document.querySelector(syncItem.target); if (targetElement && valueToSync !== null) { (targetElement as unknown as Record)[syncItem.target_value] = valueToSync; } } }); }); } /** * Reads a property off the source element. For a