mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-09-10 08:37:24 +08:00
impr: add validations to settings input (@fehmer) (#6751)
Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
da720ac130
commit
e32155edbb
10 changed files with 668 additions and 608 deletions
|
@ -256,9 +256,6 @@
|
|||
step="1"
|
||||
value=""
|
||||
/>
|
||||
<button class="save no-auto-handle">
|
||||
<i class="fas fa-save fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button data-config-value="off">off</button>
|
||||
|
@ -288,9 +285,6 @@
|
|||
step="1"
|
||||
value=""
|
||||
/>
|
||||
<button class="save no-auto-handle">
|
||||
<i class="fas fa-save fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button data-config-value="off">off</button>
|
||||
|
@ -322,9 +316,6 @@
|
|||
step="1"
|
||||
value=""
|
||||
/>
|
||||
<button class="save no-auto-handle">
|
||||
<i class="fas fa-save fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button data-config-value="off">off</button>
|
||||
|
@ -792,9 +783,6 @@
|
|||
step="1"
|
||||
value=""
|
||||
/>
|
||||
<button class="save no-auto-handle">
|
||||
<i class="fas fa-save fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button data-config-value="off">off</button>
|
||||
|
@ -1020,9 +1008,6 @@
|
|||
min="10"
|
||||
max="90"
|
||||
/>
|
||||
<button class="save no-auto-handle">
|
||||
<i class="fas fa-save fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1131,9 +1116,6 @@
|
|||
class="input"
|
||||
min="0"
|
||||
/>
|
||||
<button class="save no-auto-handle">
|
||||
<i class="fas fa-save fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1154,9 +1136,6 @@
|
|||
class="input"
|
||||
tabindex="0"
|
||||
/>
|
||||
<button class="save no-auto-handle">
|
||||
<i class="fas fa-save fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
.inputAndButton {
|
||||
display: grid;
|
||||
grid-template-columns: auto min-content;
|
||||
gap: 0.5rem;
|
||||
// gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
span {
|
||||
|
@ -104,6 +104,20 @@
|
|||
margin-right: 0rem;
|
||||
}
|
||||
}
|
||||
|
||||
.hasError {
|
||||
animation: shake 0.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.statusIndicator {
|
||||
opacity: 0;
|
||||
}
|
||||
&:has(input:focus),
|
||||
&:has([data-indicator-status="failed"]) {
|
||||
.statusIndicator {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rangeGroup {
|
||||
|
|
|
@ -14,6 +14,7 @@ import { Command, CommandsSubgroup, CommandWithValidation } from "./types";
|
|||
import { areSortedArraysEqual } from "../utils/arrays";
|
||||
import { parseIntOptional } from "../utils/numbers";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import { createInputEventHandler } from "../elements/input-validation";
|
||||
|
||||
type CommandlineMode = "search" | "input";
|
||||
type InputModeParams = {
|
||||
|
@ -618,6 +619,18 @@ async function runActiveCommand(): Promise<void> {
|
|||
value: command.defaultValue?.() ?? "",
|
||||
icon: command.icon ?? "fa-chevron-right",
|
||||
};
|
||||
if ("validation" in command && !handlersCache.has(command.id)) {
|
||||
const commandWithValidation = command as CommandWithValidation<unknown>;
|
||||
const handler = createInputEventHandler(
|
||||
updateValidationResult,
|
||||
commandWithValidation.validation,
|
||||
"inputValueConvert" in commandWithValidation
|
||||
? commandWithValidation.inputValueConvert
|
||||
: undefined
|
||||
);
|
||||
handlersCache.set(command.id, handler);
|
||||
}
|
||||
|
||||
await updateInput(inputModeParams.value as string);
|
||||
hideCommands();
|
||||
} else if (command.subgroup) {
|
||||
|
@ -788,48 +801,10 @@ function updateValidationResult(
|
|||
}
|
||||
}
|
||||
|
||||
async function isValid(
|
||||
checkValue: unknown,
|
||||
originalValue: string,
|
||||
originalInput: HTMLInputElement,
|
||||
validation: CommandWithValidation<unknown>["validation"]
|
||||
): Promise<void> {
|
||||
updateValidationResult({ status: "checking" });
|
||||
|
||||
if (validation.schema !== undefined) {
|
||||
const schemaResult = validation.schema.safeParse(checkValue);
|
||||
|
||||
if (!schemaResult.success) {
|
||||
updateValidationResult({
|
||||
status: "failed",
|
||||
errorMessage: schemaResult.error.errors
|
||||
.map((err) => err.message)
|
||||
.join(", "),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (validation.isValid === undefined) {
|
||||
updateValidationResult({ status: "success" });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await validation.isValid(checkValue);
|
||||
if (originalInput.value !== originalValue) {
|
||||
//value has change in the meantime, discard result
|
||||
return;
|
||||
}
|
||||
|
||||
if (result === true) {
|
||||
updateValidationResult({ status: "success" });
|
||||
} else {
|
||||
updateValidationResult({
|
||||
status: "failed",
|
||||
errorMessage: result,
|
||||
});
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Handlers needs to be created only once per command to ensure they debounce with the given delay
|
||||
*/
|
||||
const handlersCache = new Map<string, (e: Event) => Promise<void>>();
|
||||
|
||||
const modal = new AnimatedModal({
|
||||
dialogId: "commandLine",
|
||||
|
@ -921,34 +896,24 @@ const modal = new AnimatedModal({
|
|||
}
|
||||
});
|
||||
|
||||
input.addEventListener(
|
||||
"input",
|
||||
debounce(100, async (e) => {
|
||||
if (
|
||||
inputModeParams === null ||
|
||||
inputModeParams.command === null ||
|
||||
!("validation" in inputModeParams.command)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
input.addEventListener("input", async (e) => {
|
||||
if (
|
||||
inputModeParams === null ||
|
||||
inputModeParams.command === null ||
|
||||
!("validation" in inputModeParams.command)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const originalInput = (e as InputEvent).target as HTMLInputElement;
|
||||
const currentValue = originalInput.value;
|
||||
let checkValue: unknown = currentValue;
|
||||
const command =
|
||||
inputModeParams.command as CommandWithValidation<unknown>;
|
||||
|
||||
if ("inputValueConvert" in command) {
|
||||
checkValue = command.inputValueConvert(currentValue);
|
||||
}
|
||||
await isValid(
|
||||
checkValue,
|
||||
currentValue,
|
||||
originalInput,
|
||||
command.validation
|
||||
const handler = handlersCache.get(inputModeParams.command.id);
|
||||
if (handler === undefined) {
|
||||
throw new Error(
|
||||
`Expected handler for command ${inputModeParams.command.id} is missing`
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await handler(e);
|
||||
});
|
||||
|
||||
modalEl.addEventListener("mousemove", (_e) => {
|
||||
mouseMode = true;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Config } from "@monkeytype/schemas/configs";
|
||||
import AnimatedModal from "../utils/animated-modal";
|
||||
import { z } from "zod";
|
||||
import { Validation } from "../elements/input-validation";
|
||||
|
||||
// this file is needed becauase otherwise it would produce a circular dependency
|
||||
|
||||
|
@ -47,21 +47,7 @@ export type CommandWithValidation<T> = (T extends string
|
|||
* If the schema is defined it is always checked first.
|
||||
* Only if the schema validaton is passed or missing the `isValid` method is called.
|
||||
*/
|
||||
validation: {
|
||||
/**
|
||||
* Zod schema to validate the input value against.
|
||||
* The indicator will show the error messages from the schema.
|
||||
*/
|
||||
schema?: z.Schema<T>;
|
||||
/**
|
||||
* Custom async validation method.
|
||||
* This is intended to be used for validations that cannot be handled with a Zod schema like server-side validations.
|
||||
* @param value current input value
|
||||
* @param thisPopup the current modal
|
||||
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
|
||||
*/
|
||||
isValid?: (value: T) => Promise<true | string>;
|
||||
};
|
||||
validation: Validation<T>;
|
||||
};
|
||||
|
||||
export type CommandsSubgroup = {
|
||||
|
|
|
@ -74,6 +74,7 @@ export class InputIndicator {
|
|||
}
|
||||
|
||||
$(this.inputElement).css("padding-right", "2.1em");
|
||||
this.parentElement.attr("data-indicator-status", optionId);
|
||||
}
|
||||
|
||||
get(): keyof typeof this.options | null {
|
||||
|
|
252
frontend/src/ts/elements/input-validation.ts
Normal file
252
frontend/src/ts/elements/input-validation.ts
Normal file
|
@ -0,0 +1,252 @@
|
|||
import { debounce } from "throttle-debounce";
|
||||
import { z, ZodType } from "zod";
|
||||
import { InputIndicator } from "./input-indicator";
|
||||
import {
|
||||
ConfigKey,
|
||||
ConfigSchema,
|
||||
Config as ConfigType,
|
||||
} from "@monkeytype/schemas/configs";
|
||||
import Config, * as UpdateConfig from "../config";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
|
||||
export type ValidationResult = {
|
||||
status: "checking" | "success" | "failed";
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
export type Validation<T> = {
|
||||
/**
|
||||
* Zod schema to validate the input value against.
|
||||
* The indicator will show the error messages from the schema.
|
||||
*/
|
||||
schema?: z.Schema<T>;
|
||||
|
||||
/**
|
||||
* Custom async validation method.
|
||||
* This is intended to be used for validations that cannot be handled with a Zod schema like server-side validations.
|
||||
* @param value current input value
|
||||
* @param thisPopup the current modal
|
||||
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
|
||||
*/
|
||||
isValid?: (value: T) => Promise<true | string>;
|
||||
|
||||
/** custom debounce delay for `isValid` call. defaults to 100 */
|
||||
debounceDelay?: number;
|
||||
};
|
||||
/**
|
||||
* Create input handler for validated input element.
|
||||
* the `callback` is called for each validation state change, including "checking".
|
||||
* @param callback callback to call for each change of the validation status
|
||||
* @param validation validation options
|
||||
* @param inputValueConvert convert method from string to the schema type, mandatory if the schema is not a string schema
|
||||
* @returns debounced input event handler
|
||||
*/
|
||||
export function createInputEventHandler<T>(
|
||||
callback: (result: ValidationResult) => void,
|
||||
validation: Validation<T>,
|
||||
inputValueConvert?: (val: string) => T
|
||||
): (e: Event) => Promise<void> {
|
||||
let callIsValid =
|
||||
validation.isValid !== undefined
|
||||
? debounce(
|
||||
validation.debounceDelay ?? 100,
|
||||
async (
|
||||
originalInput: HTMLInputElement,
|
||||
currentValue: string,
|
||||
checkValue: T
|
||||
) => {
|
||||
const result = await validation.isValid?.(checkValue);
|
||||
if (originalInput.value !== currentValue) {
|
||||
//value has change in the meantime, discard result
|
||||
return;
|
||||
}
|
||||
|
||||
if (result === true) {
|
||||
callback({ status: "success" });
|
||||
} else {
|
||||
callback({ status: "failed", errorMessage: result });
|
||||
}
|
||||
}
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return async (e) => {
|
||||
const originalInput = e.target as HTMLInputElement;
|
||||
const currentValue = originalInput.value;
|
||||
let checkValue: unknown = currentValue;
|
||||
|
||||
if (inputValueConvert !== undefined) {
|
||||
checkValue = inputValueConvert(currentValue);
|
||||
}
|
||||
|
||||
callback({ status: "checking" });
|
||||
|
||||
if (validation.schema !== undefined) {
|
||||
const schemaResult = validation.schema.safeParse(checkValue);
|
||||
|
||||
if (!schemaResult.success) {
|
||||
callback({
|
||||
status: "failed",
|
||||
errorMessage: schemaResult.error.errors
|
||||
.map((err) => err.message)
|
||||
.join(", "),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (callIsValid === undefined) {
|
||||
callback({ status: "success" });
|
||||
//call original handler if defined
|
||||
originalInput.oninput?.(e);
|
||||
return;
|
||||
}
|
||||
|
||||
callIsValid(originalInput, currentValue, checkValue as T);
|
||||
//call original handler if defined
|
||||
originalInput.oninput?.(e);
|
||||
};
|
||||
}
|
||||
|
||||
export type ValidationOptions<T> = (T extends string
|
||||
? Validation<T>
|
||||
: Validation<T> & {
|
||||
/** convert string input. For `number`s use `Number` constructor */
|
||||
inputValueConvert: (val: string) => T;
|
||||
}) & {
|
||||
/** optional callback is called for each change of the validation result */
|
||||
callback?: (result: ValidationResult) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* adds an 'InputIndicator` to the given `inputElement` and updates its status depending on the given validation
|
||||
* @param inputElement
|
||||
* @param options
|
||||
*/
|
||||
export function validateWithIndicator<T>(
|
||||
inputElement: HTMLInputElement,
|
||||
options: ValidationOptions<T>
|
||||
): void {
|
||||
//use indicator
|
||||
const indicator = new InputIndicator(inputElement, {
|
||||
success: {
|
||||
icon: "fa-check",
|
||||
level: 1,
|
||||
},
|
||||
failed: {
|
||||
icon: "fa-times",
|
||||
level: -1,
|
||||
},
|
||||
checking: {
|
||||
icon: "fa-circle-notch",
|
||||
spinIcon: true,
|
||||
level: 0,
|
||||
},
|
||||
});
|
||||
const callback = (result: ValidationResult): void => {
|
||||
if (result.status === "failed") {
|
||||
indicator.show(result.status, result.errorMessage);
|
||||
} else {
|
||||
indicator.show(result.status);
|
||||
}
|
||||
options.callback?.(result);
|
||||
};
|
||||
|
||||
const handler = createInputEventHandler(
|
||||
callback,
|
||||
options,
|
||||
"inputValueConvert" in options ? options.inputValueConvert : undefined
|
||||
);
|
||||
|
||||
inputElement.addEventListener("input", handler);
|
||||
}
|
||||
|
||||
export type ConfigInputOptions<K extends ConfigKey, T = ConfigType[K]> = {
|
||||
input: HTMLInputElement | null;
|
||||
configName: K;
|
||||
validation?: (T extends string
|
||||
? Omit<Validation<T>, "schema">
|
||||
: Omit<Validation<T>, "schema"> & {
|
||||
inputValueConvert: (val: string) => T;
|
||||
}) & {
|
||||
/**set to `true` to validate against the `ConfigSchema` */
|
||||
schema: boolean;
|
||||
/** optional callback is called for each change of the validation result */
|
||||
validationCallback?: (result: ValidationResult) => void;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds input event listeners to the given input element. On `focusOut` and when pressing `Enter` the current value is stored in the Config using `genericSet`.
|
||||
* Note: Config is not updated if the value has not changed.
|
||||
*
|
||||
* If validation is set, Adds input validation using `InputIndicator` to the given input element. Config is only updated if the value is valid.
|
||||
*
|
||||
*/
|
||||
export function handleConfigInput<T extends ConfigKey>({
|
||||
input,
|
||||
configName,
|
||||
validation,
|
||||
}: ConfigInputOptions<T, ConfigType[T]>): void {
|
||||
if (input === null) {
|
||||
throw new Error(`Failed to find input element for ${configName}`);
|
||||
}
|
||||
|
||||
const inputValueConvert =
|
||||
validation !== undefined && "inputValueConvert" in validation
|
||||
? validation.inputValueConvert
|
||||
: undefined;
|
||||
let status: ValidationResult["status"] = "checking";
|
||||
|
||||
if (validation !== undefined) {
|
||||
const schema = ConfigSchema.shape[configName] as ZodType;
|
||||
|
||||
validateWithIndicator(input, {
|
||||
schema: validation.schema ? schema : undefined,
|
||||
//@ts-expect-error this is fine
|
||||
isValid: validation.isValid,
|
||||
inputValueConvert,
|
||||
callback: (result) => {
|
||||
status = result.status;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const handleStore = (): void => {
|
||||
if (input.value === "") {
|
||||
//use last config value, clear validation
|
||||
input.value = new String(Config[configName]).toString();
|
||||
input.dispatchEvent(new Event("input"));
|
||||
}
|
||||
if (status === "failed") {
|
||||
const parent = $(input.parentElement as HTMLElement);
|
||||
parent
|
||||
.stop(true, true)
|
||||
.addClass("hasError")
|
||||
.animate({ undefined: 1 }, 500, () => {
|
||||
parent.removeClass("hasError");
|
||||
});
|
||||
return;
|
||||
}
|
||||
const value = (inputValueConvert?.(input.value) ??
|
||||
input.value) as ConfigType[T];
|
||||
|
||||
if (Config[configName] === value) {
|
||||
return;
|
||||
}
|
||||
const didConfigSave = UpdateConfig.genericSet(configName, value, false);
|
||||
|
||||
if (didConfigSave) {
|
||||
Notifications.add("Saved", 1, {
|
||||
duration: 1,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener("keypress", (e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleStore();
|
||||
}
|
||||
});
|
||||
input.addEventListener("focusout", (e) => handleStore());
|
||||
}
|
|
@ -1,31 +1,68 @@
|
|||
import { ConfigValue } from "@monkeytype/schemas/configs";
|
||||
import { Config as ConfigType, ConfigKey } from "@monkeytype/schemas/configs";
|
||||
|
||||
import Config from "../../config";
|
||||
import * as Notifications from "../notifications";
|
||||
import SlimSelect from "slim-select";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import {
|
||||
handleConfigInput,
|
||||
ConfigInputOptions,
|
||||
Validation,
|
||||
} from "../input-validation";
|
||||
|
||||
type Mode = "select" | "button" | "range";
|
||||
type Mode = "select" | "button" | "range" | "input";
|
||||
|
||||
export default class SettingsGroup<T extends ConfigValue> {
|
||||
public configName: string;
|
||||
export type SimpleValidation<T> = Omit<Validation<T>, "schema"> & {
|
||||
schema?: true;
|
||||
};
|
||||
|
||||
export default class SettingsGroup<K extends ConfigKey, T = ConfigType[K]> {
|
||||
public configName: K;
|
||||
public configFunction: (param: T, nosave?: boolean) => boolean;
|
||||
public mode: Mode;
|
||||
public setCallback?: () => void;
|
||||
public updateCallback?: () => void;
|
||||
private elements: Element[];
|
||||
private validation?: T extends string
|
||||
? SimpleValidation<T>
|
||||
: SimpleValidation<T> & {
|
||||
inputValueConvert: (val: string) => T;
|
||||
};
|
||||
|
||||
constructor(
|
||||
configName: string,
|
||||
configName: K,
|
||||
configFunction: (param: T, nosave?: boolean) => boolean,
|
||||
mode: Mode,
|
||||
setCallback?: () => void,
|
||||
updateCallback?: () => void
|
||||
options?: {
|
||||
setCallback?: () => void;
|
||||
updateCallback?: () => void;
|
||||
validation?: T extends string
|
||||
? SimpleValidation<T>
|
||||
: SimpleValidation<T> & {
|
||||
inputValueConvert: (val: string) => T;
|
||||
};
|
||||
}
|
||||
) {
|
||||
this.configName = configName;
|
||||
this.mode = mode;
|
||||
this.configFunction = configFunction;
|
||||
this.setCallback = setCallback;
|
||||
this.updateCallback = updateCallback;
|
||||
this.setCallback = options?.setCallback;
|
||||
this.updateCallback = options?.updateCallback;
|
||||
this.validation = options?.validation;
|
||||
|
||||
const convertValue = (value: string): T => {
|
||||
let typed = value as T;
|
||||
if (
|
||||
this.validation !== undefined &&
|
||||
"inputValueConvert" in this.validation
|
||||
) {
|
||||
typed = this.validation.inputValueConvert(value);
|
||||
}
|
||||
if (typed === "true") typed = true as T;
|
||||
if (typed === "false") typed = false as T;
|
||||
|
||||
return typed;
|
||||
};
|
||||
|
||||
if (this.mode === "select") {
|
||||
const el = document.querySelector(
|
||||
|
@ -68,8 +105,9 @@ export default class SettingsGroup<T extends ConfigValue> {
|
|||
) {
|
||||
return;
|
||||
}
|
||||
const value = button.getAttribute("data-config-value");
|
||||
if (value === undefined || value === "") {
|
||||
|
||||
let value = button.getAttribute("data-config-value");
|
||||
if (value === null || value === "") {
|
||||
console.error(
|
||||
`Failed to handle settings button click for ${configName}: data-${configName} is missing or empty.`
|
||||
);
|
||||
|
@ -79,14 +117,39 @@ export default class SettingsGroup<T extends ConfigValue> {
|
|||
);
|
||||
return;
|
||||
}
|
||||
let typed = value as T;
|
||||
if (typed === "true") typed = true as T;
|
||||
if (typed === "false") typed = false as T;
|
||||
|
||||
let typed = convertValue(value);
|
||||
this.setValue(typed);
|
||||
});
|
||||
}
|
||||
|
||||
this.elements = Array.from(els);
|
||||
} else if (this.mode === "input") {
|
||||
const input: HTMLInputElement | null = document.querySelector(`
|
||||
.pageSettings .section[data-config-name=${this.configName}] .inputs .inputAndButton input`);
|
||||
if (input === null) {
|
||||
throw new Error(`Failed to find input element for ${configName}`);
|
||||
}
|
||||
|
||||
let validation;
|
||||
if (this.validation !== undefined) {
|
||||
validation = {
|
||||
schema: this.validation.schema ?? false,
|
||||
isValid: this.validation.isValid,
|
||||
inputValueConvert:
|
||||
"inputValueConvert" in this.validation
|
||||
? this.validation.inputValueConvert
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
handleConfigInput({
|
||||
input,
|
||||
configName: this.configName,
|
||||
validation,
|
||||
} as ConfigInputOptions<K>);
|
||||
|
||||
this.elements = [input];
|
||||
} else if (this.mode === "range") {
|
||||
const el = document.querySelector(
|
||||
`.pageSettings .section[data-config-name=${this.configName}] input[type=range]`
|
||||
|
@ -126,18 +189,18 @@ export default class SettingsGroup<T extends ConfigValue> {
|
|||
this.updateUI();
|
||||
}
|
||||
|
||||
setValue(value: T): void {
|
||||
if (Config[this.configName as keyof typeof Config] === value) {
|
||||
return;
|
||||
setValue(value: T): boolean {
|
||||
if (Config[this.configName] === value) {
|
||||
return false;
|
||||
}
|
||||
this.configFunction(value);
|
||||
const didSet = this.configFunction(value);
|
||||
this.updateUI();
|
||||
if (this.setCallback) this.setCallback();
|
||||
return didSet;
|
||||
}
|
||||
|
||||
updateUI(valueOverride?: T): void {
|
||||
const newValue =
|
||||
valueOverride ?? (Config[this.configName as keyof typeof Config] as T);
|
||||
const newValue = valueOverride ?? (Config[this.configName] as T);
|
||||
|
||||
if (this.mode === "select") {
|
||||
const select = this.elements?.[0] as HTMLSelectElement | null | undefined;
|
||||
|
|
|
@ -478,6 +478,7 @@ list.updateName = new SimpleModal({
|
|||
|
||||
return checkNameResponse === 200 ? true : "Name not available";
|
||||
},
|
||||
debounceDelay: 1000,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,8 +4,12 @@ import { format as dateFormat } from "date-fns/format";
|
|||
import * as Loader from "../elements/loader";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as ConnectionState from "../states/connection";
|
||||
import { InputIndicator } from "../elements/input-indicator";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import {
|
||||
Validation,
|
||||
ValidationOptions,
|
||||
ValidationResult,
|
||||
validateWithIndicator as withValidation,
|
||||
} from "../elements/input-validation";
|
||||
|
||||
type CommonInput<TType, TValue> = {
|
||||
type: TType;
|
||||
|
@ -21,12 +25,7 @@ type CommonInput<TType, TValue> = {
|
|||
* If the schema is defined it is always checked first.
|
||||
* Only if the schema validaton is passed or missing the `isValid` method is called.
|
||||
*/
|
||||
validation?: {
|
||||
/**
|
||||
* Zod schema to validate the input value against.
|
||||
* The indicator will show the error messages from the schema.
|
||||
*/
|
||||
schema?: Zod.Schema<TValue>;
|
||||
validation?: Omit<Validation<string>, "isValid"> & {
|
||||
/**
|
||||
* Custom async validation method.
|
||||
* This is intended to be used for validations that cannot be handled with a Zod schema like server-side validations.
|
||||
|
@ -90,7 +89,7 @@ export type ExecReturn = {
|
|||
};
|
||||
|
||||
type FormInput = CommonInputType & {
|
||||
indicator?: InputIndicator;
|
||||
hasError?: boolean;
|
||||
currentValue: () => string;
|
||||
};
|
||||
type SimpleModalOptions = {
|
||||
|
@ -327,70 +326,23 @@ export class SimpleModal {
|
|||
};
|
||||
|
||||
if (input.validation !== undefined) {
|
||||
const indicator = new InputIndicator(element, {
|
||||
valid: {
|
||||
icon: "fa-check",
|
||||
level: 1,
|
||||
const options: ValidationOptions<string> = {
|
||||
schema: input.validation.schema ?? undefined,
|
||||
isValid:
|
||||
input.validation.isValid !== undefined
|
||||
? async (val: string) => {
|
||||
//@ts-expect-error this is fine
|
||||
return input.validation.isValid(val, this);
|
||||
}
|
||||
: undefined,
|
||||
|
||||
callback: (result: ValidationResult) => {
|
||||
input.hasError = result.status !== "success";
|
||||
},
|
||||
invalid: {
|
||||
icon: "fa-times",
|
||||
level: -1,
|
||||
},
|
||||
checking: {
|
||||
icon: "fa-circle-notch",
|
||||
spinIcon: true,
|
||||
level: 0,
|
||||
},
|
||||
});
|
||||
input.indicator = indicator;
|
||||
|
||||
const debouceIsValid = debounce(1000, async (value: string) => {
|
||||
const result = await input.validation?.isValid?.(value, this);
|
||||
|
||||
if (element.value !== value) {
|
||||
//value of the input has changed in the meantime. discard
|
||||
return;
|
||||
}
|
||||
|
||||
if (result === true) {
|
||||
indicator.show("valid");
|
||||
} else {
|
||||
indicator.show("invalid", result);
|
||||
}
|
||||
});
|
||||
|
||||
const validateInput = async (value: string): Promise<void> => {
|
||||
if (value === undefined || value === "") {
|
||||
indicator.hide();
|
||||
return;
|
||||
}
|
||||
if (input.validation?.schema !== undefined) {
|
||||
const schemaResult = input.validation.schema.safeParse(value);
|
||||
if (!schemaResult.success) {
|
||||
indicator.show(
|
||||
"invalid",
|
||||
schemaResult.error.errors.map((err) => err.message).join(", ")
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (input.validation?.isValid !== undefined) {
|
||||
indicator.show("checking");
|
||||
debouceIsValid(value);
|
||||
return;
|
||||
}
|
||||
|
||||
indicator.show("valid");
|
||||
debounceDelay: input.validation.debounceDelay,
|
||||
};
|
||||
|
||||
element.oninput = async (event) => {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
await validateInput(value);
|
||||
|
||||
//call original handler if defined
|
||||
input.oninput?.(event);
|
||||
};
|
||||
withValidation(element, options);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -399,7 +351,6 @@ export class SimpleModal {
|
|||
|
||||
exec(): void {
|
||||
if (!this.canClose) return;
|
||||
|
||||
if (
|
||||
this.inputs
|
||||
.filter((i) => i.hidden !== true && i.optional !== true)
|
||||
|
@ -409,7 +360,7 @@ export class SimpleModal {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.inputs.some((i) => i.indicator?.get() === "invalid")) {
|
||||
if (this.inputs.some((i) => i.hasError === true)) {
|
||||
Notifications.add("Please solve all validation errors", 0);
|
||||
return;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue