add TimeInput component with time-of-day form option
This commit is contained in:
109
index.ts
109
index.ts
@@ -53,8 +53,17 @@ export const phone: RegExp = new RegExp(String.raw`^(\+)?(\(?\d+\)?)(([\s-]+)?(\
|
||||
// score is a regex pattern for validating lesson grades.
|
||||
export const score: RegExp = new RegExp(String.raw`^(-?\d{0,2}|100)$`);
|
||||
|
||||
// InputState is some historical state of an input element, tracking its value,
|
||||
// selectionStart and selectionEnd.
|
||||
export type InputState = {
|
||||
value: string;
|
||||
selectionStart: number;
|
||||
selectionEnd: number;
|
||||
};
|
||||
|
||||
let nextValidatorID = 0;
|
||||
const validators: Record<number, ValidatorOptions> = {};
|
||||
const lastState: Record<number, InputState> = {};
|
||||
|
||||
// validate attaches rules to an input element that can be used to validate the input.
|
||||
// If opts is undefined, no validation rules will be attached.
|
||||
@@ -113,12 +122,12 @@ export const keydownValidator: Action<
|
||||
node.dataset.validateKeydown = 'true';
|
||||
|
||||
node.addEventListener('keydown', async (event: KeyboardEvent) => {
|
||||
if (event.key.length > 1) return; // ignore non-character keys
|
||||
|
||||
const valid = await validateInput(node, node.value + event.key);
|
||||
if (event.key.length === 1 && !valid) {
|
||||
if (opts.constrain) {
|
||||
if (!valid && opts.constrain) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -132,11 +141,92 @@ export const keyupValidator: Action<InputElement> = (el) => {
|
||||
if (node.dataset.validateKeyup !== undefined) return;
|
||||
node.dataset.validateKeyup = 'true';
|
||||
|
||||
node.addEventListener('keyup', () => {
|
||||
node.addEventListener('keyup', (event: KeyboardEvent) => {
|
||||
if (event.key.length > 1) return; // ignore non-character keys
|
||||
validateInput(node);
|
||||
});
|
||||
};
|
||||
|
||||
// inputValidator attaches an oninput event listener to an input element that will
|
||||
// process the input value according to any attached validation rules, triggering
|
||||
// the `validate` event with the input value and validation state.
|
||||
export const inputValidator: Action<InputElement> = (el) => {
|
||||
const node = el as HTMLInputElement;
|
||||
|
||||
// prevent duplicate listeners
|
||||
if (node.dataset.validateInput !== undefined) return;
|
||||
node.dataset.validateInput = 'true';
|
||||
|
||||
node.addEventListener('input', () => {
|
||||
validateInput(node);
|
||||
});
|
||||
};
|
||||
|
||||
// liveValidator attaches multiple event listeners to an input element that will
|
||||
// process the input value according to any attached validation rules, triggering
|
||||
// the `validate` event with the input value and validation state. liveValidator
|
||||
// supports the constrain option without introducing edge cases with text selection.
|
||||
export const liveValidator: Action<
|
||||
InputElement,
|
||||
{
|
||||
constrain?: boolean;
|
||||
}
|
||||
> = (el, opts) => {
|
||||
const node = el as HTMLInputElement;
|
||||
|
||||
// prevent duplicate listeners
|
||||
if (node.dataset.validateLive !== undefined) return;
|
||||
node.dataset.validateLive = 'true';
|
||||
|
||||
const id = getid(node);
|
||||
if (id === -1) {
|
||||
console.warn('liveValidator: no validator ID found', node);
|
||||
return;
|
||||
}
|
||||
|
||||
// propagateState updates the stored state of the input element.
|
||||
const propagateState = (e: HTMLInputElement) => {
|
||||
lastState[id] = {
|
||||
value: e.value,
|
||||
selectionStart: e.selectionStart || 0,
|
||||
selectionEnd: e.selectionEnd || 0
|
||||
};
|
||||
};
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
// keydownHandler is used to track the last state of the input element.
|
||||
const keydownHandler = (event: KeyboardEvent) => {
|
||||
if (event.target) {
|
||||
propagateState(event.target as HTMLInputElement);
|
||||
}
|
||||
};
|
||||
|
||||
// inputHandler is used to validate the input element.
|
||||
const inputHandler = async (event: Event) => {
|
||||
const n = event.target as HTMLInputElement;
|
||||
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 the input value is different, validate it
|
||||
const valid = await validateInput(n);
|
||||
if (!valid && opts.constrain) {
|
||||
revertToState(n, state);
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
node.addEventListener('keydown', keydownHandler);
|
||||
node.addEventListener('input', inputHandler);
|
||||
};
|
||||
|
||||
// submitValidator attaches a submit event listener to a form element that will
|
||||
// process the input values of all children according to any attached validation
|
||||
// rules, triggering the validate event for individual input elements
|
||||
@@ -334,9 +424,16 @@ export const isValueValid = async (val: string, opts: ValidatorOptions): Promise
|
||||
return true;
|
||||
};
|
||||
|
||||
// getopts returns the validation rules attached to an input element.
|
||||
export const getopts = (input: InputElement): ValidatorOptions | undefined => {
|
||||
// getid returns the validator ID attached to an input element. Returns -1 if
|
||||
// no ID is attached.
|
||||
export const getid = (input: InputElement): number => {
|
||||
const idstr = input.dataset[DATA_ID] || '-1';
|
||||
const id = parseInt(idstr);
|
||||
return id;
|
||||
};
|
||||
|
||||
// getopts returns the validation rules attached to an input element.
|
||||
export const getopts = (input: InputElement): ValidatorOptions | undefined => {
|
||||
const id = getid(input);
|
||||
return validators[id];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user