diff --git a/games/views/game.py b/games/views/game.py index 9a72075..bd4699b 100644 --- a/games/views/game.py +++ b/games/views/game.py @@ -180,7 +180,7 @@ def add_game(request: HttpRequest) -> HttpResponse: ), ), title="Add New Game", - scripts=ModuleScript("search_select.js") + ModuleScript("add_game.js"), + scripts=ModuleScript("search_select.js") + ModuleScript("dist/add_game.js"), ) diff --git a/ts/add_game.ts b/ts/add_game.ts new file mode 100644 index 0000000..343e15f --- /dev/null +++ b/ts/add_game.ts @@ -0,0 +1,12 @@ +import { syncSelectInputUntilChanged } from "./utils.js"; + +const syncData = [ + { + source: "#id_name", + source_value: "value", + target: "#id_sort_name", + target_value: "value", + }, +]; + +syncSelectInputUntilChanged(syncData, "form"); diff --git a/ts/utils.ts b/ts/utils.ts new file mode 100644 index 0000000..b427a80 --- /dev/null +++ b/ts/utils.ts @@ -0,0 +1,225 @@ +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 until the target is + * focused (manual edit wins). 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; + } + // Set up a single change event listener on the document for handling all source changes + parentElement.addEventListener("change", function (event) { + // Loop through each sync configuration item + syncData.forEach((syncItem: { source: string; target: string; source_value: string; target_value: string }) => { + // Check if the change event target matches the source selector + if ((event.target as HTMLElement).matches(syncItem.source)) { + if (!event.target) return; + const sourceElement = event.target; + const valueToSync = getValueFromProperty( + sourceElement, + syncItem.source_value + ); + const targetElement = document.querySelector(syncItem.target); + + if (targetElement && valueToSync !== null) { + console.log(`Changing value of ${syncItem.target} to ${valueToSync}`); + (targetElement as unknown as Record)[syncItem.target_value] = valueToSync; + } + } + }); + }); + + // Set up a single focus event listener on the document for handling all target focuses + const syncListener = (event: Event) => { + // Loop through each sync configuration item + syncData.forEach((syncItem: { source: string; target: string; source_value: string; target_value: string }) => { + // Check if the focus event target matches the target selector + if ((event.target as HTMLElement).matches(syncItem.target)) { + // Remove the change event listener to stop syncing + // This assumes you want to stop syncing once any target receives focus + // You may need a more sophisticated way to remove listeners if you want to stop + // syncing selectively based on other conditions + document.removeEventListener("change", syncListener); + } + }); + } + parentElement.addEventListener( + "focus", + syncListener, + true + ); // Use capture phase to ensure the event is captured during focus, not bubble +} + +/** + * Reads a property off the source element. For a