From 08f9e8aba5f5c80c18f6efe17cf3bae56120f20b Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 1 Jan 2026 22:27:27 +0100 Subject: [PATCH 01/23] wip --- frontend/src/ts/commandline/commandline.ts | 40 +++++------ frontend/src/ts/modals/cookies.ts | 66 +++++++++---------- .../src/ts/modals/custom-test-duration.ts | 10 +-- frontend/src/ts/modals/edit-profile.ts | 2 +- frontend/src/ts/modals/forgot-password.ts | 17 +++-- .../src/ts/modals/import-export-settings.ts | 14 ++-- .../src/ts/modals/last-signed-out-result.ts | 22 +++---- frontend/src/ts/utils/animated-modal.ts | 59 +++++++++-------- frontend/src/ts/utils/dom.ts | 47 +++++++++++++ 9 files changed, 161 insertions(+), 116 deletions(-) diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index f76a33b3d3f9..0a5acca873bb 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -582,12 +582,12 @@ function handleInputSubmit(): void { //validation ongoing, ignore the submit return; } else if (inputModeParams.validation?.status === "failed") { - modal.getModal().classList.add("hasError"); + modal.getModal().addClass("hasError"); if (shakeTimeout !== null) { clearTimeout(shakeTimeout); } shakeTimeout = setTimeout(() => { - modal.getModal().classList.remove("hasError"); + modal.getModal().removeClass("hasError"); }, 500); return; } @@ -739,45 +739,39 @@ async function decrementActiveIndex(): Promise { } function showWarning(message: string): void { - const warningEl = modal.getModal().querySelector(".warning"); - const warningTextEl = modal - .getModal() - .querySelector(".warning .text"); + const warningEl = modal.getModal().qs(".warning"); + const warningTextEl = modal.getModal().qs(".warning .text"); if (warningEl === null || warningTextEl === null) { throw new Error("Commandline warning element not found"); } - warningEl.classList.remove("hidden"); - warningTextEl.textContent = message; + warningEl.show(); + warningTextEl.setText(message); } const showCheckingIcon = debounce(200, async () => { - const checkingiconEl = modal - .getModal() - .querySelector(".checkingicon"); + const checkingiconEl = modal.getModal().qs(".checkingicon"); if (checkingiconEl === null) { throw new Error("Commandline checking icon element not found"); } - checkingiconEl.classList.remove("hidden"); + checkingiconEl.show(); }); function hideCheckingIcon(): void { showCheckingIcon.cancel({ upcomingOnly: true }); - const checkingiconEl = modal - .getModal() - .querySelector(".checkingicon"); + const checkingiconEl = modal.getModal().qs(".checkingicon"); if (checkingiconEl === null) { throw new Error("Commandline checking icon element not found"); } - checkingiconEl.classList.add("hidden"); + checkingiconEl.hide(); } function hideWarning(): void { - const warningEl = modal.getModal().querySelector(".warning"); + const warningEl = modal.getModal().qs(".warning"); if (warningEl === null) { throw new Error("Commandline warning element not found"); } - warningEl.classList.add("hidden"); + warningEl.hide(); } function updateValidationResult( @@ -829,9 +823,9 @@ const modal = new AnimatedModal({ focusFirstInput: true, }, setup: async (modalEl): Promise => { - const input = modalEl.querySelector("input") as HTMLInputElement; + const input = modalEl.qsr("input"); - input.addEventListener( + input.on( "input", debounce(50, async (e) => { inputValue = ((e as InputEvent).target as HTMLInputElement).value; @@ -851,7 +845,7 @@ const modal = new AnimatedModal({ }), ); - input.addEventListener("keydown", async (e) => { + input.on("keydown", async (e) => { mouseMode = false; if ( e.key === "ArrowUp" || @@ -907,7 +901,7 @@ const modal = new AnimatedModal({ } }); - input.addEventListener("input", async (e) => { + input.on("input", async (e) => { if ( inputModeParams === null || inputModeParams.command === null || @@ -926,7 +920,7 @@ const modal = new AnimatedModal({ await handler(e); }); - modalEl.addEventListener("mousemove", (_e) => { + modalEl.on("mousemove", (_e) => { mouseMode = true; }); diff --git a/frontend/src/ts/modals/cookies.ts b/frontend/src/ts/modals/cookies.ts index 91fc4403cc80..a8389ad16d3b 100644 --- a/frontend/src/ts/modals/cookies.ts +++ b/frontend/src/ts/modals/cookies.ts @@ -26,23 +26,21 @@ export function show(goToSettings?: boolean): void { } function showSettings(currentAcceptedCookies?: AcceptedCookies): void { - modal.getModal().querySelector(".main")?.classList.add("hidden"); - modal.getModal().querySelector(".settings")?.classList.remove("hidden"); + modal.getModal().qs(".main")?.hide(); + modal.getModal().qs(".settings")?.show(); if (currentAcceptedCookies) { if (currentAcceptedCookies.analytics) { - ( - modal - .getModal() - .querySelector(".cookie.analytics input") as HTMLInputElement - ).checked = true; + modal + .getModal() + .qs(".cookie.analytics input") + ?.setChecked(true); } if (currentAcceptedCookies.sentry) { - ( - modal - .getModal() - .querySelector(".cookie.sentry input") as HTMLInputElement - ).checked = true; + modal + .getModal() + .qs(".cookie.sentry input") + ?.setChecked(true); } } } @@ -64,7 +62,7 @@ const modal = new AnimatedModal({ // }, setup: async (modalEl): Promise => { - modalEl.querySelector(".acceptAll")?.addEventListener("click", () => { + modalEl.qs(".acceptAll")?.on("click", () => { const accepted = { security: true, analytics: true, @@ -73,7 +71,7 @@ const modal = new AnimatedModal({ setAcceptedCookies(accepted); void hide(); }); - modalEl.querySelector(".rejectAll")?.addEventListener("click", () => { + modalEl.qs(".rejectAll")?.on("click", () => { const accepted = { security: true, analytics: false, @@ -82,29 +80,27 @@ const modal = new AnimatedModal({ setAcceptedCookies(accepted); void hide(); }); - modalEl.querySelector(".openSettings")?.addEventListener("click", () => { + modalEl.qs(".openSettings")?.on("click", () => { showSettings(); }); - modalEl - .querySelector(".cookie.ads .textButton") - ?.addEventListener("click", () => { - try { - AdController.showConsentPopup(); - } catch (e) { - console.error("Failed to open ad consent UI"); - Notifications.add( - "Failed to open Ad consent popup. Do you have an ad or cookie popup blocker enabled?", - -1, - ); - } - }); - modalEl.querySelector(".acceptSelected")?.addEventListener("click", () => { - const analyticsChecked = ( - modalEl.querySelector(".cookie.analytics input") as HTMLInputElement - ).checked; - const sentryChecked = ( - modalEl.querySelector(".cookie.sentry input") as HTMLInputElement - ).checked; + modalEl.qs(".cookie.ads .textButton")?.on("click", () => { + try { + AdController.showConsentPopup(); + } catch (e) { + console.error("Failed to open ad consent UI"); + Notifications.add( + "Failed to open Ad consent popup. Do you have an ad or cookie popup blocker enabled?", + -1, + ); + } + }); + modalEl.qs(".acceptSelected")?.on("click", () => { + const analyticsChecked = + modalEl.qs(".cookie.analytics input")?.getChecked() ?? + false; + const sentryChecked = + modalEl.qs(".cookie.sentry input")?.getChecked() ?? + false; const accepted = { security: true, analytics: analyticsChecked, diff --git a/frontend/src/ts/modals/custom-test-duration.ts b/frontend/src/ts/modals/custom-test-duration.ts index 244524417a9c..53ca3aebba52 100644 --- a/frontend/src/ts/modals/custom-test-duration.ts +++ b/frontend/src/ts/modals/custom-test-duration.ts @@ -3,6 +3,7 @@ import * as ManualRestart from "../test/manual-restart-tracker"; import * as TestLogic from "../test/test-logic"; import * as Notifications from "../elements/notifications"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; +import { ElementWithUtils } from "../utils/dom"; function parseInput(input: string): number { const re = /((-\s*)?\d+(\.\d+)?\s*[hms]?)/g; @@ -73,8 +74,7 @@ export function show(showOptions?: ShowOptions): void { ...showOptions, focusFirstInput: "focusAndSelect", beforeAnimation: async (modalEl) => { - (modalEl.querySelector("input") as HTMLInputElement).value = - `${Config.time}`; + modalEl.qs("input")?.setValue(`${Config.time}`); previewDuration(); }, }); @@ -112,12 +112,12 @@ function apply(): void { hide(true); } -async function setup(modalEl: HTMLElement): Promise { - modalEl.addEventListener("submit", (e) => { +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.on("submit", (e) => { e.preventDefault(); apply(); }); - modalEl.querySelector("input")?.addEventListener("input", (e) => { + modalEl.qs("input")?.on("input", (e) => { previewDuration(); }); } diff --git a/frontend/src/ts/modals/edit-profile.ts b/frontend/src/ts/modals/edit-profile.ts index 3eed5708533f..c51438a0f740 100644 --- a/frontend/src/ts/modals/edit-profile.ts +++ b/frontend/src/ts/modals/edit-profile.ts @@ -240,7 +240,7 @@ function addValidation( const modal = new AnimatedModal({ dialogId: "editProfileModal", setup: async (modalEl): Promise => { - modalEl.addEventListener("submit", async (e) => { + modalEl.on("submit", async (e) => { e.preventDefault(); await updateProfile(); }); diff --git a/frontend/src/ts/modals/forgot-password.ts b/frontend/src/ts/modals/forgot-password.ts index cae09613f961..df754a4e7f0e 100644 --- a/frontend/src/ts/modals/forgot-password.ts +++ b/frontend/src/ts/modals/forgot-password.ts @@ -4,6 +4,7 @@ import Ape from "../ape/index"; import * as Notifications from "../elements/notifications"; import * as Loader from "../elements/loader"; import { UserEmailSchema } from "@monkeytype/schemas/users"; +import { ElementWithUtils } from "../utils/dom"; export function show(): void { if (!CaptchaController.isCaptchaAvailable()) { @@ -20,7 +21,7 @@ export function show(): void { beforeAnimation: async (modal) => { CaptchaController.reset("forgotPasswordModal"); CaptchaController.render( - modal.querySelector(".g-recaptcha") as HTMLElement, + modal.qsr(".g-recaptcha").native, "forgotPasswordModal", async () => { await submit(); @@ -37,11 +38,13 @@ async function submit(): Promise { return; } - const email = ( - modal.getModal().querySelector("input") as HTMLInputElement - ).value.trim(); + const email = modal + .getModal() + .qs("input") + ?.getValue() + ?.trim(); - if (!email) { + if (email === "" || email === null || email === undefined) { Notifications.add("Please enter your email address"); CaptchaController.reset("forgotPasswordModal"); return; @@ -79,8 +82,8 @@ function hide(): void { void modal.hide(); } -async function setup(modalEl: HTMLElement): Promise { - modalEl.querySelector("button")?.addEventListener("click", async () => { +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.qs("button")?.on("click", async () => { await submit(); }); } diff --git a/frontend/src/ts/modals/import-export-settings.ts b/frontend/src/ts/modals/import-export-settings.ts index d23ca53a5825..5d28ea700c7d 100644 --- a/frontend/src/ts/modals/import-export-settings.ts +++ b/frontend/src/ts/modals/import-export-settings.ts @@ -18,13 +18,13 @@ export function show(mode: "import" | "export", config?: string): void { void modal.show({ focusFirstInput: "focusAndSelect", beforeAnimation: async (modal) => { - (modal.querySelector("input") as HTMLInputElement).value = state.value; + modal.qs("input")?.setValue(state.value); if (state.mode === "export") { - modal.querySelector("button")?.classList.add("hidden"); - modal.querySelector("input")?.setAttribute("readonly", "true"); + modal.qs("button")?.hide(); + modal.qs("input")?.setAttribute("readonly", "true"); } else if (state.mode === "import") { - modal.querySelector("button")?.classList.remove("hidden"); - modal.querySelector("input")?.removeAttribute("readonly"); + modal.qs("button")?.show(); + modal.qs("input")?.removeAttribute("readonly"); } }, }); @@ -33,10 +33,10 @@ export function show(mode: "import" | "export", config?: string): void { const modal = new AnimatedModal({ dialogId: "importExportSettingsModal", setup: async (modalEl): Promise => { - modalEl.querySelector("input")?.addEventListener("input", (e) => { + modalEl.qs("input")?.on("input", (e) => { state.value = (e.target as HTMLInputElement).value; }); - modalEl?.addEventListener("submit", async (e) => { + modalEl?.on("submit", async (e) => { e.preventDefault(); if (state.mode !== "import") return; if (state.value === "") { diff --git a/frontend/src/ts/modals/last-signed-out-result.ts b/frontend/src/ts/modals/last-signed-out-result.ts index 5205f803b7d9..11bb845145c7 100644 --- a/frontend/src/ts/modals/last-signed-out-result.ts +++ b/frontend/src/ts/modals/last-signed-out-result.ts @@ -8,7 +8,7 @@ import { syncNotSignedInLastResult } from "../utils/results"; import * as AuthEvent from "../observables/auth-event"; function reset(): void { - (modal.getModal().querySelector(".result") as HTMLElement).innerHTML = ` + modal.getModal().qs(".result")?.setHtml(`
wpm
-
@@ -32,7 +32,7 @@ function reset(): void {
test type
-
-
`; +
`); } function fillData(): void { @@ -124,16 +124,14 @@ AuthEvent.subscribe((event) => { const modal = new AnimatedModal({ dialogId: "lastSignedOutResult", setup: async (modalEl): Promise => { - modalEl - .querySelector("button.save") - ?.addEventListener("click", async () => { - const user = getAuthenticatedUser(); - if (user !== null) { - void syncNotSignedInLastResult(user.uid); - } - hide(); - }); - modalEl.querySelector("button.discard")?.addEventListener("click", () => { + modalEl.qs("button.save")?.on("click", async () => { + const user = getAuthenticatedUser(); + if (user !== null) { + void syncNotSignedInLastResult(user.uid); + } + hide(); + }); + modalEl.qs("button.discard")?.on("click", () => { TestLogic.clearNotSignedInResult(); Notifications.add("Last test result discarded", 0); hide(); diff --git a/frontend/src/ts/utils/animated-modal.ts b/frontend/src/ts/utils/animated-modal.ts index 6400b6e2eb2c..fe0f3b730095 100644 --- a/frontend/src/ts/utils/animated-modal.ts +++ b/frontend/src/ts/utils/animated-modal.ts @@ -1,6 +1,7 @@ import { animate, AnimationParams } from "animejs"; import { applyReducedMotion, isPopupVisible } from "./misc"; import * as Skeleton from "./skeleton"; +import { ElementWithUtils, qs } from "./dom"; type CustomWrapperAndModalAnimations = { wrapper?: AnimationParams & { @@ -16,7 +17,10 @@ type ConstructorCustomAnimations = { hide?: CustomWrapperAndModalAnimations; }; -type Animation = (modal: HTMLElement, modalChainData?: T) => Promise; +type Animation = ( + modal: ElementWithUtils, + modalChainData?: T, +) => Promise; type ShowHideOptions = { animationMode?: "none" | "both" | "modalOnly"; @@ -45,7 +49,7 @@ type ConstructorParams = { showOptionsWhenInChain?: ShowOptions; customEscapeHandler?: (e: KeyboardEvent) => void; customWrapperClickHandler?: (e: MouseEvent) => void; - setup?: (modal: HTMLElement) => Promise; + setup?: (modal: ElementWithUtils) => Promise; cleanup?: () => Promise; }; @@ -56,8 +60,8 @@ export default class AnimatedModal< IncomingModalChainData = unknown, OutgoingModalChainData = unknown, > { - private wrapperEl: HTMLDialogElement; - private modalEl: HTMLElement; + private wrapperEl: ElementWithUtils; + private modalEl: ElementWithUtils; private dialogId: string; private open = false; private setupRan = false; @@ -71,7 +75,7 @@ export default class AnimatedModal< private customEscapeHandler: ((e: KeyboardEvent) => void) | undefined; private customWrapperClickHandler: ((e: MouseEvent) => void) | undefined; - private setup: ((modal: HTMLElement) => Promise) | undefined; + private setup: ((modal: ElementWithUtils) => Promise) | undefined; private cleanup: (() => Promise) | undefined; constructor(constructorParams: ConstructorParams) { @@ -84,10 +88,10 @@ export default class AnimatedModal< Skeleton.append(constructorParams.dialogId, this.skeletonAppendParent); } - const dialogElement = document.getElementById(constructorParams.dialogId); - const modalElement = document.querySelector( - `#${constructorParams.dialogId} > .modal`, - ) as HTMLElement; + const dialogElement = qs( + "#" + constructorParams.dialogId, + ); + const modalElement = qs(`#${constructorParams.dialogId} > .modal`); if (dialogElement === null) { throw new Error( @@ -133,7 +137,7 @@ export default class AnimatedModal< } async runSetup(): Promise { - this.wrapperEl.addEventListener("keydown", async (e) => { + this.wrapperEl.on("keydown", async (e) => { if (e.key === "Escape" && isPopupVisible(this.dialogId)) { e.preventDefault(); e.stopPropagation(); @@ -146,8 +150,8 @@ export default class AnimatedModal< } }); - this.wrapperEl.addEventListener("mousedown", async (e) => { - if (e.target === this.wrapperEl) { + this.wrapperEl.on("mousedown", async (e) => { + if (e.target === this.wrapperEl.native) { if (this.customWrapperClickHandler !== undefined) { this.customWrapperClickHandler(e); void this.cleanup?.(); @@ -166,11 +170,11 @@ export default class AnimatedModal< return this.dialogId; } - getWrapper(): HTMLDialogElement { + getWrapper(): ElementWithUtils { return this.wrapperEl; } - getModal(): HTMLElement { + getModal(): ElementWithUtils { return this.modalEl; } @@ -179,8 +183,10 @@ export default class AnimatedModal< } focusFirstInput(setting: true | "focusAndSelect" | undefined): void { - const inputs = [...this.modalEl.querySelectorAll("input")]; - const input = inputs.find((input) => !input.classList.contains("hidden")); + const inputs = [ + ...this.modalEl.qsa("input"), + ] as ElementWithUtils[]; + const input = inputs.find((input) => !input.hasClass("hidden")); if (input !== undefined && input !== null) { if (setting === true) { input.focus(); @@ -238,7 +244,7 @@ export default class AnimatedModal< if (options?.mode === "dialog") { this.wrapperEl.show(); } else if (options?.mode === "modal" || options?.mode === undefined) { - this.wrapperEl.showModal(); + this.wrapperEl.native.showModal(); } await options?.beforeAnimation?.(this.modalEl, options?.modalChainData); @@ -279,14 +285,14 @@ export default class AnimatedModal< duration: animationMode === "none" ? 0 : modalAnimationDuration, }); } else { - this.modalEl.style.opacity = "1"; + this.modalEl.setStyle({ opacity: "1" }); } animate(this.wrapperEl, { ...wrapperAnimation, duration: animationMode === "none" ? 0 : wrapperAnimationDuration, onBegin: () => { - this.wrapperEl.classList.remove("hidden"); + this.wrapperEl.show(); }, onComplete: async () => { this.focusFirstInput(options?.focusFirstInput); @@ -298,7 +304,8 @@ export default class AnimatedModal< }, }); } else if (animationMode === "modalOnly") { - $(this.wrapperEl).removeClass("hidden").css("opacity", "1"); + this.wrapperEl.setStyle({ opacity: "1" }); + this.wrapperEl.show(); animate(this.modalEl, { ...modalAnimation, @@ -369,15 +376,15 @@ export default class AnimatedModal< duration: animationMode === "none" ? 0 : modalAnimationDuration, }); } else { - this.modalEl.style.opacity = "1"; + this.modalEl.setStyle({ opacity: "1" }); } animate(this.wrapperEl, { ...wrapperAnimation, duration: animationMode === "none" ? 0 : wrapperAnimationDuration, onComplete: async () => { - this.wrapperEl.close(); - this.wrapperEl.classList.add("hidden"); + this.wrapperEl.native.close(); + this.wrapperEl.hide(); Skeleton.remove(this.dialogId); this.open = false; await options?.afterAnimation?.(this.modalEl); @@ -407,7 +414,7 @@ export default class AnimatedModal< ...modalAnimation, duration: modalAnimationDuration, onComplete: async () => { - this.wrapperEl.close(); + this.wrapperEl.native.close(); $(this.wrapperEl).addClass("hidden").css("opacity", "0"); Skeleton.remove(this.dialogId); this.open = false; @@ -436,8 +443,8 @@ export default class AnimatedModal< } destroy(): void { - this.wrapperEl.close(); - this.wrapperEl.classList.add("hidden"); + this.wrapperEl.native.close(); + this.wrapperEl.hide(); void this.cleanup?.(); Skeleton.remove(this.dialogId); this.open = false; diff --git a/frontend/src/ts/utils/dom.ts b/frontend/src/ts/utils/dom.ts index 9b0d5626302f..a11aa1497b1b 100644 --- a/frontend/src/ts/utils/dom.ts +++ b/frontend/src/ts/utils/dom.ts @@ -105,6 +105,8 @@ type ElementWithValue = | HTMLTextAreaElement | HTMLSelectElement; +type ElementWithSelectableValue = HTMLInputElement | HTMLTextAreaElement; + //TODO: after the migration from jQuery to dom-utils we might want to add currentTarget back to the event object, if we have a use-case for it. // For now we remove it because currentTarget is not the same element when using dom-utils intead of jQuery to get compile errors. export type OnChildEvent = Omit & { @@ -521,6 +523,13 @@ export class ElementWithUtils { ); } + private hasSelectableValue(): this is ElementWithUtils { + return ( + this.native instanceof HTMLInputElement || + this.native instanceof HTMLTextAreaElement + ); + } + /** * Set value of input or textarea to a string. */ @@ -542,6 +551,28 @@ export class ElementWithUtils { return undefined; } + /** + * Set checked state of input element + * @param checked The checked state to set + */ + setChecked(this: ElementWithUtils, checked: boolean): this { + if (this.native instanceof HTMLInputElement) { + this.native.checked = checked; + } + return this as unknown as this; + } + + /** + * Get checked state of input element + * @returns The checked state of the element, or undefined if the element is not an input. + */ + getChecked(this: ElementWithUtils): boolean | undefined { + if (this.native instanceof HTMLInputElement) { + return this.native.checked; + } + return undefined; + } + /** * Get the parent element */ @@ -635,6 +666,22 @@ export class ElementWithUtils { }); }); } + + /** + * Focus the element + */ + focus(): void { + this.native.focus(); + } + + /** + * Select the element's content (for input and textarea elements) + */ + select(this: ElementWithUtils): void { + if (this.hasSelectableValue()) { + this.native.select(); + } + } } /** From ef1abd0371be678c706392fd1d145e63d30dac99 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 1 Jan 2026 23:03:42 +0100 Subject: [PATCH 02/23] brr --- frontend/src/ts/modals/custom-generator.ts | 12 +- frontend/src/ts/modals/custom-text.ts | 141 +++++++----------- frontend/src/ts/modals/custom-word-amount.ts | 7 +- frontend/src/ts/modals/dev-options.ts | 87 +++++------ frontend/src/ts/modals/edit-preset.ts | 10 +- frontend/src/ts/modals/edit-result-tags.ts | 22 ++- frontend/src/ts/modals/google-sign-up.ts | 6 +- frontend/src/ts/modals/mobile-test-config.ts | 31 ++-- frontend/src/ts/modals/pb-tables.ts | 12 +- frontend/src/ts/modals/practise-words.ts | 20 ++- frontend/src/ts/modals/quote-approve.ts | 5 +- frontend/src/ts/modals/quote-rate.ts | 13 +- frontend/src/ts/modals/quote-report.ts | 6 +- frontend/src/ts/modals/quote-search.ts | 137 ++++++++--------- frontend/src/ts/modals/quote-submit.ts | 6 +- frontend/src/ts/modals/register-captcha.ts | 2 +- frontend/src/ts/modals/save-custom-text.ts | 8 +- frontend/src/ts/modals/share-custom-theme.ts | 11 +- frontend/src/ts/modals/share-test-settings.ts | 15 +- frontend/src/ts/modals/streak-hour-offset.ts | 27 ++-- frontend/src/ts/modals/support.ts | 2 +- frontend/src/ts/modals/user-report.ts | 15 +- frontend/src/ts/modals/word-filter.ts | 10 +- frontend/src/ts/utils/simple-modal.ts | 20 ++- 24 files changed, 288 insertions(+), 337 deletions(-) diff --git a/frontend/src/ts/modals/custom-generator.ts b/frontend/src/ts/modals/custom-generator.ts index 7c4d77fba5a4..fd3898dc7a58 100644 --- a/frontend/src/ts/modals/custom-generator.ts +++ b/frontend/src/ts/modals/custom-generator.ts @@ -83,7 +83,7 @@ export async function show(showOptions?: ShowOptions): Promise { _presetSelect = new SlimSelect({ select: "#customGeneratorModal .presetInput", settings: { - contentLocation: modalEl, + contentLocation: modalEl.native, }, }); }, @@ -159,16 +159,18 @@ async function apply(set: boolean): Promise { }); } -async function setup(modalEl: HTMLElement): Promise { - modalEl.querySelector(".setButton")?.addEventListener("click", () => { +async function setup( + modalEl: import("../utils/dom").ElementWithUtils, +): Promise { + modalEl.qs(".setButton")?.on("click", () => { void apply(true); }); - modalEl.querySelector(".addButton")?.addEventListener("click", () => { + modalEl.qs(".addButton")?.on("click", () => { void apply(false); }); - modalEl.querySelector(".generateButton")?.addEventListener("click", () => { + modalEl.qs(".generateButton")?.on("click", () => { applyPreset(); }); } diff --git a/frontend/src/ts/modals/custom-text.ts b/frontend/src/ts/modals/custom-text.ts index f7f81c772f59..8bb29b65ffb0 100644 --- a/frontend/src/ts/modals/custom-text.ts +++ b/frontend/src/ts/modals/custom-text.ts @@ -1,3 +1,4 @@ +import { ElementWithUtils } from "../utils/dom"; import * as CustomText from "../test/custom-text"; import * as CustomTextState from "../states/custom-text-name"; import * as ManualRestart from "../test/manual-restart-tracker"; @@ -166,7 +167,7 @@ function updateUI(): void { } async function beforeAnimation( - modalEl: HTMLElement, + modalEl: ElementWithUtils, modalChainData?: IncomingData, ): Promise { state.customTextMode = CustomText.getMode(); @@ -404,14 +405,12 @@ function handleDelimiterChange(): void { state.textarea = newtext; } -async function setup(modalEl: HTMLElement): Promise { - modalEl - .querySelector("#fileInput") - ?.addEventListener("change", handleFileOpen); +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.qs("#fileInput")?.on("change", handleFileOpen); - const buttons = modalEl.querySelectorAll(".group[data-id='mode'] button"); + const buttons = modalEl.qsa(".group[data-id='mode'] button"); for (const button of buttons) { - button.addEventListener("click", (e) => { + button.on("click", (e) => { state.customTextMode = (e.target as HTMLButtonElement).value as | "simple" | "repeat" @@ -426,40 +425,32 @@ async function setup(modalEl: HTMLElement): Promise { }); } - for (const button of modalEl.querySelectorAll( - ".group[data-id='fancy'] button", - )) { - button.addEventListener("click", (e) => { + for (const button of modalEl.qsa(".group[data-id='fancy'] button")) { + button.on("click", (e: MouseEvent) => { state.removeFancyTypographyEnabled = (e.target as HTMLButtonElement).value === "true"; updateUI(); }); } - for (const button of modalEl.querySelectorAll( - ".group[data-id='control'] button", - )) { - button.addEventListener("click", (e) => { + for (const button of modalEl.qsa(".group[data-id='control'] button")) { + button.on("click", (e: MouseEvent) => { state.replaceControlCharactersEnabled = (e.target as HTMLButtonElement).value === "true"; updateUI(); }); } - for (const button of modalEl.querySelectorAll( - ".group[data-id='zeroWidth'] button", - )) { - button.addEventListener("click", (e) => { + for (const button of modalEl.qsa(".group[data-id='zeroWidth'] button")) { + button.on("click", (e: MouseEvent) => { state.removeZeroWidthCharactersEnabled = (e.target as HTMLButtonElement).value === "true"; updateUI(); }); } - for (const button of modalEl.querySelectorAll( - ".group[data-id='delimiter'] button", - )) { - button.addEventListener("click", (e) => { + for (const button of modalEl.qsa(".group[data-id='delimiter'] button")) { + button.on("click", (e: MouseEvent) => { state.customTextPipeDelimiter = (e.target as HTMLButtonElement).value === "true"; if (state.customTextPipeDelimiter && state.customTextLimits.word !== "") { @@ -476,10 +467,8 @@ async function setup(modalEl: HTMLElement): Promise { }); } - for (const button of modalEl.querySelectorAll( - ".group[data-id='newlines'] button", - )) { - button.addEventListener("click", (e) => { + for (const button of modalEl.qsa(".group[data-id='newlines'] button")) { + button.on("click", (e: MouseEvent) => { state.replaceNewlines = (e.target as HTMLButtonElement).value as | "off" | "space" @@ -488,38 +477,32 @@ async function setup(modalEl: HTMLElement): Promise { }); } - modalEl - .querySelector(".group[data-id='limit'] input.words") - ?.addEventListener("input", (e) => { - state.customTextLimits.word = (e.target as HTMLInputElement).value; - state.customTextLimits.time = ""; - state.customTextLimits.section = ""; - updateUI(); - }); + modalEl.qs(".group[data-id='limit'] input.words")?.on("input", (e) => { + state.customTextLimits.word = (e.target as HTMLInputElement).value; + state.customTextLimits.time = ""; + state.customTextLimits.section = ""; + updateUI(); + }); - modalEl - .querySelector(".group[data-id='limit'] input.time") - ?.addEventListener("input", (e) => { - state.customTextLimits.time = (e.target as HTMLInputElement).value; - state.customTextLimits.word = ""; - state.customTextLimits.section = ""; - updateUI(); - }); + modalEl.qs(".group[data-id='limit'] input.time")?.on("input", (e) => { + state.customTextLimits.time = (e.target as HTMLInputElement).value; + state.customTextLimits.word = ""; + state.customTextLimits.section = ""; + updateUI(); + }); - modalEl - .querySelector(".group[data-id='limit'] input.sections") - ?.addEventListener("input", (e) => { - state.customTextLimits.section = (e.target as HTMLInputElement).value; - state.customTextLimits.word = ""; - state.customTextLimits.time = ""; - updateUI(); - }); + modalEl.qs(".group[data-id='limit'] input.sections")?.on("input", (e) => { + state.customTextLimits.section = (e.target as HTMLInputElement).value; + state.customTextLimits.word = ""; + state.customTextLimits.time = ""; + updateUI(); + }); - const textarea = modalEl.querySelector("textarea"); - textarea?.addEventListener("input", (e) => { + const textarea = modalEl.qs("textarea"); + textarea?.on("input", (e) => { state.textarea = (e.target as HTMLTextAreaElement).value; }); - textarea?.addEventListener("keydown", (e) => { + textarea?.on("keydown", (e) => { if (e.key !== "Tab") return; e.preventDefault(); @@ -536,7 +519,7 @@ async function setup(modalEl: HTMLElement): Promise { state.textarea = area.value; }); - textarea?.addEventListener("keypress", (e) => { + textarea?.on("keypress", (e) => { if (state.longCustomTextWarning || state.challengeWarning) { e.preventDefault(); return; @@ -555,43 +538,35 @@ async function setup(modalEl: HTMLElement): Promise { }); } }); - modalEl.querySelector(".button.apply")?.addEventListener("click", () => { + modalEl.qs(".button.apply")?.on("click", () => { apply(); }); - modalEl.querySelector(".button.wordfilter")?.addEventListener("click", () => { + modalEl.qs(".button.wordfilter")?.on("click", () => { void WordFilterPopup.show({ modalChain: modal as AnimatedModal, }); }); - modalEl - .querySelector(".button.customGenerator") - ?.addEventListener("click", () => { - void CustomGeneratorPopup.show({ - modalChain: modal as AnimatedModal, - }); - }); - modalEl - .querySelector(".button.showSavedTexts") - ?.addEventListener("click", () => { - void SavedTextsPopup.show({ - modalChain: modal as AnimatedModal, - }); + modalEl.qs(".button.customGenerator")?.on("click", () => { + void CustomGeneratorPopup.show({ + modalChain: modal as AnimatedModal, }); - modalEl - .querySelector(".button.saveCustomText") - ?.addEventListener("click", () => { - void SaveCustomTextPopup.show({ - modalChain: modal as AnimatedModal, - modalChainData: { text: cleanUpText() }, - }); + }); + modalEl.qs(".button.showSavedTexts")?.on("click", () => { + void SavedTextsPopup.show({ + modalChain: modal as AnimatedModal, }); - modalEl - .querySelector(".longCustomTextWarning") - ?.addEventListener("click", () => { - state.longCustomTextWarning = false; - updateUI(); + }); + modalEl.qs(".button.saveCustomText")?.on("click", () => { + void SaveCustomTextPopup.show({ + modalChain: modal as AnimatedModal, + modalChainData: { text: cleanUpText() }, }); - modalEl.querySelector(".challengeWarning")?.addEventListener("click", () => { + }); + modalEl.qs(".longCustomTextWarning")?.on("click", () => { + state.longCustomTextWarning = false; + updateUI(); + }); + modalEl.qs(".challengeWarning")?.on("click", () => { state.challengeWarning = false; updateUI(); }); diff --git a/frontend/src/ts/modals/custom-word-amount.ts b/frontend/src/ts/modals/custom-word-amount.ts index b672a8cf0c35..034d5d14d489 100644 --- a/frontend/src/ts/modals/custom-word-amount.ts +++ b/frontend/src/ts/modals/custom-word-amount.ts @@ -9,8 +9,7 @@ export function show(showOptions?: ShowOptions): void { ...showOptions, focusFirstInput: "focusAndSelect", beforeAnimation: async (modalEl) => { - (modalEl.querySelector("input") as HTMLInputElement).value = - `${Config.words}`; + modalEl.qs("input")?.setValue(`${Config.words}`); }, }); } @@ -23,7 +22,7 @@ function hide(clearChain = false): void { function apply(): void { const val = parseInt( - modal.getModal().querySelector("input")?.value as string, + modal.getModal().qs("input")?.getValue() ?? "", 10, ); @@ -53,7 +52,7 @@ function apply(): void { const modal = new AnimatedModal({ dialogId: "customWordAmountModal", setup: async (modalEl): Promise => { - modalEl.addEventListener("submit", (e) => { + modalEl.on("submit", (e) => { e.preventDefault(); apply(); }); diff --git a/frontend/src/ts/modals/dev-options.ts b/frontend/src/ts/modals/dev-options.ts index 6c254688ada8..f0e25f664625 100644 --- a/frontend/src/ts/modals/dev-options.ts +++ b/frontend/src/ts/modals/dev-options.ts @@ -10,6 +10,7 @@ import { toggleUserFakeChartData } from "../test/result"; import { toggleCaretDebug } from "../utils/caret"; import { getInputElement } from "../input/input-element"; import { disableSlowTimerFail } from "../test/test-timer"; +import { ElementWithUtils } from "../utils/dom"; let mediaQueryDebugLevel = 0; @@ -17,47 +18,41 @@ export function show(): void { void modal.show(); } -async function setup(modalEl: HTMLElement): Promise { - modalEl.querySelector(".generateData")?.addEventListener("click", () => { +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.qs(".generateData")?.on("click", () => { showPopup("devGenerateData"); }); - modalEl - .querySelector(".showTestNotifications") - ?.addEventListener("click", () => { - Notifications.add("This is a test", 1, { - duration: 0, - }); - Notifications.add("This is a test", 0, { - duration: 0, - }); - Notifications.add("This is a test", -1, { - duration: 0, - details: { test: true, error: "Example error message" }, - }); - void modal.hide(); + modalEl.qs(".showTestNotifications")?.on("click", () => { + Notifications.add("This is a test", 1, { + duration: 0, }); - modalEl - .querySelector(".toggleMediaQueryDebug") - ?.addEventListener("click", () => { - mediaQueryDebugLevel++; - if (mediaQueryDebugLevel > 3) { - mediaQueryDebugLevel = 0; - } - Notifications.add( - `Setting media query debug level to ${mediaQueryDebugLevel}`, - 5, - ); - setMediaQueryDebugLevel(mediaQueryDebugLevel); + Notifications.add("This is a test", 0, { + duration: 0, }); - modalEl - .querySelector(".showRealWordsInput") - ?.addEventListener("click", () => { - getInputElement().style.opacity = "1"; - getInputElement().style.marginTop = "1.5em"; - getInputElement().style.caretColor = "red"; - void modal.hide(); + Notifications.add("This is a test", -1, { + duration: 0, + details: { test: true, error: "Example error message" }, }); - modalEl.querySelector(".quickLogin")?.addEventListener("click", () => { + void modal.hide(); + }); + modalEl.qs(".toggleMediaQueryDebug")?.on("click", () => { + mediaQueryDebugLevel++; + if (mediaQueryDebugLevel > 3) { + mediaQueryDebugLevel = 0; + } + Notifications.add( + `Setting media query debug level to ${mediaQueryDebugLevel}`, + 5, + ); + setMediaQueryDebugLevel(mediaQueryDebugLevel); + }); + modalEl.qs(".showRealWordsInput")?.on("click", () => { + getInputElement().style.opacity = "1"; + getInputElement().style.marginTop = "1.5em"; + getInputElement().style.caretColor = "red"; + void modal.hide(); + }); + modalEl.qs(".quickLogin")?.on("click", () => { if ( envConfig.quickLoginEmail === undefined || envConfig.quickLoginPassword === undefined @@ -76,7 +71,7 @@ async function setup(modalEl: HTMLElement): Promise { ); void modal.hide(); }); - modalEl.querySelector(".xpBarTest")?.addEventListener("click", () => { + modalEl.qs(".xpBarTest")?.on("click", () => { setTimeout(() => { void update(1000000, 20800, { base: 100, @@ -90,19 +85,15 @@ async function setup(modalEl: HTMLElement): Promise { }, 500); void modal.hide(); }); - modalEl - .querySelector(".toggleFakeChartData") - ?.addEventListener("click", () => { - toggleUserFakeChartData(); - }); - modalEl.querySelector(".toggleCaretDebug")?.addEventListener("click", () => { + modalEl.qs(".toggleFakeChartData")?.on("click", () => { + toggleUserFakeChartData(); + }); + modalEl.qs(".toggleCaretDebug")?.on("click", () => { toggleCaretDebug(); }); - modalEl - .querySelector(".disableSlowTimerFail") - ?.addEventListener("click", () => { - disableSlowTimerFail(); - }); + modalEl.qs(".disableSlowTimerFail")?.on("click", () => { + disableSlowTimerFail(); + }); } const modal = new AnimatedModal({ diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index 443df2f242a5..3f0ed9bf3e6e 100644 --- a/frontend/src/ts/modals/edit-preset.ts +++ b/frontend/src/ts/modals/edit-preset.ts @@ -412,18 +412,20 @@ function getConfigChanges(): Partial { }; } -async function setup(modalEl: HTMLElement): Promise { - modalEl.addEventListener("submit", (e) => { +async function setup( + modalEl: import("../utils/dom").ElementWithUtils, +): Promise { + modalEl.on("submit", (e) => { e.preventDefault(); void apply(); }); PresetTypeSchema.options.forEach((presetType) => { - const presetOption = modalEl.querySelector( + const presetOption = modalEl.qs( `.presetType button[value="${presetType}"]`, ); if (presetOption === null) return; - presetOption.addEventListener("click", () => { + presetOption.on("click", () => { state.presetType = presetType; updateUI(); }); diff --git a/frontend/src/ts/modals/edit-result-tags.ts b/frontend/src/ts/modals/edit-result-tags.ts index f3a65df6119e..c7d7a94236be 100644 --- a/frontend/src/ts/modals/edit-result-tags.ts +++ b/frontend/src/ts/modals/edit-result-tags.ts @@ -59,7 +59,7 @@ function hide(): void { } function appendButtons(): void { - const buttonsEl = modal.getModal().querySelector(".buttons"); + const buttonsEl = modal.getModal().qs(".buttons"); if (buttonsEl === null) { Notifications.add( @@ -74,7 +74,7 @@ function appendButtons(): void { ...state.tags, ]); - buttonsEl.innerHTML = ""; + buttonsEl.empty(); for (const tagId of tagIds) { const tag = DB.getSnapshot()?.tags.find((tag) => tag._id === tagId); const button = document.createElement("button"); @@ -85,7 +85,7 @@ function appendButtons(): void { toggleTag(tagId); updateActiveButtons(); }); - buttonsEl.appendChild(button); + buttonsEl.append(button); } } @@ -148,15 +148,13 @@ async function save(): Promise { const modal = new AnimatedModal({ dialogId: "editResultTagsModal", setup: async (modalEl): Promise => { - modalEl - .querySelector("button.saveButton") - ?.addEventListener("click", (e) => { - if (areUnsortedArraysEqual(state.startingTags, state.tags)) { - hide(); - return; - } + modalEl.qs("button.saveButton")?.on("click", (e) => { + if (areUnsortedArraysEqual(state.startingTags, state.tags)) { hide(); - void save(); - }); + return; + } + hide(); + void save(); + }); }, }); diff --git a/frontend/src/ts/modals/google-sign-up.ts b/frontend/src/ts/modals/google-sign-up.ts index d436fbc01846..dfb9e53e2fed 100644 --- a/frontend/src/ts/modals/google-sign-up.ts +++ b/frontend/src/ts/modals/google-sign-up.ts @@ -1,3 +1,4 @@ +import { ElementWithUtils, qsr } from "../utils/dom"; import * as Notifications from "../elements/notifications"; import { sendEmailVerification, @@ -17,7 +18,6 @@ import { resetIgnoreAuthCallback } from "../firebase"; import { ValidatedHtmlInputElement } from "../elements/input-validation"; import { UserNameSchema } from "@monkeytype/schemas/users"; import { remoteValidation } from "../utils/remote-validation"; -import { qsr } from "../utils/dom"; let signedInUser: UserCredential | undefined = undefined; @@ -168,8 +168,8 @@ new ValidatedHtmlInputElement(nameInputEl, { }, }); -async function setup(modalEl: HTMLElement): Promise { - modalEl.addEventListener("submit", (e) => { +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.on("submit", (e) => { e.preventDefault(); void apply(); }); diff --git a/frontend/src/ts/modals/mobile-test-config.ts b/frontend/src/ts/modals/mobile-test-config.ts index 8cd475107f6a..7295cbbbedb6 100644 --- a/frontend/src/ts/modals/mobile-test-config.ts +++ b/frontend/src/ts/modals/mobile-test-config.ts @@ -1,3 +1,4 @@ +import { ElementWithUtils } from "../utils/dom"; import * as TestLogic from "../test/test-logic"; import Config, { setConfig, setQuoteLengthAll } from "../config"; import * as ManualRestart from "../test/manual-restart-tracker"; @@ -77,10 +78,10 @@ export function show(): void { // void modal.hide(); // } -async function setup(modalEl: HTMLElement): Promise { - const wordsGroupButtons = modalEl.querySelectorAll(".wordsGroup button"); +async function setup(modalEl: ElementWithUtils): Promise { + const wordsGroupButtons = modalEl.qsa(".wordsGroup button"); for (const button of wordsGroupButtons) { - button.addEventListener("click", (e) => { + button.on("click", (e) => { const target = e.currentTarget as HTMLElement; const wrd = target.getAttribute("data-words") as string; @@ -97,9 +98,9 @@ async function setup(modalEl: HTMLElement): Promise { }); } - const modeGroupButtons = modalEl.querySelectorAll(".modeGroup button"); + const modeGroupButtons = modalEl.qsa(".modeGroup button"); for (const button of modeGroupButtons) { - button.addEventListener("click", (e) => { + button.on("click", (e) => { const target = e.currentTarget as HTMLElement; const mode = target.getAttribute("data-mode"); if (mode === Config.mode) return; @@ -109,9 +110,9 @@ async function setup(modalEl: HTMLElement): Promise { }); } - const timeGroupButtons = modalEl.querySelectorAll(".timeGroup button"); + const timeGroupButtons = modalEl.qsa(".timeGroup button"); for (const button of timeGroupButtons) { - button.addEventListener("click", (e) => { + button.on("click", (e) => { const target = e.currentTarget as HTMLElement; const time = target.getAttribute("data-time") as string; @@ -128,9 +129,9 @@ async function setup(modalEl: HTMLElement): Promise { }); } - const quoteGroupButtons = modalEl.querySelectorAll(".quoteGroup button"); + const quoteGroupButtons = modalEl.qsa(".quoteGroup button"); for (const button of quoteGroupButtons) { - button.addEventListener("click", (e) => { + button.on("click", (e) => { const target = e.currentTarget as HTMLElement; const lenAttr = target.getAttribute("data-quoteLength") ?? "0"; @@ -161,33 +162,33 @@ async function setup(modalEl: HTMLElement): Promise { }); } - modalEl.querySelector(".customChange")?.addEventListener("click", () => { + modalEl.qs(".customChange")?.on("click", () => { CustomTextPopup.show({ modalChain: modal, }); }); - modalEl.querySelector(".punctuation")?.addEventListener("click", () => { + modalEl.qs(".punctuation")?.on("click", () => { setConfig("punctuation", !Config.punctuation); ManualRestart.set(); TestLogic.restart(); }); - modalEl.querySelector(".numbers")?.addEventListener("click", () => { + modalEl.qs(".numbers")?.on("click", () => { setConfig("numbers", !Config.numbers); ManualRestart.set(); TestLogic.restart(); }); - modalEl.querySelector(".shareButton")?.addEventListener("click", () => { + modalEl.qs(".shareButton")?.on("click", () => { ShareTestSettingsPopup.show({ modalChain: modal, }); }); - const buttons = modalEl.querySelectorAll("button"); + const buttons = modalEl.qsa("button"); for (const button of buttons) { - button.addEventListener("click", () => { + button.on("click", () => { update(); }); } diff --git a/frontend/src/ts/modals/pb-tables.ts b/frontend/src/ts/modals/pb-tables.ts index f00ce6160f66..4de644923f60 100644 --- a/frontend/src/ts/modals/pb-tables.ts +++ b/frontend/src/ts/modals/pb-tables.ts @@ -13,12 +13,9 @@ type PBWithMode2 = { function update(mode: Mode): void { const modalEl = modal.getModal(); - (modalEl.querySelector("table tbody") as HTMLElement).innerHTML = ""; - (modalEl.querySelector("table thead tr td") as HTMLElement).textContent = - mode; - ( - modalEl.querySelector("table thead tr td span.unit") as HTMLElement - ).textContent = Config.typingSpeedUnit; + modalEl.qs("table tbody")?.empty(); + modalEl.qs("table thead tr td")?.setText(mode); + modalEl.qs("table thead tr td span.unit")?.setText(Config.typingSpeedUnit); const snapshot = DB.getSnapshot(); if (!snapshot) return; @@ -53,8 +50,7 @@ function update(mode: Mode): void { format(date, "HH:mm") + ""; } - modalEl.querySelector("table tbody")?.insertAdjacentHTML( - `beforeend`, + modalEl.qs("table tbody")?.appendHtml( ` ${mode2memory === pb.mode2 ? "" : pb.mode2} diff --git a/frontend/src/ts/modals/practise-words.ts b/frontend/src/ts/modals/practise-words.ts index 134a7126a189..28faa5c6496a 100644 --- a/frontend/src/ts/modals/practise-words.ts +++ b/frontend/src/ts/modals/practise-words.ts @@ -32,11 +32,11 @@ function updateUI(): void { } } -async function setup(modalEl: HTMLElement): Promise { - for (const button of modalEl.querySelectorAll( - ".group[data-id='missed'] button", - )) { - button.addEventListener("click", (e) => { +async function setup( + modalEl: import("../utils/dom").ElementWithUtils, +): Promise { + for (const button of modalEl.qsa(".group[data-id='missed'] button")) { + button.on("click", (e) => { state.missed = (e.target as HTMLButtonElement).value as | "off" | "words" @@ -45,20 +45,18 @@ async function setup(modalEl: HTMLElement): Promise { }); } - for (const button of modalEl.querySelectorAll( - ".group[data-id='slow'] button", - )) { - button.addEventListener("click", (e) => { + for (const button of modalEl.qsa(".group[data-id='slow'] button")) { + button.on("click", (e) => { state.slow = (e.target as HTMLButtonElement).value === "true"; updateUI(); }); } - modalEl.querySelector(".start")?.addEventListener("click", () => { + modalEl.qs(".start")?.on("click", () => { apply(); }); - modalEl.addEventListener("submit", (e) => { + modalEl.on("submit", (e) => { e.preventDefault(); apply(); }); diff --git a/frontend/src/ts/modals/quote-approve.ts b/frontend/src/ts/modals/quote-approve.ts index 3fd7a79febac..6953e1b53f46 100644 --- a/frontend/src/ts/modals/quote-approve.ts +++ b/frontend/src/ts/modals/quote-approve.ts @@ -1,3 +1,4 @@ +import { ElementWithUtils } from "../utils/dom"; import Ape from "../ape"; import * as Loader from "../elements/loader"; import * as Notifications from "../elements/notifications"; @@ -230,8 +231,8 @@ async function editQuote(index: number, dbid: string): Promise { updateList(); } -async function setup(modalEl: HTMLElement): Promise { - modalEl.querySelector("button.refreshList")?.addEventListener("click", () => { +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.qs("button.refreshList")?.on("click", () => { $("#quoteApproveModal .quotes").empty(); void getQuotes(); }); diff --git a/frontend/src/ts/modals/quote-rate.ts b/frontend/src/ts/modals/quote-rate.ts index e22b97f91e1c..91e06ed984d7 100644 --- a/frontend/src/ts/modals/quote-rate.ts +++ b/frontend/src/ts/modals/quote-rate.ts @@ -6,6 +6,7 @@ import * as Loader from "../elements/loader"; import * as Notifications from "../elements/notifications"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import { isSafeNumber } from "@monkeytype/util/numbers"; +import { ElementWithUtils } from "../utils/dom"; let rating = 0; @@ -208,26 +209,26 @@ async function submit(): Promise { $(".pageTest #result #rateQuoteButton .icon").addClass("fas"); } -async function setup(modalEl: HTMLElement): Promise { - modalEl.querySelector(".submitButton")?.addEventListener("click", () => { +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.qs(".submitButton")?.on("click", () => { void submit(); }); - const starButtons = modalEl.querySelectorAll(".stars button.star"); + const starButtons = modalEl.qsa(".stars button.star"); for (const button of starButtons) { - button.addEventListener("click", (e) => { + button.on("click", (e) => { const ratingValue = parseInt( (e.currentTarget as HTMLElement).getAttribute("data-rating") as string, ); rating = ratingValue; refreshStars(); }); - button.addEventListener("mouseenter", (e) => { + button.on("mouseenter", (e) => { const ratingHover = parseInt( (e.currentTarget as HTMLElement).getAttribute("data-rating") as string, ); refreshStars(ratingHover); }); - button.addEventListener("mouseleave", () => { + button.on("mouseleave", () => { refreshStars(); }); } diff --git a/frontend/src/ts/modals/quote-report.ts b/frontend/src/ts/modals/quote-report.ts index f24a423abb76..988fbb5273c5 100644 --- a/frontend/src/ts/modals/quote-report.ts +++ b/frontend/src/ts/modals/quote-report.ts @@ -1,3 +1,4 @@ +import { ElementWithUtils, qsr } from "../utils/dom"; import Ape from "../ape"; import Config from "../config"; import * as Loader from "../elements/loader"; @@ -9,7 +10,6 @@ import SlimSelect from "slim-select"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import { CharacterCounter } from "../elements/character-counter"; import { QuoteReportReason } from "@monkeytype/schemas/quotes"; -import { qsr } from "../utils/dom"; type State = { quoteToReport?: Quote; @@ -129,8 +129,8 @@ async function submitReport(): Promise { void hide(true); } -async function setup(modalEl: HTMLElement): Promise { - modalEl.querySelector("button")?.addEventListener("click", async () => { +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.qs("button")?.on("click", async () => { await submitReport(); }); } diff --git a/frontend/src/ts/modals/quote-search.ts b/frontend/src/ts/modals/quote-search.ts index ddfacc1bbc3d..b0326c3cb2c4 100644 --- a/frontend/src/ts/modals/quote-search.ts +++ b/frontend/src/ts/modals/quote-search.ts @@ -22,6 +22,7 @@ import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import * as TestLogic from "../test/test-logic"; import { createErrorMessage } from "../utils/misc"; import { highlightMatches } from "../utils/strings"; +import { ElementWithUtils } from "../utils/dom"; const searchServiceCache: Record> = {}; @@ -310,40 +311,34 @@ async function updateResults(searchText: string): Promise { resultsList.append(quoteSearchResult); }); - const searchResults = modal - .getModal() - .querySelectorAll(".searchResult"); + const searchResults = modal.getModal().qsa(".searchResult"); for (const searchResult of searchResults) { - const quoteId = parseInt(searchResult.dataset["quoteId"] as string); - searchResult - .querySelector(".textButton.favorite") - ?.addEventListener("click", (e) => { - e.stopPropagation(); - if (quoteId === undefined || isNaN(quoteId)) { - Notifications.add( - "Could not toggle quote favorite: quote id is not a number", - -1, - ); - return; - } - void toggleFavoriteForQuote(`${quoteId}`); - }); - searchResult - .querySelector(".textButton.report") - ?.addEventListener("click", (e) => { - e.stopPropagation(); - if (quoteId === undefined || isNaN(quoteId)) { - Notifications.add( - "Could not open quote report modal: quote id is not a number", - -1, - ); - return; - } - void QuoteReportModal.show(quoteId, { - modalChain: modal, - }); + const quoteId = parseInt(searchResult.native.dataset["quoteId"] as string); + searchResult.qs(".textButton.favorite")?.on("click", (e) => { + e.stopPropagation(); + if (quoteId === undefined || isNaN(quoteId)) { + Notifications.add( + "Could not toggle quote favorite: quote id is not a number", + -1, + ); + return; + } + void toggleFavoriteForQuote(`${quoteId}`); + }); + searchResult.qs(".textButton.report")?.on("click", (e) => { + e.stopPropagation(); + if (quoteId === undefined || isNaN(quoteId)) { + Notifications.add( + "Could not open quote report modal: quote id is not a number", + -1, + ); + return; + } + void QuoteReportModal.show(quoteId, { + modalChain: modal, }); - searchResult.addEventListener("click", (e) => { + }); + searchResult.on("click", (e) => { TestState.setSelectedQuoteId(quoteId); apply(quoteId); }); @@ -382,7 +377,7 @@ export async function show(showOptions?: ShowOptions): Promise { settings: { showSearch: false, placeholderText: "filter by length", - contentLocation: modal.getModal(), + contentLocation: modal.getModal().native, }, data: [ { @@ -495,61 +490,55 @@ async function toggleFavoriteForQuote(quoteId: string): Promise { } } -async function setup(modalEl: HTMLElement): Promise { - modalEl.querySelector(".searchBox")?.addEventListener("input", (e) => { +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.qs(".searchBox")?.on("input", (e) => { searchForQuotes(); }); - modalEl - .querySelector("button.toggleFavorites") - ?.addEventListener("click", (e) => { - if (!isAuthenticated()) { - // Notifications.add("You need to be logged in to use this feature!", 0); - return; - } + modalEl.qs("button.toggleFavorites")?.on("click", (e) => { + if (!isAuthenticated()) { + // Notifications.add("You need to be logged in to use this feature!", 0); + return; + } - $(e.target as HTMLElement).toggleClass("active"); - searchForQuotes(); - }); - modalEl.querySelector(".goToQuoteApprove")?.addEventListener("click", (e) => { + $(e.target as HTMLElement).toggleClass("active"); + searchForQuotes(); + }); + modalEl.qs(".goToQuoteApprove")?.on("click", (e) => { void QuoteApprovePopup.show({ modalChain: modal, }); }); - modalEl - .querySelector(".goToQuoteSubmit") - ?.addEventListener("click", async (e) => { - Loader.show(); - const getSubmissionEnabled = await Ape.quotes.isSubmissionEnabled(); - const isSubmissionEnabled = - (getSubmissionEnabled.status === 200 && - getSubmissionEnabled.body.data?.isEnabled) ?? - false; - Loader.hide(); - if (!isSubmissionEnabled) { - Notifications.add( - "Quote submission is disabled temporarily due to a large submission queue.", - 0, - { - duration: 5, - }, - ); - return; - } - void QuoteSubmitPopup.show({ - modalChain: modal, - }); + modalEl.qs(".goToQuoteSubmit")?.on("click", async (e) => { + Loader.show(); + const getSubmissionEnabled = await Ape.quotes.isSubmissionEnabled(); + const isSubmissionEnabled = + (getSubmissionEnabled.status === 200 && + getSubmissionEnabled.body.data?.isEnabled) ?? + false; + Loader.hide(); + if (!isSubmissionEnabled) { + Notifications.add( + "Quote submission is disabled temporarily due to a large submission queue.", + 0, + { + duration: 5, + }, + ); + return; + } + void QuoteSubmitPopup.show({ + modalChain: modal, }); - modalEl - .querySelector(".quoteLengthFilter") - ?.addEventListener("change", searchForQuotes); - modalEl.querySelector(".nextPage")?.addEventListener("click", () => { + }); + modalEl.qs(".quoteLengthFilter")?.on("change", searchForQuotes); + modalEl.qs(".nextPage")?.on("click", () => { const searchText = ( document.getElementById("searchBox") as HTMLInputElement ).value; currentPageNumber++; void updateResults(searchText); }); - modalEl.querySelector(".prevPage")?.addEventListener("click", () => { + modalEl.qs(".prevPage")?.on("click", () => { const searchText = ( document.getElementById("searchBox") as HTMLInputElement ).value; diff --git a/frontend/src/ts/modals/quote-submit.ts b/frontend/src/ts/modals/quote-submit.ts index 2a2f99fb337f..c4111f049959 100644 --- a/frontend/src/ts/modals/quote-submit.ts +++ b/frontend/src/ts/modals/quote-submit.ts @@ -1,3 +1,4 @@ +import { ElementWithUtils, qsr } from "../utils/dom"; import Ape from "../ape"; import * as Loader from "../elements/loader"; import * as Notifications from "../elements/notifications"; @@ -9,7 +10,6 @@ import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import { CharacterCounter } from "../elements/character-counter"; import { Language } from "@monkeytype/schemas/languages"; import { LanguageGroupNames } from "../constants/languages"; -import { qsr } from "../utils/dom"; let dropdownReady = false; async function initDropdown(): Promise { @@ -95,8 +95,8 @@ function hide(clearModalChain: boolean): void { }); } -async function setup(modalEl: HTMLElement): Promise { - modalEl.querySelector("button")?.addEventListener("click", () => { +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.qs("button")?.on("click", () => { void submitQuote(); hide(true); }); diff --git a/frontend/src/ts/modals/register-captcha.ts b/frontend/src/ts/modals/register-captcha.ts index 99b6e17c834a..e8a1f0b21eb7 100644 --- a/frontend/src/ts/modals/register-captcha.ts +++ b/frontend/src/ts/modals/register-captcha.ts @@ -28,7 +28,7 @@ export async function show(): Promise { CaptchaController.reset("register"); CaptchaController.render( - modal.querySelector(".g-recaptcha") as HTMLElement, + modal.qs(".g-recaptcha")?.native as HTMLElement, "register", (token) => { resolve(token); diff --git a/frontend/src/ts/modals/save-custom-text.ts b/frontend/src/ts/modals/save-custom-text.ts index d5a13400da1d..e53f116b6f6c 100644 --- a/frontend/src/ts/modals/save-custom-text.ts +++ b/frontend/src/ts/modals/save-custom-text.ts @@ -87,14 +87,16 @@ function save(): boolean { } } -async function setup(modalEl: HTMLElement): Promise { - modalEl.addEventListener("submit", (e) => { +async function setup( + modalEl: import("../utils/dom").ElementWithUtils, +): Promise { + modalEl.on("submit", (e) => { e.preventDefault(); if (validatedInput.getValidationResult().status === "success" && save()) { void modal.hide(); } }); - modalEl.querySelector(".isLongText")?.addEventListener("input", (e) => { + modalEl.qs(".isLongText")?.on("input", (e) => { validatedInput.triggerValidation(); }); } diff --git a/frontend/src/ts/modals/share-custom-theme.ts b/frontend/src/ts/modals/share-custom-theme.ts index 5fbfb8794bd9..05626d110870 100644 --- a/frontend/src/ts/modals/share-custom-theme.ts +++ b/frontend/src/ts/modals/share-custom-theme.ts @@ -14,8 +14,7 @@ const state: State = { export function show(): void { void modal.show({ beforeAnimation: async (m) => { - (m.querySelector("input[type='checkbox']") as HTMLInputElement).checked = - false; + m.qs("input[type='checkbox']")?.setChecked(false); state.includeBackground = false; }, }); @@ -66,7 +65,7 @@ async function copy(): Promise { modalChain: modal, focusFirstInput: "focusAndSelect", beforeAnimation: async (m) => { - (m.querySelector("input") as HTMLInputElement).value = url; + m.qs("input")?.setValue(url); }, }); } @@ -75,10 +74,10 @@ async function copy(): Promise { const modal = new AnimatedModal({ dialogId: "shareCustomThemeModal", setup: async (modalEl): Promise => { - modalEl.querySelector("button")?.addEventListener("click", copy); + modalEl.qs("button")?.on("click", copy); modalEl - .querySelector("input[type='checkbox']") - ?.addEventListener("change", (e) => { + .qs("input[type='checkbox']") + ?.on("change", (e) => { state.includeBackground = (e.target as HTMLInputElement).checked; }); }, diff --git a/frontend/src/ts/modals/share-test-settings.ts b/frontend/src/ts/modals/share-test-settings.ts index c510bb4af965..379fde39210b 100644 --- a/frontend/src/ts/modals/share-test-settings.ts +++ b/frontend/src/ts/modals/share-test-settings.ts @@ -1,3 +1,4 @@ +import { ElementWithUtils } from "../utils/dom"; import Config from "../config"; import { currentQuote } from "../test/test-words"; import { getMode2 } from "../utils/misc"; @@ -77,16 +78,14 @@ export function show(showOptions?: ShowOptions): void { }); } -async function setup(modalEl: HTMLElement): Promise { - modalEl - .querySelector("textarea.url") - ?.addEventListener("click", async (e) => { - (e.target as HTMLTextAreaElement).select(); - }); +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.qs("textarea.url")?.on("click", async (e) => { + (e.target as HTMLTextAreaElement).select(); + }); - const inputs = modalEl.querySelectorAll("label input"); + const inputs = modalEl.qsa("label input"); for (const input of inputs) { - input.addEventListener("change", async () => { + input.on("change", async () => { updateURL(); updateSubgroups(); }); diff --git a/frontend/src/ts/modals/streak-hour-offset.ts b/frontend/src/ts/modals/streak-hour-offset.ts index f2f9d90aa3c9..b265e2869f93 100644 --- a/frontend/src/ts/modals/streak-hour-offset.ts +++ b/frontend/src/ts/modals/streak-hour-offset.ts @@ -20,13 +20,14 @@ export function show(): void { focusFirstInput: true, beforeAnimation: async (modalEl) => { if (getSnapshot()?.streakHourOffset !== undefined) { - modalEl.querySelector("input")?.remove(); - modalEl.querySelector(".preview")?.remove(); - modalEl.querySelector("button")?.remove(); - (modalEl.querySelector(".text") as HTMLElement).textContent = - "You have already set your streak hour offset."; + modalEl.qs("input")?.remove(); + modalEl.qs(".preview")?.remove(); + modalEl.qs("button")?.remove(); + modalEl + .qs(".text") + ?.setText("You have already set your streak hour offset."); } else { - (modalEl.querySelector("input") as HTMLInputElement).value = "0"; + modalEl.qs("input")?.setValue("0"); updatePreview(); } }, @@ -35,11 +36,11 @@ export function show(): void { function updatePreview(): void { const inputValue = parseInt( - modal.getModal().querySelector("input")?.value as string, + modal.getModal().qs("input")?.getValue() as string, 10, ); - const preview = modal.getModal().querySelector(".preview") as HTMLElement; + const preview = modal.getModal().qs(".preview"); const date = new Date(); date.setUTCHours(0); @@ -55,10 +56,10 @@ function updatePreview(): void { newDate.setHours(newDate.getHours() - -1 * inputValue); //idk why, but it only works when i subtract (so i have to negate inputValue) - preview.innerHTML = ` + preview?.setHtml(`
Current local reset time:
${date.toLocaleTimeString()}
New local reset time:
${newDate.toLocaleTimeString()}
- `; + `); } function hide(): void { @@ -67,7 +68,7 @@ function hide(): void { async function apply(): Promise { const value = parseInt( - modal.getModal().querySelector("input")?.value as string, + modal.getModal().qs("input")?.getValue() as string, 10, ); @@ -101,10 +102,10 @@ async function apply(): Promise { const modal = new AnimatedModal({ dialogId: "streakHourOffsetModal", setup: async (modalEl): Promise => { - modalEl.querySelector("input")?.addEventListener("input", () => { + modalEl.qs("input")?.on("input", () => { updatePreview(); }); - modalEl.querySelector("button")?.addEventListener("click", () => { + modalEl.qs("button")?.on("click", () => { void apply(); }); }, diff --git a/frontend/src/ts/modals/support.ts b/frontend/src/ts/modals/support.ts index e1b0bb770edf..bc25ca8593c9 100644 --- a/frontend/src/ts/modals/support.ts +++ b/frontend/src/ts/modals/support.ts @@ -8,7 +8,7 @@ export function show(): void { const modal = new AnimatedModal({ dialogId: "supportModal", setup: async (modalEl): Promise => { - modalEl.querySelector("button.ads")?.addEventListener("click", async () => { + modalEl.qs("button.ads")?.on("click", async () => { Commandline.show( { subgroupOverride: "enableAds" }, { diff --git a/frontend/src/ts/modals/user-report.ts b/frontend/src/ts/modals/user-report.ts index 1878a09a7598..deafabd584ba 100644 --- a/frontend/src/ts/modals/user-report.ts +++ b/frontend/src/ts/modals/user-report.ts @@ -46,7 +46,7 @@ export async function show(options: ShowOptions): Promise { focusFirstInput: true, beforeAnimation: async (modalEl) => { CaptchaController.render( - modalEl.querySelector(".g-recaptcha") as HTMLElement, + modalEl.qs(".g-recaptcha")?.native as HTMLElement, "userReportModal", ); @@ -54,16 +54,15 @@ export async function show(options: ShowOptions): Promise { state.userUid = options.uid; state.lbOptOut = options.lbOptOut; - (modalEl.querySelector(".user") as HTMLElement).textContent = name; - (modalEl.querySelector(".reason") as HTMLSelectElement).value = - "Inappropriate name"; - (modalEl.querySelector(".comment") as HTMLTextAreaElement).value = ""; + modalEl.qs(".user")?.setText(name); + modalEl.qs(".reason")?.setValue("Inappropriate name"); + modalEl.qs(".comment")?.setValue(""); select = new SlimSelect({ - select: modalEl.querySelector(".reason") as HTMLElement, + select: modalEl.qs(".reason")?.native as HTMLElement, settings: { showSearch: false, - contentLocation: modalEl, + contentLocation: modalEl.native, }, }); }, @@ -139,7 +138,7 @@ async function submitReport(): Promise { const modal = new AnimatedModal({ dialogId: "userReportModal", setup: async (modalEl): Promise => { - modalEl.querySelector("button")?.addEventListener("click", () => { + modalEl.qs("button")?.on("click", () => { void submitReport(); }); }, diff --git a/frontend/src/ts/modals/word-filter.ts b/frontend/src/ts/modals/word-filter.ts index ad0913a9dd64..068514c9d153 100644 --- a/frontend/src/ts/modals/word-filter.ts +++ b/frontend/src/ts/modals/word-filter.ts @@ -118,19 +118,19 @@ export async function show(showOptions?: ShowOptions): Promise { languageSelect = new SlimSelect({ select: "#wordFilterModal .languageInput", settings: { - contentLocation: modalEl, + contentLocation: modalEl.native, }, }); layoutSelect = new SlimSelect({ select: "#wordFilterModal .layoutInput", settings: { - contentLocation: modal.getModal(), + contentLocation: modal.getModal().native, }, }); presetSelect = new SlimSelect({ select: "#wordFilterModal .presetInput", settings: { - contentLocation: modal.getModal(), + contentLocation: modal.getModal().native, }, }); $("#wordFilterModal .loadingIndicator").removeClass("hidden"); @@ -239,13 +239,13 @@ function setExactMatchInput(disable: boolean): void { } function disableButtons(): void { - for (const button of modal.getModal().querySelectorAll("button")) { + for (const button of modal.getModal().qsa("button")) { button.setAttribute("disabled", "true"); } } function enableButtons(): void { - for (const button of modal.getModal().querySelectorAll("button")) { + for (const button of modal.getModal().qsa("button")) { button.removeAttribute("disabled"); } } diff --git a/frontend/src/ts/utils/simple-modal.ts b/frontend/src/ts/utils/simple-modal.ts index 361ea5156c01..dff0df355040 100644 --- a/frontend/src/ts/utils/simple-modal.ts +++ b/frontend/src/ts/utils/simple-modal.ts @@ -11,7 +11,7 @@ import { ValidationOptions, ValidationResult, } from "../elements/input-validation"; -import { qsr } from "./dom"; +import { ElementWithUtils, qsr } from "./dom"; type CommonInput = { type: TType; @@ -117,8 +117,8 @@ type SimpleModalOptions = { export class SimpleModal { parameters: string[]; - wrapper: HTMLElement; - element: HTMLElement; + wrapper: ElementWithUtils; + element: ElementWithUtils; modal: AnimatedModal; id: string; title: string; @@ -155,11 +155,11 @@ export class SimpleModal { this.afterClickAway = options.afterClickAway; } reset(): void { - this.element.innerHTML = ` + this.element.setHtml(`
- `; + `); } init(): void { @@ -449,15 +449,13 @@ export class SimpleModal { } updateSubmitButtonState(): void { - const button = this.element.querySelector( - ".submitButton", - ) as HTMLButtonElement; + const button = this.element.qs(".submitButton"); if (button === null) return; if (this.hasMissingRequired() || this.hasValidationErrors()) { - button.disabled = true; + button.disable(); } else { - button.disabled = false; + button.enable(); } } } @@ -474,7 +472,7 @@ let activePopup: SimpleModal | null = null; const modal = new AnimatedModal({ dialogId: "simpleModal", setup: async (modalEl): Promise => { - modalEl.addEventListener("submit", (e) => { + modalEl.on("submit", (e) => { e.preventDefault(); activePopup?.exec(); }); From bda1aac3bd043ac014cf23ad216d9a689deb03d8 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 1 Jan 2026 23:13:01 +0100 Subject: [PATCH 03/23] fix ai hallucinating --- frontend/src/ts/modals/custom-generator.ts | 5 ++--- frontend/src/ts/modals/edit-preset.ts | 6 ++---- frontend/src/ts/modals/practise-words.ts | 5 ++--- frontend/src/ts/modals/save-custom-text.ts | 6 ++---- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/frontend/src/ts/modals/custom-generator.ts b/frontend/src/ts/modals/custom-generator.ts index fd3898dc7a58..c89951c42f0f 100644 --- a/frontend/src/ts/modals/custom-generator.ts +++ b/frontend/src/ts/modals/custom-generator.ts @@ -5,6 +5,7 @@ import AnimatedModal, { HideOptions, ShowOptions, } from "../utils/animated-modal"; +import { ElementWithUtils } from "../utils/dom"; type Preset = { display: string; @@ -159,9 +160,7 @@ async function apply(set: boolean): Promise { }); } -async function setup( - modalEl: import("../utils/dom").ElementWithUtils, -): Promise { +async function setup(modalEl: ElementWithUtils): Promise { modalEl.qs(".setButton")?.on("click", () => { void apply(true); }); diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index 3f0ed9bf3e6e..45b616614334 100644 --- a/frontend/src/ts/modals/edit-preset.ts +++ b/frontend/src/ts/modals/edit-preset.ts @@ -21,7 +21,7 @@ import { import { getDefaultConfig } from "../constants/default-config"; import { SnapshotPreset } from "../constants/default-snapshot"; import { ValidatedHtmlInputElement } from "../elements/input-validation"; -import { qsr } from "../utils/dom"; +import { ElementWithUtils, qsr } from "../utils/dom"; import { configMetadata } from "../config-metadata"; const state = { @@ -412,9 +412,7 @@ function getConfigChanges(): Partial { }; } -async function setup( - modalEl: import("../utils/dom").ElementWithUtils, -): Promise { +async function setup(modalEl: ElementWithUtils): Promise { modalEl.on("submit", (e) => { e.preventDefault(); void apply(); diff --git a/frontend/src/ts/modals/practise-words.ts b/frontend/src/ts/modals/practise-words.ts index 28faa5c6496a..66b0f36c9e4d 100644 --- a/frontend/src/ts/modals/practise-words.ts +++ b/frontend/src/ts/modals/practise-words.ts @@ -1,6 +1,7 @@ import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import * as PractiseWords from "../test/practise-words"; import * as TestLogic from "../test/test-logic"; +import { ElementWithUtils } from "../utils/dom"; type State = { missed: "off" | "words" | "biwords"; @@ -32,9 +33,7 @@ function updateUI(): void { } } -async function setup( - modalEl: import("../utils/dom").ElementWithUtils, -): Promise { +async function setup(modalEl: ElementWithUtils): Promise { for (const button of modalEl.qsa(".group[data-id='missed'] button")) { button.on("click", (e) => { state.missed = (e.target as HTMLButtonElement).value as diff --git a/frontend/src/ts/modals/save-custom-text.ts b/frontend/src/ts/modals/save-custom-text.ts index e53f116b6f6c..34921566c31d 100644 --- a/frontend/src/ts/modals/save-custom-text.ts +++ b/frontend/src/ts/modals/save-custom-text.ts @@ -4,7 +4,7 @@ import * as CustomTextState from "../states/custom-text-name"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import { ValidatedHtmlInputElement } from "../elements/input-validation"; import { z } from "zod"; -import { qsr } from "../utils/dom"; +import { ElementWithUtils, qsr } from "../utils/dom"; type IncomingData = { text: string[]; @@ -87,9 +87,7 @@ function save(): boolean { } } -async function setup( - modalEl: import("../utils/dom").ElementWithUtils, -): Promise { +async function setup(modalEl: ElementWithUtils): Promise { modalEl.on("submit", (e) => { e.preventDefault(); if (validatedInput.getValidationResult().status === "success" && save()) { From 5cf61fe08fd0f99b74ad7a24067a4592860f5d95 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 1 Jan 2026 23:17:19 +0100 Subject: [PATCH 04/23] fixes --- frontend/src/ts/modals/quote-search.ts | 2 +- frontend/src/ts/modals/word-filter.ts | 8 ++------ frontend/src/ts/utils/dom.ts | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/frontend/src/ts/modals/quote-search.ts b/frontend/src/ts/modals/quote-search.ts index b0326c3cb2c4..e404e4d44fbf 100644 --- a/frontend/src/ts/modals/quote-search.ts +++ b/frontend/src/ts/modals/quote-search.ts @@ -500,7 +500,7 @@ async function setup(modalEl: ElementWithUtils): Promise { return; } - $(e.target as HTMLElement).toggleClass("active"); + (e.currentTarget as HTMLElement)?.classList.toggle("active"); searchForQuotes(); }); modalEl.qs(".goToQuoteApprove")?.on("click", (e) => { diff --git a/frontend/src/ts/modals/word-filter.ts b/frontend/src/ts/modals/word-filter.ts index 068514c9d153..fa676c5a5e77 100644 --- a/frontend/src/ts/modals/word-filter.ts +++ b/frontend/src/ts/modals/word-filter.ts @@ -239,15 +239,11 @@ function setExactMatchInput(disable: boolean): void { } function disableButtons(): void { - for (const button of modal.getModal().qsa("button")) { - button.setAttribute("disabled", "true"); - } + modal.getModal().qsa("button").disable(); } function enableButtons(): void { - for (const button of modal.getModal().qsa("button")) { - button.removeAttribute("disabled"); - } + modal.getModal().qsa("button").enable(); } async function setup(): Promise { diff --git a/frontend/src/ts/utils/dom.ts b/frontend/src/ts/utils/dom.ts index a11aa1497b1b..4e1088691bc4 100644 --- a/frontend/src/ts/utils/dom.ts +++ b/frontend/src/ts/utils/dom.ts @@ -677,7 +677,7 @@ export class ElementWithUtils { /** * Select the element's content (for input and textarea elements) */ - select(this: ElementWithUtils): void { + select(this: ElementWithUtils): void { if (this.hasSelectableValue()) { this.native.select(); } From a6f620185c694f64c33990a3c541bb4b0942448d Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 1 Jan 2026 23:18:40 +0100 Subject: [PATCH 05/23] remove jq --- frontend/src/ts/utils/animated-modal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/ts/utils/animated-modal.ts b/frontend/src/ts/utils/animated-modal.ts index fe0f3b730095..98e7e16b55fa 100644 --- a/frontend/src/ts/utils/animated-modal.ts +++ b/frontend/src/ts/utils/animated-modal.ts @@ -408,14 +408,14 @@ export default class AnimatedModal< }, }); } else if (animationMode === "modalOnly") { - $(this.wrapperEl).removeClass("hidden").css("opacity", "1"); + this.wrapperEl.show().setStyle({ opacity: "1" }); animate(this.modalEl, { ...modalAnimation, duration: modalAnimationDuration, onComplete: async () => { this.wrapperEl.native.close(); - $(this.wrapperEl).addClass("hidden").css("opacity", "0"); + this.wrapperEl.hide().setStyle({ opacity: "0" }); Skeleton.remove(this.dialogId); this.open = false; await options?.afterAnimation?.(this.modalEl); From 1bbc32b3e1bfa1d5f5fb8ccce86ab73918ba2cca Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 1 Jan 2026 23:27:48 +0100 Subject: [PATCH 06/23] fix instance check --- frontend/src/ts/utils/animated-modal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/utils/animated-modal.ts b/frontend/src/ts/utils/animated-modal.ts index 98e7e16b55fa..317ec25aa1d1 100644 --- a/frontend/src/ts/utils/animated-modal.ts +++ b/frontend/src/ts/utils/animated-modal.ts @@ -99,7 +99,7 @@ export default class AnimatedModal< ); } - if (!(dialogElement instanceof HTMLDialogElement)) { + if (!(dialogElement.native instanceof HTMLDialogElement)) { throw new Error("Animated dialog must be an HTMLDialogElement"); } From 4f0e206a49c68d9bba0fa70f4ad77827809b7f6b Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 12:43:36 +0100 Subject: [PATCH 07/23] qsa.on --- frontend/src/ts/modals/mobile-test-config.ts | 141 +++++++++---------- 1 file changed, 63 insertions(+), 78 deletions(-) diff --git a/frontend/src/ts/modals/mobile-test-config.ts b/frontend/src/ts/modals/mobile-test-config.ts index 7295cbbbedb6..59f393580650 100644 --- a/frontend/src/ts/modals/mobile-test-config.ts +++ b/frontend/src/ts/modals/mobile-test-config.ts @@ -79,88 +79,76 @@ export function show(): void { // } async function setup(modalEl: ElementWithUtils): Promise { - const wordsGroupButtons = modalEl.qsa(".wordsGroup button"); - for (const button of wordsGroupButtons) { - button.on("click", (e) => { - const target = e.currentTarget as HTMLElement; - const wrd = target.getAttribute("data-words") as string; - - if (wrd === "custom") { - CustomWordAmountPopup.show({ - modalChain: modal, - }); - } else if (wrd !== undefined) { - const wrdNum = parseInt(wrd); - setConfig("words", wrdNum); - ManualRestart.set(); - TestLogic.restart(); - } - }); - } + modalEl.qsa(".wordsGroup button").on("click", (e) => { + const target = e.currentTarget as HTMLElement; + const wrd = target.getAttribute("data-words") as string; + + if (wrd === "custom") { + CustomWordAmountPopup.show({ + modalChain: modal, + }); + } else if (wrd !== undefined) { + const wrdNum = parseInt(wrd); + setConfig("words", wrdNum); + ManualRestart.set(); + TestLogic.restart(); + } + }); - const modeGroupButtons = modalEl.qsa(".modeGroup button"); - for (const button of modeGroupButtons) { - button.on("click", (e) => { - const target = e.currentTarget as HTMLElement; - const mode = target.getAttribute("data-mode"); - if (mode === Config.mode) return; - setConfig("mode", mode as Mode); + modalEl.qsa(".modeGroup button").on("click", (e) => { + const target = e.currentTarget as HTMLElement; + const mode = target.getAttribute("data-mode"); + if (mode === Config.mode) return; + setConfig("mode", mode as Mode); + ManualRestart.set(); + TestLogic.restart(); + }); + + modalEl.qsa(".timeGroup button").on("click", (e) => { + const target = e.currentTarget as HTMLElement; + const time = target.getAttribute("data-time") as string; + + if (time === "custom") { + CustomTestDurationPopup.show({ + modalChain: modal, + }); + } else if (time !== undefined) { + const timeNum = parseInt(time); + setConfig("time", timeNum); ManualRestart.set(); TestLogic.restart(); - }); - } + } + }); - const timeGroupButtons = modalEl.qsa(".timeGroup button"); - for (const button of timeGroupButtons) { - button.on("click", (e) => { - const target = e.currentTarget as HTMLElement; - const time = target.getAttribute("data-time") as string; - - if (time === "custom") { - CustomTestDurationPopup.show({ - modalChain: modal, - }); - } else if (time !== undefined) { - const timeNum = parseInt(time); - setConfig("time", timeNum); + modalEl.qsa(".quoteGroup button").on("click", (e) => { + const target = e.currentTarget as HTMLElement; + const lenAttr = target.getAttribute("data-quoteLength") ?? "0"; + + if (lenAttr === "all") { + if (setQuoteLengthAll()) { ManualRestart.set(); TestLogic.restart(); } - }); - } + } else if (lenAttr === "-2") { + void QuoteSearchModal.show({ + modalChain: modal, + }); + } else { + const len = parseInt(lenAttr, 10) as QuoteLength; + let arr: QuoteLengthConfig = []; - const quoteGroupButtons = modalEl.qsa(".quoteGroup button"); - for (const button of quoteGroupButtons) { - button.on("click", (e) => { - const target = e.currentTarget as HTMLElement; - const lenAttr = target.getAttribute("data-quoteLength") ?? "0"; - - if (lenAttr === "all") { - if (setQuoteLengthAll()) { - ManualRestart.set(); - TestLogic.restart(); - } - } else if (lenAttr === "-2") { - void QuoteSearchModal.show({ - modalChain: modal, - }); + if ((e as MouseEvent).shiftKey) { + arr = [...Config.quoteLength, len]; } else { - const len = parseInt(lenAttr, 10) as QuoteLength; - let arr: QuoteLengthConfig = []; - - if ((e as MouseEvent).shiftKey) { - arr = [...Config.quoteLength, len]; - } else { - arr = [len]; - } - - if (setConfig("quoteLength", arr)) { - ManualRestart.set(); - TestLogic.restart(); - } + arr = [len]; } - }); - } + + if (setConfig("quoteLength", arr)) { + ManualRestart.set(); + TestLogic.restart(); + } + } + }); modalEl.qs(".customChange")?.on("click", () => { CustomTextPopup.show({ @@ -186,12 +174,9 @@ async function setup(modalEl: ElementWithUtils): Promise { }); }); - const buttons = modalEl.qsa("button"); - for (const button of buttons) { - button.on("click", () => { - update(); - }); - } + modalEl.qsa("button").on("click", () => { + update(); + }); } const modal = new AnimatedModal({ From cc6e50980b069b2ccddc43fe271a3f914e933c77 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 13:36:42 +0100 Subject: [PATCH 08/23] qsa.on --- frontend/src/ts/modals/custom-text.ts | 65 +++++++++++++-------------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/frontend/src/ts/modals/custom-text.ts b/frontend/src/ts/modals/custom-text.ts index 8bb29b65ffb0..b9132ff1b0ad 100644 --- a/frontend/src/ts/modals/custom-text.ts +++ b/frontend/src/ts/modals/custom-text.ts @@ -408,49 +408,45 @@ function handleDelimiterChange(): void { async function setup(modalEl: ElementWithUtils): Promise { modalEl.qs("#fileInput")?.on("change", handleFileOpen); - const buttons = modalEl.qsa(".group[data-id='mode'] button"); - for (const button of buttons) { - button.on("click", (e) => { - state.customTextMode = (e.target as HTMLButtonElement).value as - | "simple" - | "repeat" - | "random"; - if (state.customTextMode === "simple") { - const text = cleanUpText(); - state.customTextLimits.word = `${text.length}`; - state.customTextLimits.time = ""; - state.customTextLimits.section = ""; - } - updateUI(); - }); - } + modalEl.qsa(".group[data-id='mode'] button").on("click", (e) => { + state.customTextMode = (e.target as HTMLButtonElement).value as + | "simple" + | "repeat" + | "random"; + if (state.customTextMode === "simple") { + const text = cleanUpText(); + state.customTextLimits.word = `${text.length}`; + state.customTextLimits.time = ""; + state.customTextLimits.section = ""; + } + updateUI(); + }); - for (const button of modalEl.qsa(".group[data-id='fancy'] button")) { - button.on("click", (e: MouseEvent) => { - state.removeFancyTypographyEnabled = - (e.target as HTMLButtonElement).value === "true"; - updateUI(); - }); - } + modalEl.qsa(".group[data-id='fancy'] button").on("click", (e: MouseEvent) => { + state.removeFancyTypographyEnabled = + (e.target as HTMLButtonElement).value === "true"; + updateUI(); + }); - for (const button of modalEl.qsa(".group[data-id='control'] button")) { - button.on("click", (e: MouseEvent) => { + modalEl + .qsa(".group[data-id='control'] button") + .on("click", (e: MouseEvent) => { state.replaceControlCharactersEnabled = (e.target as HTMLButtonElement).value === "true"; updateUI(); }); - } - for (const button of modalEl.qsa(".group[data-id='zeroWidth'] button")) { - button.on("click", (e: MouseEvent) => { + modalEl + .qsa(".group[data-id='zeroWidth'] button") + .on("click", (e: MouseEvent) => { state.removeZeroWidthCharactersEnabled = (e.target as HTMLButtonElement).value === "true"; updateUI(); }); - } - for (const button of modalEl.qsa(".group[data-id='delimiter'] button")) { - button.on("click", (e: MouseEvent) => { + modalEl + .qsa(".group[data-id='delimiter'] button") + .on("click", (e: MouseEvent) => { state.customTextPipeDelimiter = (e.target as HTMLButtonElement).value === "true"; if (state.customTextPipeDelimiter && state.customTextLimits.word !== "") { @@ -465,17 +461,16 @@ async function setup(modalEl: ElementWithUtils): Promise { handleDelimiterChange(); updateUI(); }); - } - for (const button of modalEl.qsa(".group[data-id='newlines'] button")) { - button.on("click", (e: MouseEvent) => { + modalEl + .qsa(".group[data-id='newlines'] button") + .on("click", (e: MouseEvent) => { state.replaceNewlines = (e.target as HTMLButtonElement).value as | "off" | "space" | "periodSpace"; updateUI(); }); - } modalEl.qs(".group[data-id='limit'] input.words")?.on("input", (e) => { state.customTextLimits.word = (e.target as HTMLInputElement).value; From 2babc26a30a2173233ddbecc073ca7b825f73a5c Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 13:39:46 +0100 Subject: [PATCH 09/23] refactor --- frontend/src/ts/modals/forgot-password.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/frontend/src/ts/modals/forgot-password.ts b/frontend/src/ts/modals/forgot-password.ts index df754a4e7f0e..504b8f312997 100644 --- a/frontend/src/ts/modals/forgot-password.ts +++ b/frontend/src/ts/modals/forgot-password.ts @@ -38,13 +38,10 @@ async function submit(): Promise { return; } - const email = modal - .getModal() - .qs("input") - ?.getValue() - ?.trim(); + const email = + modal.getModal().qs("input")?.getValue()?.trim() ?? ""; - if (email === "" || email === null || email === undefined) { + if (email === "") { Notifications.add("Please enter your email address"); CaptchaController.reset("forgotPasswordModal"); return; From 0803d08cd22ad7e1d49c51ae9790768864d54c41 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 13:45:09 +0100 Subject: [PATCH 10/23] fix animation not workign --- frontend/src/ts/utils/animated-modal.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/ts/utils/animated-modal.ts b/frontend/src/ts/utils/animated-modal.ts index 317ec25aa1d1..8f928151b56f 100644 --- a/frontend/src/ts/utils/animated-modal.ts +++ b/frontend/src/ts/utils/animated-modal.ts @@ -1,4 +1,4 @@ -import { animate, AnimationParams } from "animejs"; +import { AnimationParams } from "animejs"; import { applyReducedMotion, isPopupVisible } from "./misc"; import * as Skeleton from "./skeleton"; import { ElementWithUtils, qs } from "./dom"; @@ -280,7 +280,7 @@ export default class AnimatedModal< if (animationMode === "both" || animationMode === "none") { if (hasModalAnimation) { - animate(this.modalEl, { + this.modalEl.animate({ ...modalAnimation, duration: animationMode === "none" ? 0 : modalAnimationDuration, }); @@ -288,7 +288,7 @@ export default class AnimatedModal< this.modalEl.setStyle({ opacity: "1" }); } - animate(this.wrapperEl, { + this.wrapperEl.animate({ ...wrapperAnimation, duration: animationMode === "none" ? 0 : wrapperAnimationDuration, onBegin: () => { @@ -307,7 +307,7 @@ export default class AnimatedModal< this.wrapperEl.setStyle({ opacity: "1" }); this.wrapperEl.show(); - animate(this.modalEl, { + this.modalEl.animate({ ...modalAnimation, duration: modalAnimationDuration, onComplete: async () => { @@ -371,7 +371,7 @@ export default class AnimatedModal< if (animationMode === "both" || animationMode === "none") { if (hasModalAnimation) { - animate(this.modalEl, { + this.modalEl.animate({ ...modalAnimation, duration: animationMode === "none" ? 0 : modalAnimationDuration, }); @@ -379,7 +379,7 @@ export default class AnimatedModal< this.modalEl.setStyle({ opacity: "1" }); } - animate(this.wrapperEl, { + this.wrapperEl.animate({ ...wrapperAnimation, duration: animationMode === "none" ? 0 : wrapperAnimationDuration, onComplete: async () => { @@ -410,7 +410,7 @@ export default class AnimatedModal< } else if (animationMode === "modalOnly") { this.wrapperEl.show().setStyle({ opacity: "1" }); - animate(this.modalEl, { + this.modalEl.animate({ ...modalAnimation, duration: modalAnimationDuration, onComplete: async () => { From 8f1af7795d7a35c3afb526cae07d85496751397c Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 13:45:17 +0100 Subject: [PATCH 11/23] qsa.on --- frontend/src/ts/modals/practise-words.ts | 26 ++++++++++-------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/frontend/src/ts/modals/practise-words.ts b/frontend/src/ts/modals/practise-words.ts index 66b0f36c9e4d..8e27807e9bf3 100644 --- a/frontend/src/ts/modals/practise-words.ts +++ b/frontend/src/ts/modals/practise-words.ts @@ -34,22 +34,18 @@ function updateUI(): void { } async function setup(modalEl: ElementWithUtils): Promise { - for (const button of modalEl.qsa(".group[data-id='missed'] button")) { - button.on("click", (e) => { - state.missed = (e.target as HTMLButtonElement).value as - | "off" - | "words" - | "biwords"; - updateUI(); - }); - } + modalEl.qsa(".group[data-id='missed'] button").on("click", (e) => { + state.missed = (e.target as HTMLButtonElement).value as + | "off" + | "words" + | "biwords"; + updateUI(); + }); - for (const button of modalEl.qsa(".group[data-id='slow'] button")) { - button.on("click", (e) => { - state.slow = (e.target as HTMLButtonElement).value === "true"; - updateUI(); - }); - } + modalEl.qsa(".group[data-id='slow'] button").on("click", (e) => { + state.slow = (e.target as HTMLButtonElement).value === "true"; + updateUI(); + }); modalEl.qs(".start")?.on("click", () => { apply(); From 5bae6f6f1d0f227be061064ab205d34eebf06dc7 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 13:48:57 +0100 Subject: [PATCH 12/23] recaptcha qsr --- frontend/src/ts/modals/register-captcha.ts | 2 +- frontend/src/ts/modals/user-report.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/ts/modals/register-captcha.ts b/frontend/src/ts/modals/register-captcha.ts index e8a1f0b21eb7..a146b99ac2b8 100644 --- a/frontend/src/ts/modals/register-captcha.ts +++ b/frontend/src/ts/modals/register-captcha.ts @@ -28,7 +28,7 @@ export async function show(): Promise { CaptchaController.reset("register"); CaptchaController.render( - modal.qs(".g-recaptcha")?.native as HTMLElement, + modal.qsr(".g-recaptcha").native, "register", (token) => { resolve(token); diff --git a/frontend/src/ts/modals/user-report.ts b/frontend/src/ts/modals/user-report.ts index deafabd584ba..034ad33d9dc9 100644 --- a/frontend/src/ts/modals/user-report.ts +++ b/frontend/src/ts/modals/user-report.ts @@ -46,7 +46,7 @@ export async function show(options: ShowOptions): Promise { focusFirstInput: true, beforeAnimation: async (modalEl) => { CaptchaController.render( - modalEl.qs(".g-recaptcha")?.native as HTMLElement, + modalEl.qsr(".g-recaptcha").native, "userReportModal", ); From 87fc2648cff22ddb110d407d6078ee7d7eeba7e1 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 13:50:37 +0100 Subject: [PATCH 13/23] qsa.on --- frontend/src/ts/modals/share-test-settings.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/frontend/src/ts/modals/share-test-settings.ts b/frontend/src/ts/modals/share-test-settings.ts index 379fde39210b..49490ffae307 100644 --- a/frontend/src/ts/modals/share-test-settings.ts +++ b/frontend/src/ts/modals/share-test-settings.ts @@ -83,13 +83,10 @@ async function setup(modalEl: ElementWithUtils): Promise { (e.target as HTMLTextAreaElement).select(); }); - const inputs = modalEl.qsa("label input"); - for (const input of inputs) { - input.on("change", async () => { - updateURL(); - updateSubgroups(); - }); - } + modalEl.qsa("label input").on("change", async () => { + updateURL(); + updateSubgroups(); + }); } const modal = new AnimatedModal({ From 61165793f435329d6a52c9477a8ae06318ab68a6 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 14:17:51 +0100 Subject: [PATCH 14/23] qsa.on --- frontend/src/ts/modals/quote-rate.ts | 37 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/frontend/src/ts/modals/quote-rate.ts b/frontend/src/ts/modals/quote-rate.ts index 91e06ed984d7..fd05f192f1aa 100644 --- a/frontend/src/ts/modals/quote-rate.ts +++ b/frontend/src/ts/modals/quote-rate.ts @@ -213,25 +213,24 @@ async function setup(modalEl: ElementWithUtils): Promise { modalEl.qs(".submitButton")?.on("click", () => { void submit(); }); - const starButtons = modalEl.qsa(".stars button.star"); - for (const button of starButtons) { - button.on("click", (e) => { - const ratingValue = parseInt( - (e.currentTarget as HTMLElement).getAttribute("data-rating") as string, - ); - rating = ratingValue; - refreshStars(); - }); - button.on("mouseenter", (e) => { - const ratingHover = parseInt( - (e.currentTarget as HTMLElement).getAttribute("data-rating") as string, - ); - refreshStars(ratingHover); - }); - button.on("mouseleave", () => { - refreshStars(); - }); - } + + const stars = modalEl.qsa(".stars button.star"); + stars.on("click", (e) => { + const ratingValue = parseInt( + (e.currentTarget as HTMLElement).getAttribute("data-rating") as string, + ); + rating = ratingValue; + refreshStars(); + }); + stars.on("mouseenter", (e) => { + const ratingHover = parseInt( + (e.currentTarget as HTMLElement).getAttribute("data-rating") as string, + ); + refreshStars(ratingHover); + }); + stars.on("mouseleave", () => { + refreshStars(); + }); } const modal = new AnimatedModal({ From 85d355cb2bac1714eb346a842cf856c7e1e81a84 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 14:25:43 +0100 Subject: [PATCH 15/23] review --- frontend/src/ts/modals/quote-search.ts | 69 +++++++++++++++----------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/frontend/src/ts/modals/quote-search.ts b/frontend/src/ts/modals/quote-search.ts index e404e4d44fbf..7701ee93fdc9 100644 --- a/frontend/src/ts/modals/quote-search.ts +++ b/frontend/src/ts/modals/quote-search.ts @@ -312,37 +312,46 @@ async function updateResults(searchText: string): Promise { }); const searchResults = modal.getModal().qsa(".searchResult"); - for (const searchResult of searchResults) { - const quoteId = parseInt(searchResult.native.dataset["quoteId"] as string); - searchResult.qs(".textButton.favorite")?.on("click", (e) => { - e.stopPropagation(); - if (quoteId === undefined || isNaN(quoteId)) { - Notifications.add( - "Could not toggle quote favorite: quote id is not a number", - -1, - ); - return; - } - void toggleFavoriteForQuote(`${quoteId}`); - }); - searchResult.qs(".textButton.report")?.on("click", (e) => { - e.stopPropagation(); - if (quoteId === undefined || isNaN(quoteId)) { - Notifications.add( - "Could not open quote report modal: quote id is not a number", - -1, - ); - return; - } - void QuoteReportModal.show(quoteId, { - modalChain: modal, - }); - }); - searchResult.on("click", (e) => { - TestState.setSelectedQuoteId(quoteId); - apply(quoteId); + searchResults.qs(".textButton.favorite")?.on("click", (e) => { + e.stopPropagation(); + const quoteId = parseInt( + (e.currentTarget as HTMLElement)?.closest(".searchResult") + ?.dataset?.["quoteId"] as string, + ); + if (quoteId === undefined || isNaN(quoteId)) { + Notifications.add( + "Could not toggle quote favorite: quote id is not a number", + -1, + ); + return; + } + void toggleFavoriteForQuote(`${quoteId}`); + }); + searchResults.qs(".textButton.report")?.on("click", (e) => { + e.stopPropagation(); + const quoteId = parseInt( + (e.currentTarget as HTMLElement)?.closest(".searchResult") + ?.dataset?.["quoteId"] as string, + ); + if (quoteId === undefined || isNaN(quoteId)) { + Notifications.add( + "Could not open quote report modal: quote id is not a number", + -1, + ); + return; + } + void QuoteReportModal.show(quoteId, { + modalChain: modal, }); - } + }); + searchResults.on("click", (e) => { + const quoteId = parseInt( + (e.currentTarget as HTMLElement)?.closest(".searchResult") + ?.dataset?.["quoteId"] as string, + ); + TestState.setSelectedQuoteId(quoteId); + apply(quoteId); + }); } let lengthSelect: SlimSelect | undefined = undefined; From 6bd373fcb5d3edc0d48827ed2a8165bf93598c84 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 14:43:46 +0100 Subject: [PATCH 16/23] current target --- frontend/src/ts/modals/custom-text.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/frontend/src/ts/modals/custom-text.ts b/frontend/src/ts/modals/custom-text.ts index b9132ff1b0ad..d46fb91ceb0a 100644 --- a/frontend/src/ts/modals/custom-text.ts +++ b/frontend/src/ts/modals/custom-text.ts @@ -409,7 +409,7 @@ async function setup(modalEl: ElementWithUtils): Promise { modalEl.qs("#fileInput")?.on("change", handleFileOpen); modalEl.qsa(".group[data-id='mode'] button").on("click", (e) => { - state.customTextMode = (e.target as HTMLButtonElement).value as + state.customTextMode = (e.currentTarget as HTMLButtonElement).value as | "simple" | "repeat" | "random"; @@ -424,7 +424,7 @@ async function setup(modalEl: ElementWithUtils): Promise { modalEl.qsa(".group[data-id='fancy'] button").on("click", (e: MouseEvent) => { state.removeFancyTypographyEnabled = - (e.target as HTMLButtonElement).value === "true"; + (e.currentTarget as HTMLButtonElement).value === "true"; updateUI(); }); @@ -432,7 +432,7 @@ async function setup(modalEl: ElementWithUtils): Promise { .qsa(".group[data-id='control'] button") .on("click", (e: MouseEvent) => { state.replaceControlCharactersEnabled = - (e.target as HTMLButtonElement).value === "true"; + (e.currentTarget as HTMLButtonElement).value === "true"; updateUI(); }); @@ -440,7 +440,7 @@ async function setup(modalEl: ElementWithUtils): Promise { .qsa(".group[data-id='zeroWidth'] button") .on("click", (e: MouseEvent) => { state.removeZeroWidthCharactersEnabled = - (e.target as HTMLButtonElement).value === "true"; + (e.currentTarget as HTMLButtonElement).value === "true"; updateUI(); }); @@ -448,7 +448,7 @@ async function setup(modalEl: ElementWithUtils): Promise { .qsa(".group[data-id='delimiter'] button") .on("click", (e: MouseEvent) => { state.customTextPipeDelimiter = - (e.target as HTMLButtonElement).value === "true"; + (e.currentTarget as HTMLButtonElement).value === "true"; if (state.customTextPipeDelimiter && state.customTextLimits.word !== "") { state.customTextLimits.word = ""; } @@ -465,7 +465,7 @@ async function setup(modalEl: ElementWithUtils): Promise { modalEl .qsa(".group[data-id='newlines'] button") .on("click", (e: MouseEvent) => { - state.replaceNewlines = (e.target as HTMLButtonElement).value as + state.replaceNewlines = (e.currentTarget as HTMLButtonElement).value as | "off" | "space" | "periodSpace"; @@ -473,21 +473,23 @@ async function setup(modalEl: ElementWithUtils): Promise { }); modalEl.qs(".group[data-id='limit'] input.words")?.on("input", (e) => { - state.customTextLimits.word = (e.target as HTMLInputElement).value; + state.customTextLimits.word = (e.currentTarget as HTMLInputElement).value; state.customTextLimits.time = ""; state.customTextLimits.section = ""; updateUI(); }); modalEl.qs(".group[data-id='limit'] input.time")?.on("input", (e) => { - state.customTextLimits.time = (e.target as HTMLInputElement).value; + state.customTextLimits.time = (e.currentTarget as HTMLInputElement).value; state.customTextLimits.word = ""; state.customTextLimits.section = ""; updateUI(); }); modalEl.qs(".group[data-id='limit'] input.sections")?.on("input", (e) => { - state.customTextLimits.section = (e.target as HTMLInputElement).value; + state.customTextLimits.section = ( + e.currentTarget as HTMLInputElement + ).value; state.customTextLimits.word = ""; state.customTextLimits.time = ""; updateUI(); @@ -495,13 +497,13 @@ async function setup(modalEl: ElementWithUtils): Promise { const textarea = modalEl.qs("textarea"); textarea?.on("input", (e) => { - state.textarea = (e.target as HTMLTextAreaElement).value; + state.textarea = (e.currentTarget as HTMLTextAreaElement).value; }); textarea?.on("keydown", (e) => { if (e.key !== "Tab") return; e.preventDefault(); - const area = e.target as HTMLTextAreaElement; + const area = e.currentTarget as HTMLTextAreaElement; const start: number = area.selectionStart; const end: number = area.selectionEnd; From 1784cb9072f60b4f169672844c7d3f1908519273 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 14:44:53 +0100 Subject: [PATCH 17/23] current target --- frontend/src/ts/modals/import-export-settings.ts | 2 +- frontend/src/ts/modals/practise-words.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/modals/import-export-settings.ts b/frontend/src/ts/modals/import-export-settings.ts index 5d28ea700c7d..1a28159b646b 100644 --- a/frontend/src/ts/modals/import-export-settings.ts +++ b/frontend/src/ts/modals/import-export-settings.ts @@ -34,7 +34,7 @@ const modal = new AnimatedModal({ dialogId: "importExportSettingsModal", setup: async (modalEl): Promise => { modalEl.qs("input")?.on("input", (e) => { - state.value = (e.target as HTMLInputElement).value; + state.value = (e.currentTarget as HTMLInputElement).value; }); modalEl?.on("submit", async (e) => { e.preventDefault(); diff --git a/frontend/src/ts/modals/practise-words.ts b/frontend/src/ts/modals/practise-words.ts index 8e27807e9bf3..e7c0b08c4d56 100644 --- a/frontend/src/ts/modals/practise-words.ts +++ b/frontend/src/ts/modals/practise-words.ts @@ -35,7 +35,7 @@ function updateUI(): void { async function setup(modalEl: ElementWithUtils): Promise { modalEl.qsa(".group[data-id='missed'] button").on("click", (e) => { - state.missed = (e.target as HTMLButtonElement).value as + state.missed = (e.currentTarget as HTMLButtonElement).value as | "off" | "words" | "biwords"; @@ -43,7 +43,7 @@ async function setup(modalEl: ElementWithUtils): Promise { }); modalEl.qsa(".group[data-id='slow'] button").on("click", (e) => { - state.slow = (e.target as HTMLButtonElement).value === "true"; + state.slow = (e.currentTarget as HTMLButtonElement).value === "true"; updateUI(); }); From e2bb709308b72fd1c77760898b33e4157c471333 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 15:28:12 +0100 Subject: [PATCH 18/23] simplify --- frontend/src/ts/utils/animated-modal.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/ts/utils/animated-modal.ts b/frontend/src/ts/utils/animated-modal.ts index 8f928151b56f..06349d713a62 100644 --- a/frontend/src/ts/utils/animated-modal.ts +++ b/frontend/src/ts/utils/animated-modal.ts @@ -183,10 +183,7 @@ export default class AnimatedModal< } focusFirstInput(setting: true | "focusAndSelect" | undefined): void { - const inputs = [ - ...this.modalEl.qsa("input"), - ] as ElementWithUtils[]; - const input = inputs.find((input) => !input.hasClass("hidden")); + const input = this.modalEl.qs("input:not(.hidden)"); if (input !== undefined && input !== null) { if (setting === true) { input.focus(); From a1f3a0b19ea5daed9128c34327d1d1ac7ee68f3b Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 16:01:28 +0100 Subject: [PATCH 19/23] optional --- frontend/src/ts/modals/streak-hour-offset.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/modals/streak-hour-offset.ts b/frontend/src/ts/modals/streak-hour-offset.ts index b265e2869f93..2a6c53ec8b51 100644 --- a/frontend/src/ts/modals/streak-hour-offset.ts +++ b/frontend/src/ts/modals/streak-hour-offset.ts @@ -36,7 +36,7 @@ export function show(): void { function updatePreview(): void { const inputValue = parseInt( - modal.getModal().qs("input")?.getValue() as string, + modal.getModal().qs("input")?.getValue() ?? "", 10, ); From 53973771939329bf5265ae491f826da51d881756 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 16:02:01 +0100 Subject: [PATCH 20/23] optional --- frontend/src/ts/modals/streak-hour-offset.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/modals/streak-hour-offset.ts b/frontend/src/ts/modals/streak-hour-offset.ts index 2a6c53ec8b51..5b1dbc36a654 100644 --- a/frontend/src/ts/modals/streak-hour-offset.ts +++ b/frontend/src/ts/modals/streak-hour-offset.ts @@ -68,7 +68,7 @@ function hide(): void { async function apply(): Promise { const value = parseInt( - modal.getModal().qs("input")?.getValue() as string, + modal.getModal().qs("input")?.getValue() ?? "", 10, ); From cc12403fb7ffb894ccf07d66b12440a8c0c7228f Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 16:49:38 +0100 Subject: [PATCH 21/23] review --- frontend/src/ts/utils/animated-modal.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/ts/utils/animated-modal.ts b/frontend/src/ts/utils/animated-modal.ts index 06349d713a62..36bd52ea3bca 100644 --- a/frontend/src/ts/utils/animated-modal.ts +++ b/frontend/src/ts/utils/animated-modal.ts @@ -301,8 +301,7 @@ export default class AnimatedModal< }, }); } else if (animationMode === "modalOnly") { - this.wrapperEl.setStyle({ opacity: "1" }); - this.wrapperEl.show(); + this.wrapperEl.show().setStyle({ opacity: "1" }); this.modalEl.animate({ ...modalAnimation, From d99261f2aae374f6d3392cf0d9759500139effd6 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 17:34:29 +0100 Subject: [PATCH 22/23] back to qsa --- frontend/src/ts/utils/animated-modal.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/utils/animated-modal.ts b/frontend/src/ts/utils/animated-modal.ts index 36bd52ea3bca..2bf2085abf6d 100644 --- a/frontend/src/ts/utils/animated-modal.ts +++ b/frontend/src/ts/utils/animated-modal.ts @@ -183,7 +183,10 @@ export default class AnimatedModal< } focusFirstInput(setting: true | "focusAndSelect" | undefined): void { - const input = this.modalEl.qs("input:not(.hidden)"); + const inputs = [ + ...this.modalEl.qsa("input"), + ] as ElementWithUtils[]; + const input = inputs.find((input) => !input.hasClass("hidden")); if (input !== undefined && input !== null) { if (setting === true) { input.focus(); From 4e838fc84f4a7816e2154c82c7c2013e91c44659 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 2 Jan 2026 17:44:39 +0100 Subject: [PATCH 23/23] ayop --- frontend/src/ts/utils/animated-modal.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/src/ts/utils/animated-modal.ts b/frontend/src/ts/utils/animated-modal.ts index 2bf2085abf6d..52f832fbeb8e 100644 --- a/frontend/src/ts/utils/animated-modal.ts +++ b/frontend/src/ts/utils/animated-modal.ts @@ -183,10 +183,7 @@ export default class AnimatedModal< } focusFirstInput(setting: true | "focusAndSelect" | undefined): void { - const inputs = [ - ...this.modalEl.qsa("input"), - ] as ElementWithUtils[]; - const input = inputs.find((input) => !input.hasClass("hidden")); + const input = this.modalEl.qsa("input:not(.hidden)")[0]; if (input !== undefined && input !== null) { if (setting === true) { input.focus();