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';
|
import type { Action } from 'svelte/action';
|
||||||
|
|
||||||
/** InputElement is a union of all input types that can be validated. */
|
/** 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. */
|
/** 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. */
|
/** ValidatorOptions configures the behavior of a validator. */
|
||||||
export type ValidatorOptions = {
|
export type ValidatorOptions = {
|
||||||
@@ -23,7 +50,9 @@ export type ValidatorOptions = {
|
|||||||
baseval?: string;
|
baseval?: string;
|
||||||
/** autovalOnInvalid is a flag that automatically adds a keyup listener to the input on invalid. */
|
/** autovalOnInvalid is a flag that automatically adds a keyup listener to the input on invalid. */
|
||||||
autovalOnInvalid?: boolean;
|
autovalOnInvalid?: boolean;
|
||||||
|
/** func is checked after other validation rules */
|
||||||
func?: (node: InputElement) => boolean | Promise<boolean>;
|
func?: (node: InputElement) => boolean | Promise<boolean>;
|
||||||
|
/** valfunc is called early in validation */
|
||||||
valfunc?: (val: string) => boolean | Promise<boolean>;
|
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 = {
|
export type InputState = {
|
||||||
value: string;
|
value: string;
|
||||||
selectionStart: number;
|
selectionStart?: number;
|
||||||
selectionEnd: number;
|
selectionEnd?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
let nextValidatorID = 0;
|
let nextValidatorID = 0;
|
||||||
@@ -128,17 +157,16 @@ export const keydownValidator: Action<
|
|||||||
{
|
{
|
||||||
constrain?: boolean;
|
constrain?: boolean;
|
||||||
}
|
}
|
||||||
> = (el, opts) => {
|
> = (node, opts) => {
|
||||||
const node = el as HTMLInputElement;
|
|
||||||
|
|
||||||
// prevent duplicate listeners
|
// prevent duplicate listeners
|
||||||
if (node.dataset.validateKeydown !== undefined) return;
|
if (node.dataset.validateKeydown !== undefined) return;
|
||||||
node.dataset.validateKeydown = 'true';
|
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
|
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) {
|
if (!valid && opts.constrain) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -191,9 +219,7 @@ export const liveValidator: Action<
|
|||||||
{
|
{
|
||||||
constrain?: boolean;
|
constrain?: boolean;
|
||||||
}
|
}
|
||||||
> = (el, opts) => {
|
> = (node, opts) => {
|
||||||
const node = el as HTMLInputElement;
|
|
||||||
|
|
||||||
// prevent duplicate listeners
|
// prevent duplicate listeners
|
||||||
if (node.dataset.validateLive !== undefined) return;
|
if (node.dataset.validateLive !== undefined) return;
|
||||||
node.dataset.validateLive = 'true';
|
node.dataset.validateLive = 'true';
|
||||||
@@ -205,35 +231,64 @@ export const liveValidator: Action<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// propagateState updates the stored state of the input element.
|
// propagateState updates the stored state of the input element.
|
||||||
const propagateState = (e: HTMLInputElement) => {
|
const propagateState = (e: InputElement) => {
|
||||||
lastState[id] = {
|
lastState[id] = {
|
||||||
value: e.value,
|
value: getNodeValue(e)
|
||||||
selectionStart: e.selectionStart || 0,
|
|
||||||
selectionEnd: e.selectionEnd || 0
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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.
|
// revertToState reverts the input element to a previous state.
|
||||||
const revertToState = (e: HTMLInputElement, state: InputState) => {
|
const revertToState = (e: InputElement, state: InputState) => {
|
||||||
e.value = state.value;
|
setNodeValue(node, state.value);
|
||||||
e.setSelectionRange(state.selectionStart, state.selectionEnd);
|
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.
|
// keydownHandler is used to track the last state of the input element.
|
||||||
const keydownHandler = (event: KeyboardEvent) => {
|
const keydownHandler = (event: Event) => {
|
||||||
if (event.target) {
|
if (event.target) {
|
||||||
propagateState(event.target as HTMLInputElement);
|
propagateState(event.target as InputElement);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// inputHandler is used to validate the input element.
|
// inputHandler is used to validate the input element.
|
||||||
const inputHandler = async (event: Event) => {
|
const inputHandler = async (event: Event) => {
|
||||||
const n = event.target as HTMLInputElement;
|
const n = event.target as InputElement;
|
||||||
const state = lastState[id];
|
const state = lastState[id];
|
||||||
if (!state || !event.target) return;
|
if (!state || !event.target) return;
|
||||||
|
|
||||||
// if the input value is the same as the last state, do nothing
|
// 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
|
// if the input value is different, validate it
|
||||||
const valid = await validateInput(n);
|
const valid = await validateInput(n);
|
||||||
@@ -298,7 +353,7 @@ export const validateInput = async (
|
|||||||
|
|
||||||
input.dataset[DATA_STATE] = valid ? VALIDATOR_VALID : VALIDATOR_INVALID;
|
input.dataset[DATA_STATE] = valid ? VALIDATOR_VALID : VALIDATOR_INVALID;
|
||||||
input.dispatchEvent(
|
input.dispatchEvent(
|
||||||
new InputValidatorEvent('validate', { detail: { value: input.value, valid } })
|
new InputValidatorEvent('validate', { detail: { value: getNodeValue(input), valid } })
|
||||||
);
|
);
|
||||||
|
|
||||||
// if invalid, consider autovalOnInvalid
|
// if invalid, consider autovalOnInvalid
|
||||||
@@ -353,7 +408,7 @@ export const isInputValid = async (
|
|||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
const node = input as HTMLInputElement;
|
const node = input as HTMLInputElement;
|
||||||
|
|
||||||
const val = valueOverride || node.value.trim();
|
const val = valueOverride || getNodeValue(node);
|
||||||
const opts = getopts(input);
|
const opts = getopts(input);
|
||||||
if (!opts) {
|
if (!opts) {
|
||||||
console.warn('input has no validation rules', node);
|
console.warn('input has no validation rules', node);
|
||||||
|
|||||||
Reference in New Issue
Block a user