add TimeInput component with time-of-day form option
This commit is contained in:
111
index.ts
111
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.
|
// score is a regex pattern for validating lesson grades.
|
||||||
export const score: RegExp = new RegExp(String.raw`^(-?\d{0,2}|100)$`);
|
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;
|
let nextValidatorID = 0;
|
||||||
const validators: Record<number, ValidatorOptions> = {};
|
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.
|
// 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.
|
// If opts is undefined, no validation rules will be attached.
|
||||||
@@ -113,11 +122,11 @@ export const keydownValidator: Action<
|
|||||||
node.dataset.validateKeydown = 'true';
|
node.dataset.validateKeydown = 'true';
|
||||||
|
|
||||||
node.addEventListener('keydown', async (event: KeyboardEvent) => {
|
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);
|
const valid = await validateInput(node, node.value + event.key);
|
||||||
if (event.key.length === 1 && !valid) {
|
if (!valid && opts.constrain) {
|
||||||
if (opts.constrain) {
|
event.preventDefault();
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -132,11 +141,92 @@ export const keyupValidator: Action<InputElement> = (el) => {
|
|||||||
if (node.dataset.validateKeyup !== undefined) return;
|
if (node.dataset.validateKeyup !== undefined) return;
|
||||||
node.dataset.validateKeyup = 'true';
|
node.dataset.validateKeyup = 'true';
|
||||||
|
|
||||||
node.addEventListener('keyup', () => {
|
node.addEventListener('keyup', (event: KeyboardEvent) => {
|
||||||
|
if (event.key.length > 1) return; // ignore non-character keys
|
||||||
validateInput(node);
|
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
|
// submitValidator attaches a submit event listener to a form element that will
|
||||||
// process the input values of all children according to any attached validation
|
// process the input values of all children according to any attached validation
|
||||||
// rules, triggering the validate event for individual input elements
|
// rules, triggering the validate event for individual input elements
|
||||||
@@ -334,9 +424,16 @@ export const isValueValid = async (val: string, opts: ValidatorOptions): Promise
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// getopts returns the validation rules attached to an input element.
|
// getid returns the validator ID attached to an input element. Returns -1 if
|
||||||
export const getopts = (input: InputElement): ValidatorOptions | undefined => {
|
// no ID is attached.
|
||||||
|
export const getid = (input: InputElement): number => {
|
||||||
const idstr = input.dataset[DATA_ID] || '-1';
|
const idstr = input.dataset[DATA_ID] || '-1';
|
||||||
const id = parseInt(idstr);
|
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];
|
return validators[id];
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user