declare const htmx: any; /** * @description 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. * @param {string} selector * @param {function(Element): void} initializeElement */ 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); } }); } /** * @description Formats Date to a UTC string accepted by the datetime-local input field. * @param {Date} date * @returns {string} */ 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}`; } /** * @description Sync values between source and target elements based on syncData configuration. * @param {Array} syncData - Array of objects to define source and target elements with their respective value types. */ 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 } /** * @description Retrieve the value from the source element based on the provided property. * @param {Element} sourceElement - The source HTML element. * @param {string} property - The property to retrieve the value from. */ function getValueFromProperty(sourceElement: EventTarget, property: string): any { let source: HTMLElement | HTMLOptionElement = sourceElement instanceof HTMLSelectElement ? sourceElement.selectedOptions[0] : sourceElement as HTMLElement; if (property.startsWith("dataset.")) { let datasetKey = property.slice(8); // Remove 'dataset.' part return source.dataset[datasetKey]; } else if (property in source) { return (source as unknown as Record)[property]; } else { console.error(`Property ${property} is not valid for the option element.`); return null; } } type ElementHandlerConfig = [ condition: () => boolean, // condition function targetElements: string[], // array of target element selectors callbackfn1: (el: HTMLElement) => void, // callback function for matched condition callbackfn2: (el: HTMLElement) => void // callback function for unmatched condition ]; /** * @description Applies different behaviors to elements based on multiple conditional configurations. * Each configuration is an array containing a condition function, an array of target element selectors, * and two callback functions for handling matched and unmatched conditions. * @param {...Array} configs Each configuration is an array of the form: * - 0: {function(): boolean} condition - Function that returns true or false based on a condition. * - 1: {string[]} targetElements - Array of CSS selectors for target elements. * - 2: {function(HTMLElement): void} callbackfn1 - Function to execute when condition is true. * - 3: {function(HTMLElement): void} callbackfn2 - Function to execute when condition is false. */ function conditionalElementHandler(...configs: ElementHandlerConfig[]) { configs.forEach(([condition, targetElements, callbackfn1, callbackfn2]) => { if (condition()) { targetElements.forEach((elementName) => { let el = document.querySelector(elementName); if (el === null) { console.error(`Element ${elementName} doesn't exist.`); } else { callbackfn1(el); } }); } else { targetElements.forEach((elementName) => { let el = document.querySelector(elementName); if (el === null) { console.error(`Element ${elementName} doesn't exist.`); } else { callbackfn2(el); } }); } }); } function disableElementsWhenValueNotEqual( targetSelect: string, targetValue: string | string[], elementList: string[] ) { return conditionalElementHandler([ () => { let target = document.querySelector(targetSelect); if (!target) return false; console.debug( `${disableElementsWhenTrue.name}: triggered on ${target.id}` ); console.debug(` ${disableElementsWhenTrue.name}: matching against value(s): ${targetValue}`); if (targetValue instanceof Array) { if (targetValue.every((value) => target.value != value)) { console.debug( `${disableElementsWhenTrue.name}: none of the values is equal to ${target.value}, returning true.` ); return true; } return false; } else { console.debug( `${disableElementsWhenTrue.name}: none of the values is equal to ${target.value}, returning true.` ); return target.value != targetValue; } }, elementList, (el) => { console.debug( `${disableElementsWhenTrue.name}: evaluated true, disabling ${el.id}.` ); (el as HTMLInputElement).disabled = true; }, (el) => { console.debug( `${disableElementsWhenTrue.name}: evaluated false, NOT disabling ${el.id}.` ); (el as HTMLInputElement).disabled = false; }, ]); } function disableElementsWhenTrue(targetSelect: string, targetValue: string | string[], elementList: string[]) { return conditionalElementHandler([ () => { console.log(`${disableElementsWhenTrue.name}: triggered on ${targetSelect}`) console.log(`Value of ${targetSelect} is ${targetValue}: ${document.querySelector(targetSelect)?.value == targetValue}`) return document.querySelector(targetSelect)?.value == targetValue; }, elementList, (el) => { console.log(`${disableElementsWhenTrue.name}: disabling ${el.id}`); (el as HTMLInputElement).disabled = true; }, (el) => { console.log(`${disableElementsWhenTrue.name}: enabling ${el.id}`); (el as HTMLInputElement).disabled = false; }, ]); } export { onSwap, toISOUTCString, syncSelectInputUntilChanged, conditionalElementHandler, disableElementsWhenValueNotEqual, disableElementsWhenTrue, getValueFromProperty, };