diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html
index c77a28756..43f57ed6f 100644
--- a/frontend/src/html/pages/settings.html
+++ b/frontend/src/html/pages/settings.html
@@ -256,9 +256,6 @@
step="1"
value=""
/>
-
@@ -288,9 +285,6 @@
step="1"
value=""
/>
-
@@ -322,9 +316,6 @@
step="1"
value=""
/>
-
@@ -792,9 +783,6 @@
step="1"
value=""
/>
-
@@ -1020,9 +1008,6 @@
min="10"
max="90"
/>
-
@@ -1131,9 +1116,6 @@
class="input"
min="0"
/>
-
@@ -1154,9 +1136,6 @@
class="input"
tabindex="0"
/>
-
diff --git a/frontend/src/styles/settings.scss b/frontend/src/styles/settings.scss
index b69433b8a..db93648cc 100644
--- a/frontend/src/styles/settings.scss
+++ b/frontend/src/styles/settings.scss
@@ -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 {
diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts
index d05b1e229..aa6f88f18 100644
--- a/frontend/src/ts/commandline/commandline.ts
+++ b/frontend/src/ts/commandline/commandline.ts
@@ -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 {
value: command.defaultValue?.() ?? "",
icon: command.icon ?? "fa-chevron-right",
};
+ if ("validation" in command && !handlersCache.has(command.id)) {
+ const commandWithValidation = command as CommandWithValidation;
+ 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["validation"]
-): Promise {
- 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 Promise>();
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;
-
- 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;
diff --git a/frontend/src/ts/commandline/types.ts b/frontend/src/ts/commandline/types.ts
index ad1c7ecbf..ed4de15d2 100644
--- a/frontend/src/ts/commandline/types.ts
+++ b/frontend/src/ts/commandline/types.ts
@@ -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 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;
- /**
- * 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;
- };
+ validation: Validation;
};
export type CommandsSubgroup = {
diff --git a/frontend/src/ts/elements/input-indicator.ts b/frontend/src/ts/elements/input-indicator.ts
index 6bdca8eb2..ab32e754a 100644
--- a/frontend/src/ts/elements/input-indicator.ts
+++ b/frontend/src/ts/elements/input-indicator.ts
@@ -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 {
diff --git a/frontend/src/ts/elements/input-validation.ts b/frontend/src/ts/elements/input-validation.ts
new file mode 100644
index 000000000..db71dde3e
--- /dev/null
+++ b/frontend/src/ts/elements/input-validation.ts
@@ -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 = {
+ /**
+ * Zod schema to validate the input value against.
+ * The indicator will show the error messages from the schema.
+ */
+ schema?: z.Schema;
+
+ /**
+ * 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;
+
+ /** 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(
+ callback: (result: ValidationResult) => void,
+ validation: Validation,
+ inputValueConvert?: (val: string) => T
+): (e: Event) => Promise {
+ 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 extends string
+ ? Validation
+ : Validation & {
+ /** 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(
+ inputElement: HTMLInputElement,
+ options: ValidationOptions
+): 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 = {
+ input: HTMLInputElement | null;
+ configName: K;
+ validation?: (T extends string
+ ? Omit, "schema">
+ : Omit, "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({
+ input,
+ configName,
+ validation,
+}: ConfigInputOptions): 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());
+}
diff --git a/frontend/src/ts/elements/settings/settings-group.ts b/frontend/src/ts/elements/settings/settings-group.ts
index e093626a2..c590bda6d 100644
--- a/frontend/src/ts/elements/settings/settings-group.ts
+++ b/frontend/src/ts/elements/settings/settings-group.ts
@@ -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 {
- public configName: string;
+export type SimpleValidation = Omit, "schema"> & {
+ schema?: true;
+};
+
+export default class SettingsGroup {
+ 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
+ : SimpleValidation & {
+ 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
+ : SimpleValidation & {
+ 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 {
) {
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 {
);
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);
+
+ 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 {
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;
diff --git a/frontend/src/ts/modals/simple-modals.ts b/frontend/src/ts/modals/simple-modals.ts
index 30a36ea45..df8442f9c 100644
--- a/frontend/src/ts/modals/simple-modals.ts
+++ b/frontend/src/ts/modals/simple-modals.ts
@@ -478,6 +478,7 @@ list.updateName = new SimpleModal({
return checkNameResponse === 200 ? true : "Name not available";
},
+ debounceDelay: 1000,
},
},
],
diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts
index 260b18c7b..1fc9eed60 100644
--- a/frontend/src/ts/pages/settings.ts
+++ b/frontend/src/ts/pages/settings.ts
@@ -20,12 +20,12 @@ import SlimSelect from "slim-select";
import * as Skeleton from "../utils/skeleton";
import * as CustomBackgroundFilter from "../elements/custom-background-filter";
import {
- ConfigValue,
CustomBackgroundSchema,
ThemeName,
CustomLayoutFluid,
FunboxName,
ConfigKeySchema,
+ ConfigKey,
} from "@monkeytype/schemas/configs";
import { getAllFunboxes, checkCompatibility } from "@monkeytype/funbox";
import { getActiveFunboxNames } from "../test/funbox/list";
@@ -39,14 +39,15 @@ import { LayoutName } from "@monkeytype/schemas/layouts";
import { LanguageGroupNames, LanguageGroups } from "../constants/languages";
import { Language } from "@monkeytype/schemas/languages";
import { z } from "zod";
+import { handleConfigInput } from "../elements/input-validation";
let settingsInitialized = false;
-type SettingsGroups = Record>;
+type SettingsGroups = Partial<{ [K in ConfigKey]: SettingsGroup }>;
let customLayoutFluidSelect: SlimSelect | undefined;
let customPolyglotSelect: SlimSelect | undefined;
-export const groups: SettingsGroups = {};
+export const groups: SettingsGroups = {};
const HighlightSchema = ConfigKeySchema.or(
z.enum([
@@ -71,396 +72,407 @@ async function initGroups(): Promise {
"smoothCaret",
UpdateConfig.setSmoothCaret,
"button"
- ) as SettingsGroup;
+ );
groups["codeUnindentOnBackspace"] = new SettingsGroup(
"codeUnindentOnBackspace",
UpdateConfig.setCodeUnindentOnBackspace,
"button"
- ) as SettingsGroup;
+ );
groups["difficulty"] = new SettingsGroup(
"difficulty",
UpdateConfig.setDifficulty,
"button"
- ) as SettingsGroup;
+ );
groups["quickRestart"] = new SettingsGroup(
"quickRestart",
UpdateConfig.setQuickRestartMode,
"button"
- ) as SettingsGroup;
+ );
groups["showAverage"] = new SettingsGroup(
"showAverage",
UpdateConfig.setShowAverage,
"button"
- ) as SettingsGroup;
+ );
groups["keymapMode"] = new SettingsGroup(
"keymapMode",
UpdateConfig.setKeymapMode,
"button",
- () => {
- groups["showLiveWpm"]?.updateUI();
- },
- () => {
- if (Config.keymapMode === "off") {
- $(".pageSettings .section[data-config-name='keymapStyle']").addClass(
- "hidden"
- );
- $(".pageSettings .section[data-config-name='keymapLayout']").addClass(
- "hidden"
- );
- $(
- ".pageSettings .section[data-config-name='keymapLegendStyle']"
- ).addClass("hidden");
- $(
- ".pageSettings .section[data-config-name='keymapShowTopRow']"
- ).addClass("hidden");
- $(".pageSettings .section[data-config-name='keymapSize']").addClass(
- "hidden"
- );
- } else {
- $(".pageSettings .section[data-config-name='keymapStyle']").removeClass(
- "hidden"
- );
- $(
- ".pageSettings .section[data-config-name='keymapLayout']"
- ).removeClass("hidden");
- $(
- ".pageSettings .section[data-config-name='keymapLegendStyle']"
- ).removeClass("hidden");
- $(
- ".pageSettings .section[data-config-name='keymapShowTopRow']"
- ).removeClass("hidden");
- $(".pageSettings .section[data-config-name='keymapSize']").removeClass(
- "hidden"
- );
- }
+ {
+ updateCallback: () => {
+ if (Config.keymapMode === "off") {
+ $(".pageSettings .section[data-config-name='keymapStyle']").addClass(
+ "hidden"
+ );
+ $(".pageSettings .section[data-config-name='keymapLayout']").addClass(
+ "hidden"
+ );
+ $(
+ ".pageSettings .section[data-config-name='keymapLegendStyle']"
+ ).addClass("hidden");
+ $(
+ ".pageSettings .section[data-config-name='keymapShowTopRow']"
+ ).addClass("hidden");
+ $(".pageSettings .section[data-config-name='keymapSize']").addClass(
+ "hidden"
+ );
+ } else {
+ $(
+ ".pageSettings .section[data-config-name='keymapStyle']"
+ ).removeClass("hidden");
+ $(
+ ".pageSettings .section[data-config-name='keymapLayout']"
+ ).removeClass("hidden");
+ $(
+ ".pageSettings .section[data-config-name='keymapLegendStyle']"
+ ).removeClass("hidden");
+ $(
+ ".pageSettings .section[data-config-name='keymapShowTopRow']"
+ ).removeClass("hidden");
+ $(
+ ".pageSettings .section[data-config-name='keymapSize']"
+ ).removeClass("hidden");
+ }
+ },
}
- ) as SettingsGroup;
- groups["keymapMatrix"] = new SettingsGroup(
+ );
+ groups["keymapStyle"] = new SettingsGroup(
"keymapStyle",
UpdateConfig.setKeymapStyle,
"button"
- ) as SettingsGroup;
+ );
groups["keymapLayout"] = new SettingsGroup(
"keymapLayout",
UpdateConfig.setKeymapLayout,
"select"
- ) as SettingsGroup;
+ );
groups["keymapLegendStyle"] = new SettingsGroup(
"keymapLegendStyle",
UpdateConfig.setKeymapLegendStyle,
"button"
- ) as SettingsGroup;
+ );
groups["keymapShowTopRow"] = new SettingsGroup(
"keymapShowTopRow",
UpdateConfig.setKeymapShowTopRow,
"button"
- ) as SettingsGroup;
+ );
groups["keymapSize"] = new SettingsGroup(
"keymapSize",
UpdateConfig.setKeymapSize,
"range"
- ) as SettingsGroup;
+ );
groups["showKeyTips"] = new SettingsGroup(
"showKeyTips",
UpdateConfig.setKeyTips,
"button"
- ) as SettingsGroup;
+ );
groups["freedomMode"] = new SettingsGroup(
"freedomMode",
UpdateConfig.setFreedomMode,
"button",
- () => {
- groups["confidenceMode"]?.updateUI();
+ {
+ setCallback: () => {
+ groups["confidenceMode"]?.updateUI();
+ },
}
- ) as SettingsGroup;
+ );
groups["strictSpace"] = new SettingsGroup(
"strictSpace",
UpdateConfig.setStrictSpace,
"button"
- ) as SettingsGroup;
+ );
groups["oppositeShiftMode"] = new SettingsGroup(
"oppositeShiftMode",
UpdateConfig.setOppositeShiftMode,
"button"
- ) as SettingsGroup;
+ );
groups["confidenceMode"] = new SettingsGroup(
"confidenceMode",
UpdateConfig.setConfidenceMode,
"button",
- () => {
- groups["freedomMode"]?.updateUI();
- groups["stopOnError"]?.updateUI();
+ {
+ setCallback: () => {
+ groups["freedomMode"]?.updateUI();
+ groups["stopOnError"]?.updateUI();
+ },
}
- ) as SettingsGroup;
+ );
groups["indicateTypos"] = new SettingsGroup(
"indicateTypos",
UpdateConfig.setIndicateTypos,
"button"
- ) as SettingsGroup;
+ );
groups["hideExtraLetters"] = new SettingsGroup(
"hideExtraLetters",
UpdateConfig.setHideExtraLetters,
"button"
- ) as SettingsGroup;
+ );
groups["blindMode"] = new SettingsGroup(
"blindMode",
UpdateConfig.setBlindMode,
"button"
- ) as SettingsGroup;
+ );
groups["quickEnd"] = new SettingsGroup(
"quickEnd",
UpdateConfig.setQuickEnd,
"button"
- ) as SettingsGroup;
+ );
groups["repeatQuotes"] = new SettingsGroup(
"repeatQuotes",
UpdateConfig.setRepeatQuotes,
"button"
- ) as SettingsGroup;
- groups["ads"] = new SettingsGroup(
- "ads",
- UpdateConfig.setAds,
- "button"
- ) as SettingsGroup;
+ );
+ groups["ads"] = new SettingsGroup("ads", UpdateConfig.setAds, "button");
groups["alwaysShowWordsHistory"] = new SettingsGroup(
"alwaysShowWordsHistory",
UpdateConfig.setAlwaysShowWordsHistory,
"button"
- ) as SettingsGroup;
+ );
groups["britishEnglish"] = new SettingsGroup(
"britishEnglish",
UpdateConfig.setBritishEnglish,
"button"
- ) as SettingsGroup;
+ );
groups["singleListCommandLine"] = new SettingsGroup(
"singleListCommandLine",
UpdateConfig.setSingleListCommandLine,
"button"
- ) as SettingsGroup;
+ );
groups["capsLockWarning"] = new SettingsGroup(
"capsLockWarning",
UpdateConfig.setCapsLockWarning,
"button"
- ) as SettingsGroup;
+ );
groups["flipTestColors"] = new SettingsGroup(
"flipTestColors",
UpdateConfig.setFlipTestColors,
"button"
- ) as SettingsGroup;
+ );
groups["showOutOfFocusWarning"] = new SettingsGroup(
"showOutOfFocusWarning",
UpdateConfig.setShowOutOfFocusWarning,
"button"
- ) as SettingsGroup;
+ );
groups["colorfulMode"] = new SettingsGroup(
"colorfulMode",
UpdateConfig.setColorfulMode,
"button"
- ) as SettingsGroup;
+ );
groups["startGraphsAtZero"] = new SettingsGroup(
"startGraphsAtZero",
UpdateConfig.setStartGraphsAtZero,
"button"
- ) as SettingsGroup;
+ );
groups["autoSwitchTheme"] = new SettingsGroup(
"autoSwitchTheme",
UpdateConfig.setAutoSwitchTheme,
"button"
- ) as SettingsGroup;
+ );
groups["randomTheme"] = new SettingsGroup(
"randomTheme",
UpdateConfig.setRandomTheme,
"button"
- ) as SettingsGroup;
+ );
groups["stopOnError"] = new SettingsGroup(
"stopOnError",
UpdateConfig.setStopOnError,
"button",
- () => {
- groups["confidenceMode"]?.updateUI();
+ {
+ setCallback: () => {
+ groups["confidenceMode"]?.updateUI();
+ },
}
- ) as SettingsGroup;
+ );
groups["soundVolume"] = new SettingsGroup(
"soundVolume",
UpdateConfig.setSoundVolume,
"range"
- ) as SettingsGroup;
+ );
groups["playTimeWarning"] = new SettingsGroup(
"playTimeWarning",
UpdateConfig.setPlayTimeWarning,
"button",
- () => {
- if (Config.playTimeWarning !== "off") void Sound.playTimeWarning();
+ {
+ setCallback: () => {
+ if (Config.playTimeWarning !== "off") void Sound.playTimeWarning();
+ },
}
- ) as SettingsGroup;
+ );
groups["playSoundOnError"] = new SettingsGroup(
"playSoundOnError",
UpdateConfig.setPlaySoundOnError,
"button",
- () => {
- if (Config.playSoundOnError !== "off") void Sound.playError();
+ {
+ setCallback: () => {
+ if (Config.playSoundOnError !== "off") void Sound.playError();
+ },
}
- ) as SettingsGroup;
+ );
groups["playSoundOnClick"] = new SettingsGroup(
"playSoundOnClick",
UpdateConfig.setPlaySoundOnClick,
"button",
- () => {
- if (Config.playSoundOnClick !== "off") void Sound.playClick("KeyQ");
+ {
+ setCallback: () => {
+ if (Config.playSoundOnClick !== "off") void Sound.playClick("KeyQ");
+ },
}
- ) as SettingsGroup;
+ );
groups["showAllLines"] = new SettingsGroup(
"showAllLines",
UpdateConfig.setShowAllLines,
"button"
- ) as SettingsGroup;
+ );
groups["paceCaret"] = new SettingsGroup(
"paceCaret",
UpdateConfig.setPaceCaret,
"button"
- ) as SettingsGroup;
+ );
groups["repeatedPace"] = new SettingsGroup(
"repeatedPace",
UpdateConfig.setRepeatedPace,
"button"
- ) as SettingsGroup;
+ );
groups["minWpm"] = new SettingsGroup(
"minWpm",
UpdateConfig.setMinWpm,
"button"
- ) as SettingsGroup;
+ );
groups["minAcc"] = new SettingsGroup(
"minAcc",
UpdateConfig.setMinAcc,
"button"
- ) as SettingsGroup;
+ );
groups["minBurst"] = new SettingsGroup(
"minBurst",
UpdateConfig.setMinBurst,
"button"
- ) as SettingsGroup;
+ );
groups["smoothLineScroll"] = new SettingsGroup(
"smoothLineScroll",
UpdateConfig.setSmoothLineScroll,
"button"
- ) as SettingsGroup;
+ );
groups["lazyMode"] = new SettingsGroup(
"lazyMode",
UpdateConfig.setLazyMode,
"button"
- ) as SettingsGroup;
+ );
groups["layout"] = new SettingsGroup(
"layout",
UpdateConfig.setLayout,
"select"
- ) as SettingsGroup;
+ );
groups["language"] = new SettingsGroup(
"language",
UpdateConfig.setLanguage,
"select"
- ) as SettingsGroup;
+ );
groups["fontSize"] = new SettingsGroup(
"fontSize",
UpdateConfig.setFontSize,
- "button"
- ) as SettingsGroup;
+ "input",
+ { validation: { schema: true, inputValueConvert: Number } }
+ );
groups["maxLineWidth"] = new SettingsGroup(
"maxLineWidth",
UpdateConfig.setMaxLineWidth,
- "button"
- ) as SettingsGroup;
+ "input",
+ { validation: { schema: true, inputValueConvert: Number } }
+ );
groups["caretStyle"] = new SettingsGroup(
"caretStyle",
UpdateConfig.setCaretStyle,
"button"
- ) as SettingsGroup;
+ );
groups["paceCaretStyle"] = new SettingsGroup(
"paceCaretStyle",
UpdateConfig.setPaceCaretStyle,
"button"
- ) as SettingsGroup;
+ );
groups["timerStyle"] = new SettingsGroup(
"timerStyle",
UpdateConfig.setTimerStyle,
"button"
- ) as SettingsGroup;
+ );
groups["liveSpeedStyle"] = new SettingsGroup(
"liveSpeedStyle",
UpdateConfig.setLiveSpeedStyle,
"button"
- ) as SettingsGroup;
+ );
groups["liveAccStyle"] = new SettingsGroup(
"liveAccStyle",
UpdateConfig.setLiveAccStyle,
"button"
- ) as SettingsGroup;
+ );
groups["liveBurstStyle"] = new SettingsGroup(
"liveBurstStyle",
UpdateConfig.setLiveBurstStyle,
"button"
- ) as SettingsGroup;
+ );
groups["highlightMode"] = new SettingsGroup(
"highlightMode",
UpdateConfig.setHighlightMode,
"button"
- ) as SettingsGroup;
+ );
groups["tapeMode"] = new SettingsGroup(
"tapeMode",
UpdateConfig.setTapeMode,
"button"
- ) as SettingsGroup;
+ );
groups["tapeMargin"] = new SettingsGroup(
"tapeMargin",
UpdateConfig.setTapeMargin,
- "button"
- ) as SettingsGroup;
+ "input",
+ { validation: { schema: true, inputValueConvert: Number } }
+ );
groups["timerOpacity"] = new SettingsGroup(
"timerOpacity",
UpdateConfig.setTimerOpacity,
"button"
- ) as SettingsGroup;
+ );
groups["timerColor"] = new SettingsGroup(
"timerColor",
UpdateConfig.setTimerColor,
"button"
- ) as SettingsGroup;
+ );
groups["fontFamily"] = new SettingsGroup(
"fontFamily",
UpdateConfig.setFontFamily,
"button",
- undefined,
- () => {
- const customButton = $(
- ".pageSettings .section[data-config-name='fontFamily'] .buttons button[data-config-value='custom']"
- );
+ {
+ updateCallback: () => {
+ const customButton = $(
+ ".pageSettings .section[data-config-name='fontFamily'] .buttons button[data-config-value='custom']"
+ );
- if (
- $(
- ".pageSettings .section[data-config-name='fontFamily'] .buttons .active"
- ).length === 0
- ) {
- customButton.addClass("active");
- customButton.text(`Custom (${Config.fontFamily.replace(/_/g, " ")})`);
- } else {
- customButton.text("Custom");
- }
+ if (
+ $(
+ ".pageSettings .section[data-config-name='fontFamily'] .buttons .active"
+ ).length === 0
+ ) {
+ customButton.addClass("active");
+ customButton.text(`Custom (${Config.fontFamily.replace(/_/g, " ")})`);
+ } else {
+ customButton.text("Custom");
+ }
+ },
}
- ) as SettingsGroup;
+ );
groups["alwaysShowDecimalPlaces"] = new SettingsGroup(
"alwaysShowDecimalPlaces",
UpdateConfig.setAlwaysShowDecimalPlaces,
"button"
- ) as SettingsGroup;
+ );
groups["typingSpeedUnit"] = new SettingsGroup(
"typingSpeedUnit",
UpdateConfig.setTypingSpeedUnit,
"button"
- ) as SettingsGroup;
+ );
groups["customBackgroundSize"] = new SettingsGroup(
"customBackgroundSize",
UpdateConfig.setCustomBackgroundSize,
"button"
- ) as SettingsGroup;
+ );
}
async function fillSettingsPage(): Promise {
@@ -641,6 +653,55 @@ async function fillSettingsPage(): Promise {
},
});
+ handleConfigInput({
+ input: document.querySelector(
+ ".pageSettings .section[data-config-name='minWpm'] input"
+ ),
+ configName: "minWpmCustomSpeed",
+ validation: {
+ schema: true,
+ inputValueConvert: (it) =>
+ getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(
+ new Number(it).valueOf()
+ ),
+ },
+ });
+
+ handleConfigInput({
+ input: document.querySelector(
+ ".pageSettings .section[data-config-name='minAcc'] input"
+ ),
+ configName: "minAccCustom",
+ validation: {
+ schema: true,
+ inputValueConvert: Number,
+ },
+ });
+
+ handleConfigInput({
+ input: document.querySelector(
+ ".pageSettings .section[data-config-name='minBurst'] input"
+ ),
+ configName: "minBurstCustomSpeed",
+ validation: {
+ schema: true,
+ inputValueConvert: (it) =>
+ getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(
+ new Number(it).valueOf()
+ ),
+ },
+ });
+
+ handleConfigInput({
+ input: document.querySelector(
+ ".pageSettings .section[data-config-name='paceCaret'] input"
+ ),
+ configName: "paceCaretCustomSpeed",
+ validation: {
+ schema: true,
+ inputValueConvert: Number,
+ },
+ });
setEventDisabled(true);
await initGroups();
@@ -749,15 +810,21 @@ function refreshPresetsSettingsSection(): void {
}
}
-export async function update(): Promise {
+export async function update(
+ options: {
+ eventKey?: ConfigEvent.ConfigEventKey;
+ } = {}
+): Promise {
if (Config.showKeyTips) {
$(".pageSettings .tip").removeClass("hidden");
} else {
$(".pageSettings .tip").addClass("hidden");
}
- for (const group of Object.keys(groups)) {
- groups[group]?.updateUI();
+ for (const group of Object.values(groups)) {
+ if ("updateUI" in group) {
+ group.updateUI();
+ }
}
refreshTagsSettingsSection();
@@ -769,25 +836,45 @@ export async function update(): Promise {
ThemePicker.setCustomInputs(true);
// ThemePicker.updateActiveButton();
- $(
- ".pageSettings .section[data-config-name='paceCaret'] input.customPaceCaretSpeed"
- ).val(
+ const setInputValue = (
+ key: ConfigKey,
+ query: string,
+ value: string | number
+ ): void => {
+ if (options.eventKey === undefined || options.eventKey === key) {
+ const element = document.querySelector(query) as HTMLInputElement;
+ if (element === null) {
+ throw new Error("Unknown input element " + query);
+ }
+
+ element.value = new String(value).toString();
+ element.dispatchEvent(new Event("input"));
+ }
+ };
+
+ setInputValue(
+ "paceCaret",
+ ".pageSettings .section[data-config-name='paceCaret'] input.customPaceCaretSpeed",
getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm(
Config.paceCaretCustomSpeed
)
);
- $(
- ".pageSettings .section[data-config-name='minWpm'] input.customMinWpmSpeed"
- ).val(
+ setInputValue(
+ "minWpmCustomSpeed",
+ ".pageSettings .section[data-config-name='minWpm'] input.customMinWpmSpeed",
getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm(Config.minWpmCustomSpeed)
);
- $(".pageSettings .section[data-config-name='minAcc'] input.customMinAcc").val(
+
+ setInputValue(
+ "minAccCustom",
+ ".pageSettings .section[data-config-name='minAcc'] input.customMinAcc",
Config.minAccCustom
);
- $(
- ".pageSettings .section[data-config-name='minBurst'] input.customMinBurst"
- ).val(
+
+ setInputValue(
+ "minBurstCustomSpeed",
+ ".pageSettings .section[data-config-name='minBurst'] input.customMinBurst",
getTypingSpeedUnit(Config.typingSpeedUnit).fromWpm(
Config.minBurstCustomSpeed
)
@@ -814,25 +901,35 @@ export async function update(): Promise {
}
updateCustomBackgroundRemoveButtonVisibility();
- $(".pageSettings .section[data-config-name='fontSize'] input").val(
+ setInputValue(
+ "fontSize",
+ ".pageSettings .section[data-config-name='fontSize'] input",
Config.fontSize
);
- $(".pageSettings .section[data-config-name='maxLineWidth'] input").val(
+ setInputValue(
+ "maxLineWidth",
+ ".pageSettings .section[data-config-name='maxLineWidth'] input",
Config.maxLineWidth
);
- $(".pageSettings .section[data-config-name='keymapSize'] input").val(
+ setInputValue(
+ "keymapSize",
+ ".pageSettings .section[data-config-name='keymapSize'] input",
Config.keymapSize
);
- $(".pageSettings .section[data-config-name='tapeMargin'] input").val(
+ setInputValue(
+ "tapeMargin",
+ ".pageSettings .section[data-config-name='tapeMargin'] input",
Config.tapeMargin
);
- $(
- ".pageSettings .section[data-config-name='customBackgroundSize'] input"
- ).val(Config.customBackground);
+ setInputValue(
+ "customBackground",
+ ".pageSettings .section[data-config-name='customBackgroundSize'] input",
+ Config.customBackground
+ );
if (isAuthenticated()) {
showAccountSection();
@@ -907,130 +1004,6 @@ function updateCustomBackgroundRemoveButtonVisibility(): void {
}
}
-$(".pageSettings .section[data-config-name='paceCaret']").on(
- "focusout",
- "input.customPaceCaretSpeed",
- () => {
- const inputValue = parseInt(
- $(
- ".pageSettings .section[data-config-name='paceCaret'] input.customPaceCaretSpeed"
- ).val() as string
- );
- const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(
- inputValue
- );
- UpdateConfig.setPaceCaretCustomSpeed(newConfigValue);
- }
-);
-
-$(".pageSettings .section[data-config-name='paceCaret']").on(
- "click",
- "button.save",
- () => {
- const inputValue = parseInt(
- $(
- ".pageSettings .section[data-config-name='paceCaret'] input.customPaceCaretSpeed"
- ).val() as string
- );
- const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(
- inputValue
- );
- UpdateConfig.setPaceCaretCustomSpeed(newConfigValue);
- }
-);
-
-$(".pageSettings .section[data-config-name='minWpm']").on(
- "focusout",
- "input.customMinWpmSpeed",
- () => {
- const inputValue = parseInt(
- $(
- ".pageSettings .section[data-config-name='minWpm'] input.customMinWpmSpeed"
- ).val() as string
- );
- const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(
- inputValue
- );
- UpdateConfig.setMinWpmCustomSpeed(newConfigValue);
- }
-);
-
-$(".pageSettings .section[data-config-name='minWpm']").on(
- "click",
- "button.save",
- () => {
- const inputValue = parseInt(
- $(
- ".pageSettings .section[data-config-name='minWpm'] input.customMinWpmSpeed"
- ).val() as string
- );
- const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(
- inputValue
- );
- UpdateConfig.setMinWpmCustomSpeed(newConfigValue);
- }
-);
-
-$(".pageSettings .section[data-config-name='minAcc']").on(
- "focusout",
- "input.customMinAcc",
- () => {
- UpdateConfig.setMinAccCustom(
- parseInt(
- $(
- ".pageSettings .section[data-config-name='minAcc'] input.customMinAcc"
- ).val() as string
- )
- );
- }
-);
-
-$(".pageSettings .section[data-config-name='minAcc']").on(
- "click",
- "button.save",
- () => {
- UpdateConfig.setMinAccCustom(
- parseInt(
- $(
- ".pageSettings .section[data-config-name='minAcc'] input.customMinAcc"
- ).val() as string
- )
- );
- }
-);
-
-$(".pageSettings .section[data-config-name='minBurst']").on(
- "focusout",
- "input.customMinBurst",
- () => {
- const inputValue = parseInt(
- $(
- ".pageSettings .section[data-config-name='minBurst'] input.customMinBurst"
- ).val() as string
- );
- const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(
- inputValue
- );
- UpdateConfig.setMinBurstCustomSpeed(newConfigValue);
- }
-);
-
-$(".pageSettings .section[data-config-name='minBurst']").on(
- "click",
- "button.save",
- () => {
- const inputValue = parseInt(
- $(
- ".pageSettings .section[data-config-name='minBurst'] input.customMinBurst"
- ).val() as string
- );
- const newConfigValue = getTypingSpeedUnit(Config.typingSpeedUnit).toWpm(
- inputValue
- );
- UpdateConfig.setMinBurstCustomSpeed(newConfigValue);
- }
-);
-
//funbox
$(".pageSettings .section[data-config-name='funbox'] .buttons").on(
"click",
@@ -1133,131 +1106,6 @@ $(
}
});
-$(
- ".pageSettings .section[data-config-name='fontSize'] .inputAndButton button.save"
-).on("click", () => {
- const didConfigSave = UpdateConfig.setFontSize(
- parseFloat(
- $(
- ".pageSettings .section[data-config-name='fontSize'] .inputAndButton input"
- ).val() as string
- )
- );
- if (didConfigSave) {
- Notifications.add("Saved", 1, {
- duration: 1,
- });
- }
-});
-
-$(
- ".pageSettings .section[data-config-name='fontSize'] .inputAndButton input"
-).on("keypress", (e) => {
- if (e.key === "Enter") {
- const didConfigSave = UpdateConfig.setFontSize(
- parseFloat(
- $(
- ".pageSettings .section[data-config-name='fontSize'] .inputAndButton input"
- ).val() as string
- )
- );
- if (didConfigSave) {
- Notifications.add("Saved", 1, {
- duration: 1,
- });
- }
- }
-});
-
-$(
- ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton button.save"
-).on("click", () => {
- const didConfigSave = UpdateConfig.setTapeMargin(
- parseFloat(
- $(
- ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input"
- ).val() as string
- )
- );
- if (didConfigSave) {
- Notifications.add("Saved", 1, {
- duration: 1,
- });
- }
-});
-
-$(
- ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input"
-).on("keypress", (e) => {
- if (e.key === "Enter") {
- const didConfigSave = UpdateConfig.setTapeMargin(
- parseFloat(
- $(
- ".pageSettings .section[data-config-name='tapeMargin'] .inputAndButton input"
- ).val() as string
- )
- );
- if (didConfigSave) {
- Notifications.add("Saved", 1, {
- duration: 1,
- });
- }
- }
-});
-
-$(
- ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton button.save"
-).on("click", () => {
- const didConfigSave = UpdateConfig.setMaxLineWidth(
- parseFloat(
- $(
- ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton input"
- ).val() as string
- )
- );
- if (didConfigSave) {
- Notifications.add("Saved", 1, {
- duration: 1,
- });
- }
-});
-
-$(
- ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton input"
-).on("focusout", () => {
- const didConfigSave = UpdateConfig.setMaxLineWidth(
- parseFloat(
- $(
- ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton input"
- ).val() as string
- )
- );
- if (didConfigSave) {
- Notifications.add("Saved", 1, {
- duration: 1,
- });
- }
-});
-
-$(
- ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton input"
-).on("keypress", (e) => {
- if (e.key === "Enter") {
- const didConfigSave = UpdateConfig.setMaxLineWidth(
- parseFloat(
- $(
- ".pageSettings .section[data-config-name='maxLineWidth'] .inputAndButton input"
- ).val() as string
- )
- );
- if (didConfigSave) {
- Notifications.add("Saved", 1, {
- duration: 1,
- });
- }
- }
-});
-
$(
".pageSettings .section[data-config-name='keymapSize'] .inputAndButton button.save"
).on("click", () => {
@@ -1421,7 +1269,7 @@ ConfigEvent.subscribe((eventKey, eventValue) => {
//make sure the page doesnt update a billion times when applying a preset/config at once
if (configEventDisabled || eventKey === "saveToLocalStorage") return;
if (ActivePage.get() === "settings" && eventKey !== "theme") {
- void update();
+ void update({ eventKey });
}
});
diff --git a/frontend/src/ts/utils/simple-modal.ts b/frontend/src/ts/utils/simple-modal.ts
index 0679e75bc..bd5200ba7 100644
--- a/frontend/src/ts/utils/simple-modal.ts
+++ b/frontend/src/ts/utils/simple-modal.ts
@@ -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 = {
type: TType;
@@ -21,12 +25,7 @@ type CommonInput = {
* 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;
+ validation?: Omit, "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 = {
+ 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 => {
- 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;
}