diff --git a/index.ts b/index.ts index f949a8a..935ee11 100644 --- a/index.ts +++ b/index.ts @@ -1,9 +1,36 @@ import type { Action } from 'svelte/action'; /** InputElement is a union of all input types that can be validated. */ -export type InputElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement; +export type InputElement = + | HTMLInputElement + | HTMLTextAreaElement + | HTMLSelectElement + | HTMLDivElement; /** InputNodeList is a union of all node lists that can be validated. */ -export type InputNodeList = NodeListOf | NodeListOf | []; +// export type InputNodeList = NodeListOf | NodeListOf | []; +export type InputNodeList = NodeListOf; + +// getNodeValue is a utility function that returns the value of an input element. +export const getNodeValue = (node: InputElement): string => { + if (node instanceof HTMLDivElement) { + return node.textContent || ''; + } else { + return node.value; + } +}; + +// setNodeValue is a utility function that sets the value of an input element. +export const setNodeValue = (node: InputElement, value: string): void => { + if (node instanceof HTMLDivElement) { + node.textContent = value; + } else { + node.value = value; + if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) { + // Update selection range to the end of the input value + node.setSelectionRange(value.length, value.length); + } + } +}; /** ValidatorOptions configures the behavior of a validator. */ export type ValidatorOptions = { @@ -23,7 +50,9 @@ export type ValidatorOptions = { baseval?: string; /** autovalOnInvalid is a flag that automatically adds a keyup listener to the input on invalid. */ autovalOnInvalid?: boolean; + /** func is checked after other validation rules */ func?: (node: InputElement) => boolean | Promise; + /** valfunc is called early in validation */ valfunc?: (val: string) => boolean | Promise; }; @@ -67,8 +96,8 @@ export const score: RegExp = new RegExp(String.raw`^(-?\d{0,2}|100)$`); */ export type InputState = { value: string; - selectionStart: number; - selectionEnd: number; + selectionStart?: number; + selectionEnd?: number; }; let nextValidatorID = 0; @@ -128,17 +157,16 @@ export const keydownValidator: Action< { constrain?: boolean; } -> = (el, opts) => { - const node = el as HTMLInputElement; - +> = (node, opts) => { // prevent duplicate listeners if (node.dataset.validateKeydown !== undefined) return; node.dataset.validateKeydown = 'true'; - node.addEventListener('keydown', async (event: KeyboardEvent) => { + node.addEventListener('keydown', async (evt: Event) => { + const event = evt as KeyboardEvent; if (event.key.length > 1) return; // ignore non-character keys - const valid = await validateInput(node, node.value + event.key); + const valid = await validateInput(node, getNodeValue(node) + event.key); if (!valid && opts.constrain) { event.preventDefault(); } @@ -191,9 +219,7 @@ export const liveValidator: Action< { constrain?: boolean; } -> = (el, opts) => { - const node = el as HTMLInputElement; - +> = (node, opts) => { // prevent duplicate listeners if (node.dataset.validateLive !== undefined) return; node.dataset.validateLive = 'true'; @@ -205,35 +231,64 @@ export const liveValidator: Action< } // propagateState updates the stored state of the input element. - const propagateState = (e: HTMLInputElement) => { + const propagateState = (e: InputElement) => { lastState[id] = { - value: e.value, - selectionStart: e.selectionStart || 0, - selectionEnd: e.selectionEnd || 0 + value: getNodeValue(e) }; + + if (e instanceof HTMLInputElement || e instanceof HTMLTextAreaElement) { + lastState[id].selectionStart = e.selectionStart || 0; + lastState[id].selectionEnd = e.selectionEnd || 0; + } else if (e instanceof HTMLDivElement) { + const selection = window.getSelection(); + if (selection && selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + const preRange = range.cloneRange(); + preRange.selectNodeContents(e); + preRange.setEnd(range.startContainer, range.startOffset); + lastState[id].selectionStart = preRange.toString().length; + lastState[id].selectionEnd = lastState[id].selectionStart + range.toString().length; + } + } }; // revertToState reverts the input element to a previous state. - const revertToState = (e: HTMLInputElement, state: InputState) => { - e.value = state.value; - e.setSelectionRange(state.selectionStart, state.selectionEnd); + const revertToState = (e: InputElement, state: InputState) => { + setNodeValue(node, state.value); + if ( + (e instanceof HTMLInputElement || e instanceof HTMLTextAreaElement) && + state.selectionStart !== undefined && + state.selectionEnd !== undefined + ) { + e.setSelectionRange(state.selectionStart, state.selectionEnd); + } else if (e instanceof HTMLDivElement) { + const selection = window.getSelection(); + if (selection) { + const range = document.createRange(); + range.selectNodeContents(e); + range.setStart(e.firstChild || e, state.selectionStart || 0); + range.setEnd(e.firstChild || e, state.selectionEnd || 0); + selection.removeAllRanges(); + selection.addRange(range); + } + } }; // keydownHandler is used to track the last state of the input element. - const keydownHandler = (event: KeyboardEvent) => { + const keydownHandler = (event: Event) => { if (event.target) { - propagateState(event.target as HTMLInputElement); + propagateState(event.target as InputElement); } }; // inputHandler is used to validate the input element. const inputHandler = async (event: Event) => { - const n = event.target as HTMLInputElement; + const n = event.target as InputElement; const state = lastState[id]; if (!state || !event.target) return; // if the input value is the same as the last state, do nothing - if (n.value === state.value) return; + if (getNodeValue(n) === state.value) return; // if the input value is different, validate it const valid = await validateInput(n); @@ -298,7 +353,7 @@ export const validateInput = async ( input.dataset[DATA_STATE] = valid ? VALIDATOR_VALID : VALIDATOR_INVALID; input.dispatchEvent( - new InputValidatorEvent('validate', { detail: { value: input.value, valid } }) + new InputValidatorEvent('validate', { detail: { value: getNodeValue(input), valid } }) ); // if invalid, consider autovalOnInvalid @@ -353,7 +408,7 @@ export const isInputValid = async ( ): Promise => { const node = input as HTMLInputElement; - const val = valueOverride || node.value.trim(); + const val = valueOverride || getNodeValue(node); const opts = getopts(input); if (!opts) { console.warn('input has no validation rules', node);