Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 61 additions & 66 deletions frontend/src/ts/elements/input-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,80 +142,75 @@ export type ValidationOptions<T> = (T extends string
callback?: (result: ValidationResult) => void;
};

export type ValidatedHtmlInputElement = HTMLInputElement & {
getValidationResult: () => ValidationResult;
setValue: (val: string | null) => void;
triggerValidation: () => 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>
): ValidatedHtmlInputElement {
//use indicator
const indicator = new InputIndicator(inputElement, {
success: {
icon: "fa-check",
level: 1,
},
failed: {
icon: "fa-times",
level: -1,
},
warning: {
icon: "fa-exclamation-triangle",
level: 1,
},
checking: {
icon: "fa-circle-notch",
spinIcon: true,
level: 0,
},
});

let currentStatus: ValidationResult = {
export class ValidatedHtmlInputElement<T = string> {
public native: HTMLInputElement;
private indicator: InputIndicator;
private currentStatus: ValidationResult = {
status: "checking",
};
const callback = (result: ValidationResult): void => {
currentStatus = result;
if (result.status === "failed" || result.status === "warning") {
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
);
constructor(inputElement: HTMLInputElement, options: ValidationOptions<T>) {
this.native = inputElement;

inputElement.addEventListener("input", handler);
this.indicator = new InputIndicator(inputElement, {
success: {
icon: "fa-check",
level: 1,
},
failed: {
icon: "fa-times",
level: -1,
},
warning: {
icon: "fa-exclamation-triangle",
level: 1,
},
checking: {
icon: "fa-circle-notch",
spinIcon: true,
level: 0,
},
});

const result = inputElement as ValidatedHtmlInputElement;
result.getValidationResult = () => {
return currentStatus;
};
result.setValue = (val: string | null) => {
inputElement.value = val ?? "";
const callback = (result: ValidationResult): void => {
this.currentStatus = result;
if (result.status === "failed" || result.status === "warning") {
this.indicator.show(result.status, result.errorMessage);
} else {
this.indicator.show(result.status);
}
options.callback?.(result);
};

const handler = createInputEventHandler(
callback,
options,
"inputValueConvert" in options ? options.inputValueConvert : undefined
);

inputElement.addEventListener("input", handler);
}

getValidationResult(): ValidationResult {
return this.currentStatus;
}
setValue(val: string | null): this {
this.native.value = val ?? "";
if (val === null) {
indicator.hide();
currentStatus = { status: "checking" };
this.indicator.hide();
this.currentStatus = { status: "checking" };
} else {
inputElement.dispatchEvent(new Event("input"));
this.native.dispatchEvent(new Event("input"));
}
};
result.triggerValidation = () => {
inputElement.dispatchEvent(new Event("input"));
};

return result;
return this;
}
getValue(): string {
return this.native.value;
}
triggerValidation(): void {
this.native.dispatchEvent(new Event("input"));
}
}

export type ConfigInputOptions<K extends ConfigKey, T = ConfigType[K]> = {
Expand Down Expand Up @@ -260,7 +255,7 @@ export function handleConfigInput<T extends ConfigKey>({
if (validation !== undefined) {
const schema = ConfigSchema.shape[configName] as ZodType;

validateWithIndicator(input, {
new ValidatedHtmlInputElement(input, {
schema: validation.schema ? schema : undefined,
//@ts-expect-error this is fine
isValid: validation.isValid,
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/ts/elements/settings/fps-limit-section.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getfpsLimit, fpsLimitSchema, setfpsLimit } from "../../anim";
import { validateWithIndicator } from "../input-validation";
import { ValidatedHtmlInputElement } from "../input-validation";
import * as Notifications from "../notifications";

const section = document.querySelector(
Expand All @@ -10,7 +10,7 @@ const button = section.querySelector(
"button[data-fpsLimit='native']"
) as HTMLButtonElement;

const input = validateWithIndicator(
const input = new ValidatedHtmlInputElement(
section.querySelector('input[type="number"]') as HTMLInputElement,
{
schema: fpsLimitSchema,
Expand All @@ -24,7 +24,7 @@ export function update(): void {
input.setValue(null);
button.classList.add("active");
} else {
input.value = fpsLimit.toString();
input.setValue(fpsLimit.toString());
button.classList.remove("active");
}
}
Expand All @@ -38,7 +38,7 @@ function save(value: number): void {

function saveFromInput(): void {
if (input.getValidationResult().status !== "success") return;
const val = parseInt(input.value, 10);
const val = parseInt(input.getValue(), 10);
save(val);
}

Expand All @@ -47,10 +47,10 @@ button.addEventListener("click", () => {
update();
});

input.addEventListener("keypress", (e) => {
input.native.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
saveFromInput();
}
});

input.addEventListener("focusout", (e) => saveFromInput());
input.native.addEventListener("focusout", (e) => saveFromInput());
13 changes: 5 additions & 8 deletions frontend/src/ts/modals/edit-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ import {
} from "@monkeytype/schemas/configs";
import { getDefaultConfig } from "../constants/default-config";
import { SnapshotPreset } from "../constants/default-snapshot";
import {
ValidatedHtmlInputElement,
validateWithIndicator,
} from "../elements/input-validation";
import { ValidatedHtmlInputElement } from "../elements/input-validation";

const state = {
presetType: "full" as PresetType,
Expand All @@ -50,7 +47,7 @@ export function show(action: string, id?: string, name?: string): void {
$("#editPresetModal .modal .text").addClass("hidden");
addCheckBoxes();
if (!presetNameEl) {
presetNameEl = validateWithIndicator(
presetNameEl = new ValidatedHtmlInputElement(
document.querySelector(
"#editPresetModal .modal input"
) as HTMLInputElement,
Expand All @@ -64,7 +61,7 @@ export function show(action: string, id?: string, name?: string): void {
$("#editPresetModal .modal .popupTitle").html("Add new preset");
$("#editPresetModal .modal .submit").html(`add`);
presetNameEl?.setValue(null);
presetNameEl?.parentElement?.classList.remove("hidden");
presetNameEl?.native.parentElement?.classList.remove("hidden");
$("#editPresetModal .modal input").removeClass("hidden");
$(
"#editPresetModal .modal label.changePresetToCurrentCheckbox"
Expand All @@ -79,7 +76,7 @@ export function show(action: string, id?: string, name?: string): void {
$("#editPresetModal .modal .popupTitle").html("Edit preset");
$("#editPresetModal .modal .submit").html(`save`);
presetNameEl?.setValue(name);
presetNameEl?.parentElement?.classList.remove("hidden");
presetNameEl?.native.parentElement?.classList.remove("hidden");

$("#editPresetModal .modal input").removeClass("hidden");
$(
Expand Down Expand Up @@ -108,7 +105,7 @@ export function show(action: string, id?: string, name?: string): void {
$("#editPresetModal .modal .inputs").addClass("hidden");
$("#editPresetModal .modal .presetType").addClass("hidden");
$("#editPresetModal .modal .presetNameTitle").addClass("hidden");
presetNameEl?.parentElement?.classList.add("hidden");
presetNameEl?.native.parentElement?.classList.add("hidden");
}
updateUI();
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/ts/modals/google-sign-up.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as Loader from "../elements/loader";
import { subscribe as subscribeToSignUpEvent } from "../observables/google-sign-up-event";
import AnimatedModal from "../utils/animated-modal";
import { resetIgnoreAuthCallback } from "../firebase";
import { validateWithIndicator } from "../elements/input-validation";
import { ValidatedHtmlInputElement } from "../elements/input-validation";
import { UserNameSchema } from "@monkeytype/schemas/users";
import { remoteValidation } from "../utils/remote-validation";

Expand Down Expand Up @@ -153,7 +153,7 @@ function disableInput(): void {
nameInputEl.disabled = true;
}

validateWithIndicator(nameInputEl, {
new ValidatedHtmlInputElement(nameInputEl, {
schema: UserNameSchema,
isValid: remoteValidation(
async (name) => Ape.users.getNameAvailability({ params: { name } }),
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/ts/modals/save-custom-text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as CustomText from "../test/custom-text";
import * as Notifications from "../elements/notifications";
import * as CustomTextState from "../states/custom-text-name";
import AnimatedModal, { ShowOptions } from "../utils/animated-modal";
import { validateWithIndicator } from "../elements/input-validation";
import { ValidatedHtmlInputElement } from "../elements/input-validation";
import { z } from "zod";

type IncomingData = {
Expand All @@ -17,7 +17,7 @@ const state: State = {
textToSave: [],
};

const validatedInput = validateWithIndicator(
const validatedInput = new ValidatedHtmlInputElement(
$("#saveCustomTextModal .textName")[0] as HTMLInputElement,
{
debounceDelay: 500,
Expand Down
22 changes: 11 additions & 11 deletions frontend/src/ts/pages/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
UserEmailSchema,
UserNameSchema,
} from "@monkeytype/schemas/users";
import { validateWithIndicator } from "../elements/input-validation";
import { ValidatedHtmlInputElement } from "../elements/input-validation";
import { isDevEnvironment } from "../utils/misc";
import { z } from "zod";
import { remoteValidation } from "../utils/remote-validation";
Expand Down Expand Up @@ -72,7 +72,7 @@ export function getSignupData(): SignupData | false {
const nameInputEl = document.querySelector(
".page.pageLogin .register.side input.usernameInput"
) as HTMLInputElement;
validateWithIndicator(nameInputEl, {
new ValidatedHtmlInputElement(nameInputEl, {
schema: UserNameSchema,
isValid: remoteValidation(
async (name) => Ape.users.getNameAvailability({ params: { name } }),
Expand All @@ -90,7 +90,7 @@ let disposableEmailModule: typeof import("disposable-email-domains-js") | null =
null;
let moduleLoadAttempted = false;

const emailInputEl = validateWithIndicator(
const emailInputEl = new ValidatedHtmlInputElement(
document.querySelector(
".page.pageLogin .register.side input.emailInput"
) as HTMLInputElement,
Expand Down Expand Up @@ -143,7 +143,7 @@ const emailInputEl = validateWithIndicator(
}
);

emailInputEl.addEventListener("focus", async () => {
emailInputEl.native.addEventListener("focus", async () => {
if (!moduleLoadAttempted) {
moduleLoadAttempted = true;
try {
Expand All @@ -157,9 +157,9 @@ emailInputEl.addEventListener("focus", async () => {
const emailVerifyInputEl = document.querySelector(
".page.pageLogin .register.side input.verifyEmailInput"
) as HTMLInputElement;
validateWithIndicator(emailVerifyInputEl, {
new ValidatedHtmlInputElement(emailVerifyInputEl, {
isValid: async (emailVerify: string) => {
return emailInputEl.value === emailVerify
return emailInputEl.getValue() === emailVerify
? true
: "verify email not matching email";
},
Expand All @@ -168,13 +168,13 @@ validateWithIndicator(emailVerifyInputEl, {
registerForm.email =
emailInputEl.getValidationResult().status === "success" &&
result.status === "success"
? emailInputEl.value
? emailInputEl.getValue()
: undefined;
updateSignupButton();
},
});

const passwordInputEl = validateWithIndicator(
const passwordInputEl = new ValidatedHtmlInputElement(
document.querySelector(
".page.pageLogin .register.side .passwordInput"
) as HTMLInputElement,
Expand All @@ -192,9 +192,9 @@ const passwordInputEl = validateWithIndicator(
const passwordVerifyInputEl = document.querySelector(
".page.pageLogin .register.side .verifyPasswordInput"
) as HTMLInputElement;
validateWithIndicator(passwordVerifyInputEl, {
new ValidatedHtmlInputElement(passwordVerifyInputEl, {
isValid: async (passwordVerify: string) => {
return passwordInputEl.value === passwordVerify
return passwordInputEl.getValue() === passwordVerify
? true
: "verify password not matching password";
},
Expand All @@ -203,7 +203,7 @@ validateWithIndicator(passwordVerifyInputEl, {
registerForm.password =
passwordInputEl.getValidationResult().status === "success" &&
result.status === "success"
? passwordInputEl.value
? passwordInputEl.getValue()
: undefined;
updateSignupButton();
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/ts/utils/simple-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import * as Notifications from "../elements/notifications";
import * as ConnectionState from "../states/connection";
import {
IsValidResponse,
ValidatedHtmlInputElement,
Validation,
ValidationOptions,
ValidationResult,
validateWithIndicator as withValidation,
} from "../elements/input-validation";

type CommonInput<TType, TValue> = {
Expand Down Expand Up @@ -351,7 +351,7 @@ export class SimpleModal {
debounceDelay: input.validation.debounceDelay,
};

withValidation(element, options);
new ValidatedHtmlInputElement(element, options);
}
});

Expand Down