support contenteditable div validation
This commit is contained in:
105
index.ts
105
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<HTMLInputElement> | NodeListOf<HTMLTextAreaElement> | [];
|
||||
// export type InputNodeList = NodeListOf<HTMLInputElement> | NodeListOf<HTMLTextAreaElement> | [];
|
||||
export type InputNodeList = NodeListOf<InputElement>;
|
||||
|
||||
// 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<boolean>;
|
||||
/** valfunc is called early in validation */
|
||||
valfunc?: (val: string) => boolean | Promise<boolean>;
|
||||
};
|
||||
|
||||
@@ -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<boolean> => {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user