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-generator.ts b/frontend/src/ts/modals/custom-generator.ts index 7c4d77fba5a4..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; @@ -83,7 +84,7 @@ export async function show(showOptions?: ShowOptions): Promise { _presetSelect = new SlimSelect({ select: "#customGeneratorModal .presetInput", settings: { - contentLocation: modalEl, + contentLocation: modalEl.native, }, }); }, @@ -159,16 +160,16 @@ async function apply(set: boolean): Promise { }); } -async function setup(modalEl: HTMLElement): Promise { - modalEl.querySelector(".setButton")?.addEventListener("click", () => { +async function setup(modalEl: 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-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/custom-text.ts b/frontend/src/ts/modals/custom-text.ts index f7f81c772f59..d46fb91ceb0a 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,64 +405,50 @@ function handleDelimiterChange(): void { state.textarea = newtext; } -async function setup(modalEl: HTMLElement): Promise { - modalEl - .querySelector("#fileInput") - ?.addEventListener("change", handleFileOpen); - - const buttons = modalEl.querySelectorAll(".group[data-id='mode'] button"); - for (const button of buttons) { - button.addEventListener("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(); - }); - } +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.currentTarget 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.querySelectorAll( - ".group[data-id='fancy'] button", - )) { - button.addEventListener("click", (e) => { - state.removeFancyTypographyEnabled = - (e.target as HTMLButtonElement).value === "true"; - updateUI(); - }); - } + modalEl.qsa(".group[data-id='fancy'] button").on("click", (e: MouseEvent) => { + state.removeFancyTypographyEnabled = + (e.currentTarget as HTMLButtonElement).value === "true"; + updateUI(); + }); - for (const button of modalEl.querySelectorAll( - ".group[data-id='control'] button", - )) { - button.addEventListener("click", (e) => { + modalEl + .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(); }); - } - for (const button of modalEl.querySelectorAll( - ".group[data-id='zeroWidth'] button", - )) { - button.addEventListener("click", (e) => { + modalEl + .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(); }); - } - for (const button of modalEl.querySelectorAll( - ".group[data-id='delimiter'] button", - )) { - button.addEventListener("click", (e) => { + modalEl + .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 = ""; } @@ -474,56 +461,49 @@ async function setup(modalEl: HTMLElement): Promise { handleDelimiterChange(); updateUI(); }); - } - for (const button of modalEl.querySelectorAll( - ".group[data-id='newlines'] button", - )) { - button.addEventListener("click", (e) => { - state.replaceNewlines = (e.target as HTMLButtonElement).value as + modalEl + .qsa(".group[data-id='newlines'] button") + .on("click", (e: MouseEvent) => { + state.replaceNewlines = (e.currentTarget as HTMLButtonElement).value as | "off" | "space" | "periodSpace"; updateUI(); }); - } - 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.currentTarget 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.currentTarget 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.currentTarget as HTMLInputElement + ).value; + state.customTextLimits.word = ""; + state.customTextLimits.time = ""; + updateUI(); + }); - const textarea = modalEl.querySelector("textarea"); - textarea?.addEventListener("input", (e) => { - state.textarea = (e.target as HTMLTextAreaElement).value; + const textarea = modalEl.qs("textarea"); + textarea?.on("input", (e) => { + state.textarea = (e.currentTarget as HTMLTextAreaElement).value; }); - textarea?.addEventListener("keydown", (e) => { + 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; @@ -536,7 +516,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 +535,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..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,18 +412,18 @@ function getConfigChanges(): Partial { }; } -async function setup(modalEl: HTMLElement): Promise { - modalEl.addEventListener("submit", (e) => { +async function setup(modalEl: 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-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/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/forgot-password.ts b/frontend/src/ts/modals/forgot-password.ts index cae09613f961..504b8f312997 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,10 @@ 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 === "") { Notifications.add("Please enter your email address"); CaptchaController.reset("forgotPasswordModal"); return; @@ -79,8 +79,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/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/import-export-settings.ts b/frontend/src/ts/modals/import-export-settings.ts index d23ca53a5825..1a28159b646b 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) => { - state.value = (e.target as HTMLInputElement).value; + modalEl.qs("input")?.on("input", (e) => { + state.value = (e.currentTarget 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/modals/mobile-test-config.ts b/frontend/src/ts/modals/mobile-test-config.ts index 8cd475107f6a..59f393580650 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,120 +78,105 @@ export function show(): void { // void modal.hide(); // } -async function setup(modalEl: HTMLElement): Promise { - const wordsGroupButtons = modalEl.querySelectorAll(".wordsGroup button"); - for (const button of wordsGroupButtons) { - button.addEventListener("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(); - } - }); - } +async function setup(modalEl: ElementWithUtils): Promise { + 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.querySelectorAll(".modeGroup button"); - for (const button of modeGroupButtons) { - button.addEventListener("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.querySelectorAll(".timeGroup button"); - for (const button of timeGroupButtons) { - button.addEventListener("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.querySelectorAll(".quoteGroup button"); - for (const button of quoteGroupButtons) { - button.addEventListener("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]; } - }); - } - modalEl.querySelector(".customChange")?.addEventListener("click", () => { + if (setConfig("quoteLength", arr)) { + ManualRestart.set(); + TestLogic.restart(); + } + } + }); + + 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"); - for (const button of buttons) { - button.addEventListener("click", () => { - update(); - }); - } + modalEl.qsa("button").on("click", () => { + update(); + }); } const modal = new AnimatedModal({ 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..e7c0b08c4d56 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,33 +33,25 @@ function updateUI(): void { } } -async function setup(modalEl: HTMLElement): Promise { - for (const button of modalEl.querySelectorAll( - ".group[data-id='missed'] button", - )) { - button.addEventListener("click", (e) => { - state.missed = (e.target as HTMLButtonElement).value as - | "off" - | "words" - | "biwords"; - updateUI(); - }); - } +async function setup(modalEl: ElementWithUtils): Promise { + modalEl.qsa(".group[data-id='missed'] button").on("click", (e) => { + state.missed = (e.currentTarget as HTMLButtonElement).value as + | "off" + | "words" + | "biwords"; + updateUI(); + }); - for (const button of modalEl.querySelectorAll( - ".group[data-id='slow'] button", - )) { - button.addEventListener("click", (e) => { - state.slow = (e.target as HTMLButtonElement).value === "true"; - updateUI(); - }); - } + modalEl.qsa(".group[data-id='slow'] button").on("click", (e) => { + state.slow = (e.currentTarget 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..fd05f192f1aa 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,29 +209,28 @@ 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"); - for (const button of starButtons) { - button.addEventListener("click", (e) => { - const ratingValue = parseInt( - (e.currentTarget as HTMLElement).getAttribute("data-rating") as string, - ); - rating = ratingValue; - refreshStars(); - }); - button.addEventListener("mouseenter", (e) => { - const ratingHover = parseInt( - (e.currentTarget as HTMLElement).getAttribute("data-rating") as string, - ); - refreshStars(ratingHover); - }); - button.addEventListener("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({ 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..7701ee93fdc9 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,44 +311,47 @@ async function updateResults(searchText: string): Promise { resultsList.append(quoteSearchResult); }); - const searchResults = modal - .getModal() - .querySelectorAll(".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, - }); - }); - searchResult.addEventListener("click", (e) => { - TestState.setSelectedQuoteId(quoteId); - apply(quoteId); + const searchResults = modal.getModal().qsa(".searchResult"); + 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; @@ -382,7 +386,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 +499,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.currentTarget as HTMLElement)?.classList.toggle("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..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.querySelector(".g-recaptcha") as HTMLElement, + modal.qsr(".g-recaptcha").native, "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..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,14 +87,14 @@ function save(): boolean { } } -async function setup(modalEl: HTMLElement): Promise { - modalEl.addEventListener("submit", (e) => { +async function setup(modalEl: 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..49490ffae307 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,20 +78,15 @@ 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"); - for (const input of inputs) { - input.addEventListener("change", async () => { - updateURL(); - updateSubgroups(); - }); - } + modalEl.qsa("label input").on("change", async () => { + updateURL(); + updateSubgroups(); + }); } const modal = new AnimatedModal({ diff --git a/frontend/src/ts/modals/streak-hour-offset.ts b/frontend/src/ts/modals/streak-hour-offset.ts index f2f9d90aa3c9..5b1dbc36a654 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() ?? "", 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() ?? "", 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..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.querySelector(".g-recaptcha") as HTMLElement, + modalEl.qsr(".g-recaptcha").native, "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..fa676c5a5e77 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,15 +239,11 @@ function setExactMatchInput(disable: boolean): void { } function disableButtons(): void { - for (const button of modal.getModal().querySelectorAll("button")) { - button.setAttribute("disabled", "true"); - } + modal.getModal().qsa("button").disable(); } function enableButtons(): void { - for (const button of modal.getModal().querySelectorAll("button")) { - button.removeAttribute("disabled"); - } + modal.getModal().qsa("button").enable(); } async function setup(): Promise { diff --git a/frontend/src/ts/utils/animated-modal.ts b/frontend/src/ts/utils/animated-modal.ts index 6400b6e2eb2c..52f832fbeb8e 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 { 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( @@ -95,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"); } @@ -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,7 @@ 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 input = this.modalEl.qsa("input:not(.hidden)")[0]; if (input !== undefined && input !== null) { if (setting === true) { input.focus(); @@ -238,7 +241,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); @@ -274,19 +277,19 @@ export default class AnimatedModal< if (animationMode === "both" || animationMode === "none") { if (hasModalAnimation) { - animate(this.modalEl, { + this.modalEl.animate({ ...modalAnimation, duration: animationMode === "none" ? 0 : modalAnimationDuration, }); } else { - this.modalEl.style.opacity = "1"; + this.modalEl.setStyle({ opacity: "1" }); } - animate(this.wrapperEl, { + this.wrapperEl.animate({ ...wrapperAnimation, duration: animationMode === "none" ? 0 : wrapperAnimationDuration, onBegin: () => { - this.wrapperEl.classList.remove("hidden"); + this.wrapperEl.show(); }, onComplete: async () => { this.focusFirstInput(options?.focusFirstInput); @@ -298,9 +301,9 @@ export default class AnimatedModal< }, }); } else if (animationMode === "modalOnly") { - $(this.wrapperEl).removeClass("hidden").css("opacity", "1"); + this.wrapperEl.show().setStyle({ opacity: "1" }); - animate(this.modalEl, { + this.modalEl.animate({ ...modalAnimation, duration: modalAnimationDuration, onComplete: async () => { @@ -364,20 +367,20 @@ export default class AnimatedModal< if (animationMode === "both" || animationMode === "none") { if (hasModalAnimation) { - animate(this.modalEl, { + this.modalEl.animate({ ...modalAnimation, duration: animationMode === "none" ? 0 : modalAnimationDuration, }); } else { - this.modalEl.style.opacity = "1"; + this.modalEl.setStyle({ opacity: "1" }); } - animate(this.wrapperEl, { + this.wrapperEl.animate({ ...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); @@ -401,14 +404,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, { + this.modalEl.animate({ ...modalAnimation, duration: modalAnimationDuration, onComplete: async () => { - this.wrapperEl.close(); - $(this.wrapperEl).addClass("hidden").css("opacity", "0"); + this.wrapperEl.native.close(); + this.wrapperEl.hide().setStyle({ opacity: "0" }); Skeleton.remove(this.dialogId); this.open = false; await options?.afterAnimation?.(this.modalEl); @@ -436,8 +439,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..4e1088691bc4 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(); + } + } } /** 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(); });