From 4efae156b0146fab06f3c5b5597cf3ba6222e9ac Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 15:55:05 +0800 Subject: [PATCH 01/28] refactor: remove jquery in modals/custom-generator --- frontend/src/ts/modals/custom-generator.ts | 35 +++++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/frontend/src/ts/modals/custom-generator.ts b/frontend/src/ts/modals/custom-generator.ts index 7c4d77fba5a4..d1179604a27a 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 { qs, qsr } from "../utils/dom"; type Preset = { display: string; @@ -91,10 +92,15 @@ export async function show(showOptions?: ShowOptions): Promise { } function applyPreset(): void { - const presetName = $("#customGeneratorModal .presetInput").val() as string; + const presetName = qs( + "#customGeneratorModal .presetInput", + )?.getValue(); + if (presetName !== undefined && presetName !== "" && presets[presetName]) { const preset = presets[presetName]; - $("#customGeneratorModal .characterInput").val(preset.characters.join(" ")); + qsr("#customGeneratorModal .characterInput").setValue( + preset.characters.join(" "), + ); } } @@ -105,17 +111,30 @@ function hide(hideOptions?: HideOptions): void { } function generateWords(): string[] { - const characterInput = $( + const characterInput = qs( "#customGeneratorModal .characterInput", - ).val() as string; + )?.getValue(); + const minLength = - parseInt($("#customGeneratorModal .minLengthInput").val() as string) || 2; + parseInt( + qs( + "#customGeneratorModal .minLengthInput", + )?.getValue() as string, + ) || 2; const maxLength = - parseInt($("#customGeneratorModal .maxLengthInput").val() as string) || 5; + parseInt( + qs( + "#customGeneratorModal .maxLengthInput", + )?.getValue() as string, + ) || 5; const wordCount = - parseInt($("#customGeneratorModal .wordCountInput").val() as string) || 100; + parseInt( + qs( + "#customGeneratorModal .wordCountInput", + )?.getValue() as string, + ) || 100; - if (!characterInput || characterInput.trim() === "") { + if (characterInput === undefined || characterInput.trim() === "") { Notifications.add("Character set cannot be empty", 0); return []; } From 8e839980cba9b90f21a6f56cf6b29c9330760408 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 16:29:51 +0800 Subject: [PATCH 02/28] refactor: remove jquery in modals/custom-test-duration --- frontend/src/ts/modals/custom-test-duration.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/modals/custom-test-duration.ts b/frontend/src/ts/modals/custom-test-duration.ts index 244524417a9c..674d01371a4a 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 { qs, qsr } from "../utils/dom"; function parseInput(input: string): number { const re = /((-\s*)?\d+(\.\d+)?\s*[hms]?)/g; @@ -53,7 +54,9 @@ function format(duration: number): string { } function previewDuration(): void { - const input = $("#customTestDurationModal input").val() as string; + const input = qsr( + "#customTestDurationModal input", + ).getValue() as string; const duration = parseInput(input); let formattedDuration = ""; @@ -65,7 +68,7 @@ function previewDuration(): void { formattedDuration = format(duration); } - $("#customTestDurationModal .preview").text(formattedDuration); + qs("#customTestDurationModal .preview")?.setText(formattedDuration); } export function show(showOptions?: ShowOptions): void { @@ -87,7 +90,11 @@ function hide(clearChain = false): void { } function apply(): void { - const val = parseInput($("#customTestDurationModal input").val() as string); + const val = parseInput( + qsr( + "#customTestDurationModal input", + ).getValue() as string, + ); if (val !== null && !isNaN(val) && val >= 0 && isFinite(val)) { setConfig("time", val); From dcd87557b5ee42cd637a4207bf2a337ef5cc6f06 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 17:50:44 +0800 Subject: [PATCH 03/28] feat(dom): add setValue method in ElementsWithUtils --- frontend/src/ts/utils/dom.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/src/ts/utils/dom.ts b/frontend/src/ts/utils/dom.ts index 9b0d5626302f..de41eb6322d5 100644 --- a/frontend/src/ts/utils/dom.ts +++ b/frontend/src/ts/utils/dom.ts @@ -743,6 +743,16 @@ export class ElementsWithUtils< return this; } + /** + * Set value of all input elements in the array + */ + setValue(this: ElementsWithUtils, value: string): this { + for (const item of this) { + item.setValue(value); + } + return this as unknown as this; + } + /** * Query all elements in the array for a child element matching the selector */ From b4550fb304709f01781f8d48b750fd60e02191f6 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 17:51:51 +0800 Subject: [PATCH 04/28] refactor: remove jquery in modals/custom-text --- frontend/src/ts/modals/custom-text.ts | 147 ++++++++++++-------------- 1 file changed, 70 insertions(+), 77 deletions(-) diff --git a/frontend/src/ts/modals/custom-text.ts b/frontend/src/ts/modals/custom-text.ts index f7f81c772f59..dde8c82ab818 100644 --- a/frontend/src/ts/modals/custom-text.ts +++ b/frontend/src/ts/modals/custom-text.ts @@ -13,6 +13,7 @@ import * as SavedTextsPopup from "./saved-texts"; import * as SaveCustomTextPopup from "./save-custom-text"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import { CustomTextMode } from "@monkeytype/schemas/util"; +import { qs, qsa } from "../utils/dom"; const popup = "#customTextModal .modal"; @@ -54,114 +55,106 @@ const state: State = { }; function updateUI(): void { - $(`${popup} .inputs .group[data-id="mode"] button`).removeClass("active"); - $( + qsa(`${popup} .inputs .group[data-id="mode"] button`)?.removeClass("active"); + qs( `${popup} .inputs .group[data-id="mode"] button[value="${state.customTextMode}"]`, - ).addClass("active"); - - $(`${popup} .inputs .group[data-id="limit"] input.words`).addClass("hidden"); - $(`${popup} .inputs .group[data-id="limit"] input.sections`).addClass( - "hidden", - ); - - $(`${popup} .inputs .group[data-id="limit"] input.words`).val( - state.customTextLimits.word, - ); - $(`${popup} .inputs .group[data-id="limit"] input.time`).val( - state.customTextLimits.time, - ); - $(`${popup} .inputs .group[data-id="limit"] input.sections`).val( - state.customTextLimits.section, - ); + )?.addClass("active"); + + qs(`${popup} .inputs .group[data-id="limit"] input.words`)?.hide(); + qs(`${popup} .inputs .group[data-id="limit"] input.sections`)?.hide(); + + qs( + `${popup} .inputs .group[data-id="limit"] input.words`, + )?.setValue(state.customTextLimits.word); + qs( + `${popup} .inputs .group[data-id="limit"] input.time`, + )?.setValue(state.customTextLimits.time); + qs( + `${popup} .inputs .group[data-id="limit"] input.sections`, + )?.setValue(state.customTextLimits.section); if (state.customTextLimits.word !== "") { - $(`${popup} .inputs .group[data-id="limit"] input.words`).removeClass( - "hidden", - ); + qs(`${popup} .inputs .group[data-id="limit"] input.words`)?.show(); } if (state.customTextLimits.section !== "") { - $(`${popup} .inputs .group[data-id="limit"] input.sections`).removeClass( - "hidden", - ); + qs(`${popup} .inputs .group[data-id="limit"] input.sections`)?.show(); } if (state.customTextPipeDelimiter) { - $(`${popup} .inputs .group[data-id="limit"] input.sections`).removeClass( - "hidden", - ); - $(`${popup} .inputs .group[data-id="limit"] input.words`).addClass( - "hidden", - ); + qsa(`${popup} .inputs .group[data-id="limit"] input.sections`)?.show(); + qsa(`${popup} .inputs .group[data-id="limit"] input.words`)?.hide(); } else { - $(`${popup} .inputs .group[data-id="limit"] input.words`).removeClass( - "hidden", - ); - $(`${popup} .inputs .group[data-id="limit"] input.sections`).addClass( - "hidden", - ); + qsa(`${popup} .inputs .group[data-id="limit"] input.words`)?.show(); + qsa(`${popup} .inputs .group[data-id="limit"] input.sections`)?.hide(); } if (state.customTextMode === "simple") { - $(`${popup} .inputs .group[data-id="limit"]`).addClass("disabled"); - $(`${popup} .inputs .group[data-id="limit"] input`).val(""); - $(`${popup} .inputs .group[data-id="limit"] input`).prop("disabled", true); + qs(`${popup} .inputs .group[data-id="limit"]`)?.addClass("disabled"); + qsa( + `${popup} .inputs .group[data-id="limit"] input`, + )?.setValue(""); + qsa(`${popup} .inputs .group[data-id="limit"] input`)?.disable(); } else { - $(`${popup} .inputs .group[data-id="limit"]`).removeClass("disabled"); - $(`${popup} .inputs .group[data-id="limit"] input`).prop("disabled", false); + qs(`${popup} .inputs .group[data-id="limit"]`)?.removeClass("disabled"); + qsa(`${popup} .inputs .group[data-id="limit"] input`)?.enable(); } - $(`${popup} .inputs .group[data-id="fancy"] button`).removeClass("active"); - $( + qsa(`${popup} .inputs .group[data-id="fancy"] button`)?.removeClass("active"); + qs( `${popup} .inputs .group[data-id="fancy"] button[value="${state.removeFancyTypographyEnabled}"]`, - ).addClass("active"); + )?.addClass("active"); - $(`${popup} .inputs .group[data-id="control"] button`).removeClass("active"); - $( + qsa(`${popup} .inputs .group[data-id="control"] button`)?.removeClass( + "active", + ); + qs( `${popup} .inputs .group[data-id="control"] button[value="${state.replaceControlCharactersEnabled}"]`, - ).addClass("active"); + )?.addClass("active"); - $(`${popup} .inputs .group[data-id="zeroWidth"] button`).removeClass( + qsa(`${popup} .inputs .group[data-id="zeroWidth"] button`)?.removeClass( "active", ); - $( + qs( `${popup} .inputs .group[data-id="zeroWidth"] button[value="${state.removeZeroWidthCharactersEnabled}"]`, - ).addClass("active"); + )?.addClass("active"); - $(`${popup} .inputs .group[data-id="delimiter"] button`).removeClass( + qsa(`${popup} .inputs .group[data-id="delimiter"] button`)?.removeClass( "active", ); - $( + qs( `${popup} .inputs .group[data-id="delimiter"] button[value="${state.customTextPipeDelimiter}"]`, - ).addClass("active"); + )?.addClass("active"); - $(`${popup} .inputs .group[data-id="newlines"] button`).removeClass("active"); - $( + qsa(`${popup} .inputs .group[data-id="newlines"] button`)?.removeClass( + "active", + ); + qs( `${popup} .inputs .group[data-id="newlines"] button[value="${state.replaceNewlines}"]`, - ).addClass("active"); + )?.addClass("active"); - $(`${popup} textarea`).val(state.textarea); + qs(`${popup} textarea`)?.setValue(state.textarea); if (state.longCustomTextWarning) { - $(`${popup} .longCustomTextWarning`).removeClass("hidden"); - $(`${popup} .randomWordsCheckbox input`).prop("checked", false); - $(`${popup} .delimiterCheck input`).prop("checked", false); - $(`${popup} .typographyCheck`).prop("checked", true); - $(`${popup} .replaceNewlineWithSpace input`).prop("checked", false); - $(`${popup} .inputs`).addClass("disabled"); + qs(`${popup} .longCustomTextWarning`)?.show(); + qs(`${popup} .randomWordsCheckbox input`)?.removeAttribute("checked"); + qs(`${popup} .delimiterCheck input`)?.removeAttribute("checked"); + qs(`${popup} .typographyCheck`)?.setAttribute("checked", "true"); + qs(`${popup} .replaceNewlineWithSpace input`)?.removeAttribute("checked"); + qs(`${popup} .inputs`)?.addClass("disabled"); } else { - $(`${popup} .longCustomTextWarning`).addClass("hidden"); - $(`${popup} .inputs`).removeClass("disabled"); + qs(`${popup} .longCustomTextWarning`)?.hide(); + qs(`${popup} .inputs`)?.removeClass("disabled"); } if (state.challengeWarning) { - $(`${popup} .challengeWarning`).removeClass("hidden"); - $(`${popup} .randomWordsCheckbox input`).prop("checked", false); - $(`${popup} .delimiterCheck input`).prop("checked", false); - $(`${popup} .typographyCheck`).prop("checked", true); - $(`${popup} .replaceNewlineWithSpace input`).prop("checked", false); - $(`${popup} .inputs`).addClass("disabled"); + qs(`${popup} .challengeWarning`)?.show(); + qs(`${popup} .randomWordsCheckbox input`)?.removeAttribute("checked"); + qs(`${popup} .delimiterCheck input`)?.removeAttribute("checked"); + qs(`${popup} .typographyCheck`)?.setAttribute("checked", "true"); + qs(`${popup} .replaceNewlineWithSpace input`)?.removeAttribute("checked"); + qs(`${popup} .inputs`)?.addClass("disabled"); } else { - $(`${popup} .challengeWarning`).addClass("hidden"); - $(`${popup} .inputs`).removeClass("disabled"); + qs(`${popup} .challengeWarning`)?.hide(); + qs(`${popup} .inputs`)?.removeClass("disabled"); } } @@ -218,7 +211,7 @@ async function beforeAnimation( async function afterAnimation(): Promise { if (!state.challengeWarning && !state.longCustomTextWarning) { - $(`${popup} textarea`).trigger("focus"); + qs(`${popup} textarea`)?.native.focus(); } } @@ -238,7 +231,7 @@ function hide(): void { } function handleFileOpen(): void { - const file = ($(`#fileInput`)[0] as HTMLInputElement).files?.[0]; + const file = qs("#fileInput")?.native.files?.[0]; if (file) { if (file.type !== "text/plain") { Notifications.add("File is not a text file", -1, { @@ -254,7 +247,7 @@ function handleFileOpen(): void { const content = readerEvent.target?.result as string; state.textarea = content; updateUI(); - $(`#fileInput`).val(""); + qs(`#fileInput`)?.setValue(""); }; reader.onerror = (): void => { Notifications.add("Failed to read file", -1, { @@ -542,7 +535,7 @@ async function setup(modalEl: HTMLElement): Promise { return; } if (e.code === "Enter" && e.ctrlKey) { - $(`${popup} .button.apply`).trigger("click"); + qs(`${popup} .button.apply`)?.dispatch("click"); } if ( CustomTextState.isCustomTextLong() && From 60119551964193d39dafdad50c08eccfa43dac40 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 18:17:40 +0800 Subject: [PATCH 05/28] refactor: remove jquery in modals/dev-options --- frontend/src/ts/modals/dev-options.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/modals/dev-options.ts b/frontend/src/ts/modals/dev-options.ts index 6c254688ada8..ffb931748b13 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 { qsr } from "../utils/dom"; let mediaQueryDebugLevel = 0; @@ -111,7 +112,7 @@ const modal = new AnimatedModal({ }); export function appendButton(): void { - $("body").prepend( + qsr("body").prependHtml( `
From 326531d38ff798ec16d916be483455aeb94a39cf Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 20:20:04 +0800 Subject: [PATCH 06/28] refactor: remove jquery in modals/edit-preset --- frontend/src/ts/modals/edit-preset.ts | 148 ++++++++++++++------------ 1 file changed, 79 insertions(+), 69 deletions(-) diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index 443df2f242a5..1ca5a4cf4d4d 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 { qsa, qsr } from "../utils/dom"; import { configMetadata } from "../config-metadata"; const state = { @@ -45,7 +45,7 @@ export function show(action: string, id?: string, name?: string): void { void modal.show({ focusFirstInput: true, beforeAnimation: async () => { - $("#editPresetModal .modal .text").addClass("hidden"); + qsr("#editPresetModal .modal .text").hide(); addCheckBoxes(); presetNameEl ??= new ValidatedHtmlInputElement( qsr("#editPresetModal .modal input[type=text]"), @@ -54,32 +54,32 @@ export function show(action: string, id?: string, name?: string): void { }, ); if (action === "add") { - $("#editPresetModal .modal").attr("data-action", "add"); - $("#editPresetModal .modal .popupTitle").html("Add new preset"); - $("#editPresetModal .modal .submit").html(`add`); + qsr("#editPresetModal .modal").setAttribute("data-action", "add"); + qsr("#editPresetModal .modal .popupTitle").setHtml("Add new preset"); + qsr("#editPresetModal .modal .submit").setHtml("add"); presetNameEl.setValue(null); - presetNameEl.getParent()?.removeClass("hidden"); - $("#editPresetModal .modal input").removeClass("hidden"); - $( + presetNameEl.getParent()?.show(); + qsa("#editPresetModal .modal input").show(); + qsr( "#editPresetModal .modal label.changePresetToCurrentCheckbox", - ).addClass("hidden"); - $("#editPresetModal .modal .inputs").removeClass("hidden"); - $("#editPresetModal .modal .presetType").removeClass("hidden"); - $("#editPresetModal .modal .presetNameTitle").removeClass("hidden"); + ).hide(); + qsr("#editPresetModal .modal .inputs").show(); + qsr("#editPresetModal .modal .presetType").show(); + qsr("#editPresetModal .modal .presetNameTitle").show(); state.presetType = "full"; } else if (action === "edit" && id !== undefined && name !== undefined) { - $("#editPresetModal .modal").attr("data-action", "edit"); - $("#editPresetModal .modal").attr("data-preset-id", id); - $("#editPresetModal .modal .popupTitle").html("Edit preset"); - $("#editPresetModal .modal .submit").html(`save`); + qsr("#editPresetModal .modal").setAttribute("data-action", "edit"); + qsr("#editPresetModal .modal").setAttribute("data-preset-id", id); + qsr("#editPresetModal .modal .popupTitle").setHtml("Edit preset"); + qsr("#editPresetModal .modal .submit").setHtml(`save`); presetNameEl?.setValue(name); - presetNameEl?.getParent()?.removeClass("hidden"); + presetNameEl?.getParent()?.show(); - $("#editPresetModal .modal input").removeClass("hidden"); - $( + qsa("#editPresetModal .modal input").show(); + qsr( "#editPresetModal .modal label.changePresetToCurrentCheckbox", - ).removeClass("hidden"); - $("#editPresetModal .modal .presetNameTitle").removeClass("hidden"); + ).show(); + qsr("#editPresetModal .modal .presetNameTitle").show(); state.setPresetToCurrent = false; await updateEditPresetUI(); } else if ( @@ -87,22 +87,22 @@ export function show(action: string, id?: string, name?: string): void { id !== undefined && name !== undefined ) { - $("#editPresetModal .modal").attr("data-action", "remove"); - $("#editPresetModal .modal").attr("data-preset-id", id); - $("#editPresetModal .modal .popupTitle").html("Delete preset"); - $("#editPresetModal .modal .submit").html("delete"); - $("#editPresetModal .modal input").addClass("hidden"); - $( + qsr("#editPresetModal .modal").setAttribute("data-action", "remove"); + qsr("#editPresetModal .modal").setAttribute("data-preset-id", id); + qsr("#editPresetModal .modal .popupTitle").setHtml("Delete preset"); + qsr("#editPresetModal .modal .submit").setHtml("delete"); + qsa("#editPresetModal .modal input").hide(); + qsr( "#editPresetModal .modal label.changePresetToCurrentCheckbox", - ).addClass("hidden"); - $("#editPresetModal .modal .text").removeClass("hidden"); - $("#editPresetModal .modal .deletePrompt").text( + ).hide(); + qsr("#editPresetModal .modal .text").show(); + qsr("#editPresetModal .modal .deletePrompt").setText( `Are you sure you want to delete the preset ${name}?`, ); - $("#editPresetModal .modal .inputs").addClass("hidden"); - $("#editPresetModal .modal .presetType").addClass("hidden"); - $("#editPresetModal .modal .presetNameTitle").addClass("hidden"); - presetNameEl?.getParent()?.addClass("hidden"); + qsr("#editPresetModal .modal .inputs").hide(); + qsr("#editPresetModal .modal .presetType").hide(); + qsr("#editPresetModal .modal .presetNameTitle").hide(); + presetNameEl?.getParent()?.hide(); } updateUI(); }, @@ -138,24 +138,20 @@ async function initializeEditState(id: string): Promise { function addCheckboxListeners(): void { ConfigGroupNameSchema.options.forEach((settingGroup: ConfigGroupName) => { - const checkboxInput = $( + const checkboxInput = qsr( `#editPresetModal .modal .checkboxList .checkboxTitlePair[data-id="${settingGroup}"] input`, ); - checkboxInput.on("change", (e) => { - state.checkboxes.set( - settingGroup, - checkboxInput.prop("checked") as boolean, - ); + + checkboxInput.on("change", async () => { + state.checkboxes.set(settingGroup, checkboxInput.isChecked() as boolean); }); }); - const presetToCurrentCheckbox = $( + const presetToCurrentCheckbox = qsr( `#editPresetModal .modal .changePresetToCurrentCheckbox input`, ); presetToCurrentCheckbox.on("change", async () => { - state.setPresetToCurrent = presetToCurrentCheckbox.prop( - "checked", - ) as boolean; + state.setPresetToCurrent = presetToCurrentCheckbox.isChecked() as boolean; await updateEditPresetUI(); }); } @@ -164,16 +160,17 @@ function addCheckBoxes(): void { function camelCaseToSpaced(input: string): string { return input.replace(/([a-z])([A-Z])/g, "$1 $2"); } - const settingGroupListEl = $( + const settingGroupListEl = qsr( "#editPresetModal .modal .inputs .checkboxList", ).empty(); + ConfigGroupNameSchema.options.forEach((currSettingGroup) => { const currSettingGroupTitle = camelCaseToSpaced(currSettingGroup); const settingGroupCheckbox: string = ``; - settingGroupListEl.append(settingGroupCheckbox); + settingGroupListEl.appendHtml(settingGroupCheckbox); }); for (const key of state.checkboxes.keys()) { state.checkboxes.set(key, true); @@ -183,34 +180,43 @@ function addCheckBoxes(): void { function updateUI(): void { ConfigGroupNameSchema.options.forEach((settingGroup: ConfigGroupName) => { - $( - `#editPresetModal .modal .checkboxList .checkboxTitlePair[data-id="${settingGroup}"] input`, - ).prop("checked", state.checkboxes.get(settingGroup)); + if (state.checkboxes.get(settingGroup)) { + qsr( + `#editPresetModal .modal .checkboxList .checkboxTitlePair[data-id="${settingGroup}"] input`, + ).setAttribute("checked", "true"); + } else { + qsr( + `#editPresetModal .modal .checkboxList .checkboxTitlePair[data-id="${settingGroup}"] input`, + ).removeAttribute("checked"); + } }); - $(`#editPresetModal .modal .presetType button`).removeClass("active"); - $( + + qsa(`#editPresetModal .modal .presetType button`).removeClass("active"); + qsr( `#editPresetModal .modal .presetType button[value="${state.presetType}"]`, ).addClass("active"); - $(`#editPresetModal .modal .partialPresetGroups`).removeClass("hidden"); + qsr(`#editPresetModal .modal .partialPresetGroups`).show(); if (state.presetType === "full") { - $(`#editPresetModal .modal .partialPresetGroups`).addClass("hidden"); + qsr(`#editPresetModal .modal .partialPresetGroups`).hide(); } } async function updateEditPresetUI(): Promise { - $("#editPresetModal .modal label.changePresetToCurrentCheckbox input").prop( - "checked", - state.setPresetToCurrent, - ); if (state.setPresetToCurrent) { - const presetId = $("#editPresetModal .modal").attr( + qsr( + "#editPresetModal .modal label.changePresetToCurrentCheckbox input", + ).setAttribute("checked", "true"); + const presetId = qsr("#editPresetModal .modal").getAttribute( "data-preset-id", ) as string; await initializeEditState(presetId); - $("#editPresetModal .modal .inputs").removeClass("hidden"); - $("#editPresetModal .modal .presetType").removeClass("hidden"); + qsr("#editPresetModal .modal .inputs").show(); + qsr("#editPresetModal .modal .presetType").show(); } else { - $("#editPresetModal .modal .inputs").addClass("hidden"); - $("#editPresetModal .modal .presetType").addClass("hidden"); + qsr( + "#editPresetModal .modal label.changePresetToCurrentCheckbox input", + ).removeAttribute("checked"); + qsr("#editPresetModal .modal .inputs").hide(); + qsr("#editPresetModal .modal .presetType").hide(); } } @@ -219,23 +225,27 @@ function hide(): void { } async function apply(): Promise { - const action = $("#editPresetModal .modal").attr("data-action"); - const propPresetName = $("#editPresetModal .modal input").val() as string; + const action = qsr("#editPresetModal .modal").getAttribute("data-action"); + const propPresetName = qsa( + "#editPresetModal .modal input", + )[0]?.getValue() as string; const presetName = propPresetName.replaceAll(" ", "_"); - const presetId = $("#editPresetModal .modal").attr( + const presetId = qsr("#editPresetModal .modal").getAttribute( "data-preset-id", ) as string; - const updateConfig = $("#editPresetModal .modal label input").prop( - "checked", - ) as boolean; + const updateConfig = qsa( + "#editPresetModal .modal label input", + )[0]?.isChecked(); const snapshotPresets = DB.getSnapshot()?.presets ?? []; - if (action === undefined) { + if (action === null) { return; } + console.log({ updateConfig, propPresetName }); + const noPartialGroupSelected: boolean = ["add", "edit"].includes(action) && state.presetType === "partial" && From 57f71f90ff69ddea169201db79aead20cbf716d2 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 21:07:48 +0800 Subject: [PATCH 07/28] refactor: remove jquery in modals/edit-profile --- frontend/src/ts/modals/edit-profile.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/ts/modals/edit-profile.ts b/frontend/src/ts/modals/edit-profile.ts index 3eed5708533f..c27409c88fe6 100644 --- a/frontend/src/ts/modals/edit-profile.ts +++ b/frontend/src/ts/modals/edit-profile.ts @@ -101,13 +101,17 @@ function hydrateInputs(): void { `, ); - $(".badgeSelectionItem").on("click", ({ currentTarget }) => { - const selectionId = $(currentTarget).attr("selection-id") as string; - currentSelectedBadgeId = parseInt(selectionId, 10); - - badgeIdsSelect?.qsa(".badgeSelectionItem")?.removeClass("selected"); - $(currentTarget).addClass("selected"); - }); + badgeIdsSelect + ?.qsa(".badgeSelectionItem") + ?.on("click", ({ currentTarget }) => { + const selectionId = (currentTarget as HTMLElement).getAttribute( + "selection-id", + ) as string; + currentSelectedBadgeId = parseInt(selectionId, 10); + + badgeIdsSelect?.qsa(".badgeSelectionItem")?.removeClass("selected"); + (currentTarget as HTMLElement).classList.add("selected"); + }); indicators.forEach((it) => it.hide()); } From b4fa61d01a638d0b53939f43009ebeb9a92d6734 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 21:25:14 +0800 Subject: [PATCH 08/28] refactor: remove jquery in modals/edit-result-tags --- frontend/src/ts/modals/edit-result-tags.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/ts/modals/edit-result-tags.ts b/frontend/src/ts/modals/edit-result-tags.ts index f3a65df6119e..492d00dcedc0 100644 --- a/frontend/src/ts/modals/edit-result-tags.ts +++ b/frontend/src/ts/modals/edit-result-tags.ts @@ -7,6 +7,7 @@ import * as ConnectionState from "../states/connection"; import { areUnsortedArraysEqual } from "../utils/arrays"; import * as TestResult from "../test/result"; import AnimatedModal from "../utils/animated-modal"; +import { qsa } from "../utils/dom"; type State = { resultId: string; @@ -90,12 +91,12 @@ function appendButtons(): void { } function updateActiveButtons(): void { - for (const button of $("#editResultTagsModal .modal .buttons button")) { - const tagid: string = $(button).attr("data-tag-id") ?? ""; + for (const button of qsa("#editResultTagsModal .modal .buttons button")) { + const tagid: string = button.getAttribute("data-tag-id") ?? ""; if (state.tags.includes(tagid)) { - $(button).addClass("active"); + button.addClass("active"); } else { - $(button).removeClass("active"); + button.removeClass("active"); } } } From f4629c7d076622fa6116317a841afbe2e394c062 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 22:41:38 +0800 Subject: [PATCH 09/28] refactor: remove jquery in modals/google-sign-up --- frontend/src/ts/modals/google-sign-up.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/ts/modals/google-sign-up.ts b/frontend/src/ts/modals/google-sign-up.ts index d436fbc01846..67f435ecd2c5 100644 --- a/frontend/src/ts/modals/google-sign-up.ts +++ b/frontend/src/ts/modals/google-sign-up.ts @@ -37,7 +37,7 @@ function show(credential: UserCredential): void { } CaptchaController.reset("googleSignUpModal"); CaptchaController.render( - $("#googleSignUpModal .captcha")[0] as HTMLElement, + qsr("#googleSignUpModal .captcha").native, "googleSignUpModal", ); enableInput(); @@ -93,7 +93,9 @@ async function apply(): Promise { disableButton(); Loader.show(); - const name = $("#googleSignUpModal input").val() as string; + const name = qsr( + "#googleSignUpModal input", + ).getValue() as string; try { if (name.length === 0) throw new Error("Name cannot be empty"); const response = await Ape.users.create({ body: { name, captcha } }); @@ -135,11 +137,11 @@ async function apply(): Promise { } function enableButton(): void { - $("#googleSignUpModal button").prop("disabled", false); + qsr("#googleSignUpModal button").enable(); } function disableButton(): void { - $("#googleSignUpModal button").prop("disabled", true); + qsr("#googleSignUpModal button").disable(); } const nameInputEl = qsr("#googleSignUpModal input"); From dfd13a4e7b828aebf00b425a1f3a6b5b4f9fa5cd Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 23:03:07 +0800 Subject: [PATCH 10/28] refactor: remove jquery in modals/last-signout-result --- frontend/src/ts/modals/last-signed-out-result.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/ts/modals/last-signed-out-result.ts b/frontend/src/ts/modals/last-signed-out-result.ts index 5205f803b7d9..f1d7c691a9e1 100644 --- a/frontend/src/ts/modals/last-signed-out-result.ts +++ b/frontend/src/ts/modals/last-signed-out-result.ts @@ -85,10 +85,13 @@ function fillGroup( text: string | number, html = false, ): void { + const el = modal.getModal().querySelector(`.group.${groupClass} .val`); + if (!el) return; + if (html) { - $(modal.getModal()).find(`.group.${groupClass} .val`).html(`${text}`); + el.innerHTML = `${text}`; } else { - $(modal.getModal()).find(`.group.${groupClass} .val`).text(text); + el.textContent = `${text}`; } } From 2e0ee984d13d42ec04a8a0e488ae6d2c7c4a8dba Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 23:03:27 +0800 Subject: [PATCH 11/28] refactor: remove jquery in modals/mobile-test-config --- frontend/src/ts/modals/mobile-test-config.ts | 53 ++++++++++---------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/frontend/src/ts/modals/mobile-test-config.ts b/frontend/src/ts/modals/mobile-test-config.ts index 8cd475107f6a..2f932d80355c 100644 --- a/frontend/src/ts/modals/mobile-test-config.ts +++ b/frontend/src/ts/modals/mobile-test-config.ts @@ -10,58 +10,59 @@ import { QuoteLength, QuoteLengthConfig } from "@monkeytype/schemas/configs"; import { Mode } from "@monkeytype/schemas/shared"; import { areUnsortedArraysEqual } from "../utils/arrays"; import * as ShareTestSettingsPopup from "./share-test-settings"; +import { qsr } from "../utils/dom"; function update(): void { - const el = $("#mobileTestConfigModal"); - el.find("button").removeClass("active"); + const el = qsr("#mobileTestConfigModal"); + el.qs("button")?.removeClass("active"); - el.find(`.modeGroup button[data-mode='${Config.mode}']`).addClass("active"); - el.find(".timeGroup").addClass("hidden"); - el.find(".wordsGroup").addClass("hidden"); - el.find(".quoteGroup").addClass("hidden"); - el.find(".customGroup").addClass("hidden"); - el.find(`.${Config.mode}Group`).removeClass("hidden"); + el.qs(`.modeGroup button[data-mode='${Config.mode}']`)?.addClass("active"); + el.qs(".timeGroup")?.hide(); + el.qs(".wordsGroup")?.hide(); + el.qs(".quoteGroup")?.hide(); + el.qs(".customGroup")?.hide(); + el.qs(`.${Config.mode}Group`)?.show(); if (Config.punctuation) { - el.find(".punctuation").addClass("active"); + el.qs(".punctuation")?.addClass("active"); } else { - el.find(".punctuation").removeClass("active"); + el.qs(".punctuation")?.removeClass("active"); } if (Config.numbers) { - el.find(".numbers").addClass("active"); + el.qs(".numbers")?.addClass("active"); } else { - el.find(".numbers").removeClass("active"); + el.qs(".numbers")?.removeClass("active"); } if (Config.mode === "time") { - el.find(`.timeGroup button[data-time='${Config.time}']`).addClass("active"); - el.find(".punctuation").removeClass("disabled"); - el.find(".numbers").removeClass("disabled"); + el.qs(`.timeGroup button[data-time='${Config.time}']`)?.addClass("active"); + el.qs(".punctuation")?.enable(); + el.qs(".numbers")?.enable(); } else if (Config.mode === "words") { - el.find(`.wordsGroup button[data-words='${Config.words}']`).addClass( + el.qs(`.wordsGroup button[data-words='${Config.words}']`)?.addClass( "active", ); - el.find(".punctuation").removeClass("disabled"); - el.find(".numbers").removeClass("disabled"); + el.qs(".punctuation")?.enable(); + el.qs(".numbers")?.enable(); } else if (Config.mode === "quote") { if (areUnsortedArraysEqual(Config.quoteLength, [0, 1, 2, 3])) { - el.find(`.quoteGroup button[data-quoteLength='all']`).addClass("active"); + el.qs(`.quoteGroup button[data-quoteLength='all']`)?.addClass("active"); } else { for (const ql of Config.quoteLength) { - el.find(`.quoteGroup button[data-quoteLength='${ql}']`).addClass( + el.qs(`.quoteGroup button[data-quoteLength='${ql}']`)?.addClass( "active", ); } } - el.find(".punctuation").addClass("disabled"); - el.find(".numbers").addClass("disabled"); + el.qs(".punctuation")?.disable(); + el.qs(".numbers")?.disable(); } else if (Config.mode === "zen") { - el.find(".punctuation").addClass("disabled"); - el.find(".numbers").addClass("disabled"); + el.qs(".punctuation")?.disable(); + el.qs(".numbers")?.disable(); } else if (Config.mode === "custom") { - el.find(".punctuation").removeClass("disabled"); - el.find(".numbers").removeClass("disabled"); + el.qs(".punctuation")?.enable(); + el.qs(".numbers")?.enable(); } } From 3f89b9a0d20435287fb0a9d70a571199e850bc59 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Thu, 25 Dec 2025 23:03:37 +0800 Subject: [PATCH 12/28] refactor: remove jquery in modals/practice-words --- frontend/src/ts/modals/practise-words.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/ts/modals/practise-words.ts b/frontend/src/ts/modals/practise-words.ts index 134a7126a189..42e32dba4aed 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 { qs } from "../utils/dom"; type State = { missed: "off" | "words" | "biwords"; @@ -15,20 +16,20 @@ const state: State = { const practiseModal = "#practiseWordsModal .modal"; function updateUI(): void { - $(`${practiseModal} .group[data-id="missed"] button`).removeClass("active"); - $( + qs(`${practiseModal} .group[data-id="missed"] button`)?.removeClass("active"); + qs( `${practiseModal} .group[data-id="missed"] button[value="${state.missed}"]`, - ).addClass("active"); + )?.addClass("active"); - $(`${practiseModal} .group[data-id="slow"] button`).removeClass("active"); - $( + qs(`${practiseModal} .group[data-id="slow"] button`)?.removeClass("active"); + qs( `${practiseModal} .group[data-id="slow"] button[value="${state.slow}"]`, - ).addClass("active"); + )?.addClass("active"); if (state.missed === "off" && !state.slow) { - $(`${practiseModal} .start`).prop("disabled", true); + qs(`${practiseModal} .start`)?.disable(); } else { - $(`${practiseModal} .start`).prop("disabled", false); + qs(`${practiseModal} .start`)?.enable(); } } From e0078f3578b3705145a95406fb740275fadfb8e0 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 18:44:43 +0800 Subject: [PATCH 13/28] refactor: remove jquery in modals/quote-approve --- frontend/src/ts/modals/quote-approve.ts | 127 +++++++++++------------- 1 file changed, 60 insertions(+), 67 deletions(-) diff --git a/frontend/src/ts/modals/quote-approve.ts b/frontend/src/ts/modals/quote-approve.ts index 3fd7a79febac..86bb39b1bb22 100644 --- a/frontend/src/ts/modals/quote-approve.ts +++ b/frontend/src/ts/modals/quote-approve.ts @@ -5,13 +5,15 @@ import { format } from "date-fns/format"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import { Quote } from "@monkeytype/schemas/quotes"; import { escapeHTML } from "../utils/misc"; +import { qsr, createElementWithUtils } from "../utils/dom"; let quotes: Quote[] = []; function updateList(): void { - $("#quoteApproveModal .quotes").empty(); + qsr("#quoteApproveModal .quotes").empty(); quotes.forEach((quote, index) => { - const quoteEl = $(` + const quoteEl = createElementWithUtils("div"); + quoteEl.setHtml(`
{ - $(`#quoteApproveModal .quote[data-id=${index}] .undo`).prop( - "disabled", - false, - ); - $(`#quoteApproveModal .quote[data-id=${index}] .approve`).addClass( - "hidden", - ); - $(`#quoteApproveModal .quote[data-id=${index}] .edit`).removeClass( - "hidden", - ); + + qsr("#quoteApproveModal .quotes").append(quoteEl); + quoteEl.qsr(".source").on("input", () => { + qsr(`#quoteApproveModal .quote[data-id="${index}"] .undo`).enable(); + qsr(`#quoteApproveModal .quote[data-id="${index}"] .approve`).hide(); + qsr(`#quoteApproveModal .quote[data-id="${index}"] .edit`).show(); }); - quoteEl.find(".text").on("input", () => { - $(`#quoteApproveModal .quote[data-id=${index}] .undo`).prop( - "disabled", - false, - ); - $(`#quoteApproveModal .quote[data-id=${index}] .approve`).addClass( - "hidden", - ); - $(`#quoteApproveModal .quote[data-id=${index}] .edit`).removeClass( - "hidden", - ); + quoteEl.qsr(".text").on("input", () => { + qsr(`#quoteApproveModal .quote[data-id="${index}"] .undo`).enable(); + qsr(`#quoteApproveModal .quote[data-id="${index}"] .approve`).hide(); + qsr(`#quoteApproveModal .quote[data-id="${index}"] .edit`).show(); updateQuoteLength(index); }); - quoteEl.find(".undo").on("click", () => { + quoteEl.qsr(".undo").on("click", () => { undoQuote(index); }); - quoteEl.find(".approve").on("click", () => { + quoteEl.qsr(".approve").on("click", () => { void approveQuote(index, quote._id); }); - quoteEl.find(".refuse").on("click", () => { + quoteEl.qsr(".refuse").on("click", () => { void refuseQuote(index, quote._id); }); - quoteEl.find(".edit").on("click", () => { + quoteEl.qsr(".edit").on("click", () => { void editQuote(index, quote._id); }); }); @@ -80,15 +69,21 @@ function updateList(): void { function updateQuoteLength(index: number): void { const len = ( - $(`#quoteApproveModal .quote[data-id=${index}] .text`).val() as string + qsr( + `#quoteApproveModal .quote[data-id="${index}"] .text`, + ).getValue() as string )?.length; - $(`#quoteApproveModal .quote[data-id=${index}] .length`).html( + qsr(`#quoteApproveModal .quote[data-id="${index}"] .length`).setHtml( `${len}`, ); if (len < 60) { - $(`#quoteApproveModal .quote[data-id=${index}] .length`).addClass("red"); + qsr(`#quoteApproveModal .quote[data-id="${index}"] .length`).addClass( + "red", + ); } else { - $(`#quoteApproveModal .quote[data-id=${index}] .length`).removeClass("red"); + qsr(`#quoteApproveModal .quote[data-id="${index}"] .length`).removeClass( + "red", + ); } } @@ -123,33 +118,31 @@ export async function show(showOptions?: ShowOptions): Promise { // } function resetButtons(index: number): void { - const quote = $(`#quoteApproveModal .quotes .quote[data-id=${index}]`); - quote.find("button").prop("disabled", false); - if (quote.find(".edit").hasClass("hidden")) { - quote.find(".undo").prop("disabled", true); + const quote = qsr(`#quoteApproveModal .quotes .quote[data-id="${index}"]`); + quote.qsa("button").enable(); + if (quote.qsr(".edit").hasClass("hidden")) { + quote.qsr(".undo").disable(); } } function undoQuote(index: number): void { - $(`#quoteApproveModal .quote[data-id=${index}] .text`).val( - quotes[index]?.text ?? "", - ); - $(`#quoteApproveModal .quote[data-id=${index}] .source`).val( - quotes[index]?.source ?? "", - ); - $(`#quoteApproveModal .quote[data-id=${index}] .undo`).prop("disabled", true); - $(`#quoteApproveModal .quote[data-id=${index}] .approve`).removeClass( - "hidden", - ); - $(`#quoteApproveModal .quote[data-id=${index}] .edit`).addClass("hidden"); + qsr( + `#quoteApproveModal .quote[data-id="${index}"] .text`, + ).setValue(quotes[index]?.text ?? ""); + qsr( + `#quoteApproveModal .quote[data-id="${index}"] .source`, + ).setValue(quotes[index]?.source ?? ""); + qsr(`#quoteApproveModal .quote[data-id="${index}"] .undo`).disable(); + qsr(`#quoteApproveModal .quote[data-id="${index}"] .approve`).show(); + qsr(`#quoteApproveModal .quote[data-id="${index}"] .edit`).hide(); updateQuoteLength(index); } async function approveQuote(index: number, dbid: string): Promise { if (!confirm("Are you sure?")) return; - const quote = $(`#quoteApproveModal .quotes .quote[data-id=${index}]`); - quote.find("button").prop("disabled", true); - quote.find("textarea, input").prop("disabled", true); + const quote = qsr(`#quoteApproveModal .quotes .quote[data-id="${index}"]`); + quote.qsa("button").disable(); + quote.qsa("textarea, input").disable(); Loader.show(); const response = await Ape.quotes.approveSubmission({ @@ -159,7 +152,7 @@ async function approveQuote(index: number, dbid: string): Promise { if (response.status !== 200) { resetButtons(index); - quote.find("textarea, input").prop("disabled", false); + quote.qsa("textarea, input").enable(); Notifications.add("Failed to approve quote", -1, { response }); return; } @@ -171,9 +164,9 @@ async function approveQuote(index: number, dbid: string): Promise { async function refuseQuote(index: number, dbid: string): Promise { if (!confirm("Are you sure?")) return; - const quote = $(`#quoteApproveModal .quotes .quote[data-id=${index}]`); - quote.find("button").prop("disabled", true); - quote.find("textarea, input").prop("disabled", true); + const quote = qsr(`#quoteApproveModal .quotes .quote[data-id="${index}"]`); + quote.qsa("button").disable(); + quote.qsa("textarea, input").disable(); Loader.show(); const response = await Ape.quotes.rejectSubmission({ @@ -183,7 +176,7 @@ async function refuseQuote(index: number, dbid: string): Promise { if (response.status !== 200) { resetButtons(index); - quote.find("textarea, input").prop("disabled", false); + quote.qsa("textarea, input").enable(); Notifications.add("Failed to refuse quote", -1, { response }); return; } @@ -195,15 +188,15 @@ async function refuseQuote(index: number, dbid: string): Promise { async function editQuote(index: number, dbid: string): Promise { if (!confirm("Are you sure?")) return; - const editText = $( - `#quoteApproveModal .quote[data-id=${index}] .text`, - ).val() as string; - const editSource = $( - `#quoteApproveModal .quote[data-id=${index}] .source`, - ).val() as string; - const quote = $(`#quoteApproveModal .quotes .quote[data-id=${index}]`); - quote.find("button").prop("disabled", true); - quote.find("textarea, input").prop("disabled", true); + const editText = qsr( + `#quoteApproveModal .quote[data-id="${index}"] .text`, + ).getValue() as string; + const editSource = qsr( + `#quoteApproveModal .quote[data-id="${index}"] .source`, + ).getValue() as string; + const quote = qsr(`#quoteApproveModal .quotes .quote[data-id="${index}"]`); + quote.qsa("button").disable(); + quote.qsa("textarea, input").disable(); Loader.show(); const response = await Ape.quotes.approveSubmission({ @@ -217,7 +210,7 @@ async function editQuote(index: number, dbid: string): Promise { if (response.status !== 200) { resetButtons(index); - quote.find("textarea, input").prop("disabled", false); + quote.qsa("textarea, input").enable(); Notifications.add("Failed to approve quote", -1, { response }); return; } @@ -232,7 +225,7 @@ async function editQuote(index: number, dbid: string): Promise { async function setup(modalEl: HTMLElement): Promise { modalEl.querySelector("button.refreshList")?.addEventListener("click", () => { - $("#quoteApproveModal .quotes").empty(); + qsr("#quoteApproveModal .quotes").empty(); void getQuotes(); }); } From f850bd5fb8be849a7739e6676e468807a7e2fbd3 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 19:05:41 +0800 Subject: [PATCH 14/28] refactor: remove jquery in modals/quote-rate --- frontend/src/ts/modals/quote-rate.ts | 38 +++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/frontend/src/ts/modals/quote-rate.ts b/frontend/src/ts/modals/quote-rate.ts index e22b97f91e1c..e7ecd4cd077c 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 { qs, qsa, qsr } from "../utils/dom"; let rating = 0; @@ -25,12 +26,12 @@ export function clearQuoteStats(): void { } function reset(): void { - $(`#quoteRateModal .quote .text`).text("-"); - $(`#quoteRateModal .quote .source .val`).text("-"); - $(`#quoteRateModal .quote .id .val`).text("-"); - $(`#quoteRateModal .quote .length .val`).text("-"); - $("#quoteRateModal .ratingCount .val").text("-"); - $("#quoteRateModal .ratingAverage .val").text("-"); + qsr(`#quoteRateModal .quote .text`).setText("-"); + qsr(`#quoteRateModal .quote .source .val`).setText("-"); + qsr(`#quoteRateModal .quote .id .val`).setText("-"); + qsr(`#quoteRateModal .quote .length .val`).setText("-"); + qsr("#quoteRateModal .ratingCount .val").setText("-"); + qsr("#quoteRateModal .ratingAverage .val").setText("-"); } function getRatingAverage(quoteStats: QuoteStats): number { @@ -78,16 +79,19 @@ export async function getQuoteStats( function refreshStars(force?: number): void { const limit = force ?? rating; - $(`#quoteRateModal .star`).removeClass("active"); + qsa(`#quoteRateModal .star`).removeClass("active"); for (let i = 1; i <= limit; i++) { - $(`#quoteRateModal .star[data-rating=${i}]`).addClass("active"); + qsr(`#quoteRateModal .star[data-rating="${i}"]`).addClass("active"); } } async function updateRatingStats(): Promise { if (!quoteStats) await getQuoteStats(); - $("#quoteRateModal .ratingCount .val").text(quoteStats?.ratings ?? "0"); - $("#quoteRateModal .ratingAverage .val").text( + const ratings = quoteStats?.ratings; + qsr("#quoteRateModal .ratingCount .val").setText( + ratings === undefined ? "0" : ratings.toString(), + ); + qsr("#quoteRateModal .ratingAverage .val").setText( quoteStats?.average?.toFixed(1) ?? "-", ); } @@ -104,10 +108,10 @@ function updateData(): void { } else if (currentQuote.group === 3) { lengthDesc = "thicc"; } - $(`#quoteRateModal .quote .text`).text(currentQuote.text); - $(`#quoteRateModal .quote .source .val`).text(currentQuote.source); - $(`#quoteRateModal .quote .id .val`).text(currentQuote.id); - $(`#quoteRateModal .quote .length .val`).text(lengthDesc as string); + qsr(`#quoteRateModal .quote .text`).setText(currentQuote.text); + qsr(`#quoteRateModal .quote .source .val`).setText(currentQuote.source); + qsr(`#quoteRateModal .quote .id .val`).setText(`${currentQuote.id}`); + qsr(`#quoteRateModal .quote .length .val`).setText(lengthDesc as string); void updateRatingStats(); } @@ -201,11 +205,11 @@ async function submit(): Promise { DB.setSnapshot(snapshot); quoteStats.average = getRatingAverage(quoteStats); - $(".pageTest #result #rateQuoteButton .rating").text( + qs(".pageTest #result #rateQuoteButton .rating")?.setText( quoteStats.average?.toFixed(1), ); - $(".pageTest #result #rateQuoteButton .icon").removeClass("far"); - $(".pageTest #result #rateQuoteButton .icon").addClass("fas"); + qs(".pageTest #result #rateQuoteButton .icon")?.removeClass("far"); + qs(".pageTest #result #rateQuoteButton .icon")?.addClass("fas"); } async function setup(modalEl: HTMLElement): Promise { From e3cfa82a7ea2a98a8cb00ad0e5b1276a6412b03e Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 20:47:50 +0800 Subject: [PATCH 15/28] feat(dom): add force argument to toggleClass --- frontend/src/ts/utils/dom.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/ts/utils/dom.ts b/frontend/src/ts/utils/dom.ts index de41eb6322d5..5da12400da55 100644 --- a/frontend/src/ts/utils/dom.ts +++ b/frontend/src/ts/utils/dom.ts @@ -255,8 +255,8 @@ export class ElementWithUtils { /** * Toggle a class on the element */ - toggleClass(className: string): this { - this.native.classList.toggle(className); + toggleClass(className: string, force?: boolean): this { + this.native.classList.toggle(className, force); return this; } From 94871600e57dcc291781a5580deb2e0013a0a59a Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 20:48:02 +0800 Subject: [PATCH 16/28] refactor: remove jquery in modals/quote-report --- frontend/src/ts/modals/quote-report.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/src/ts/modals/quote-report.ts b/frontend/src/ts/modals/quote-report.ts index f24a423abb76..4ac20e5842ea 100644 --- a/frontend/src/ts/modals/quote-report.ts +++ b/frontend/src/ts/modals/quote-report.ts @@ -50,9 +50,13 @@ export async function show( return quote.id === quoteId; }); - $("#quoteReportModal .quote").text(state.quoteToReport?.text as string); - $("#quoteReportModal .reason").val("Grammatical error"); - $("#quoteReportModal .comment").val(""); + qsr("#quoteReportModal .quote").setText( + state.quoteToReport?.text as string, + ); + qsr("#quoteReportModal .reason").setValue( + "Grammatical error", + ); + qsr("#quoteReportModal .comment").setValue(""); state.reasonSelect = new SlimSelect({ select: "#quoteReportModal .reason", @@ -81,8 +85,12 @@ async function submitReport(): Promise { const quoteId = state.quoteToReport?.id.toString(); const quoteLanguage = removeLanguageSize(Config.language); - const reason = $("#quoteReportModal .reason").val() as QuoteReportReason; - const comment = $("#quoteReportModal .comment").val() as string; + const reason = qsr( + "#quoteReportModal .reason", + ).getValue() as QuoteReportReason; + const comment = qsr( + "#quoteReportModal .comment", + ).getValue() as string; const captcha = captchaResponse; if (quoteId === undefined || quoteId === "") { From bbe3c24d6f0ed1f627ec5b4bd5b38bf163575a50 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 20:48:12 +0800 Subject: [PATCH 17/28] refactor: remove jquery in modals/quote-search --- frontend/src/ts/modals/quote-search.ts | 44 +++++++++++++++----------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/frontend/src/ts/modals/quote-search.ts b/frontend/src/ts/modals/quote-search.ts index ddfacc1bbc3d..dfbc23c0f567 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 { qs, qsr } from "../utils/dom"; const searchServiceCache: Record> = {}; @@ -52,8 +53,13 @@ function getSearchService( function applyQuoteLengthFilter(quotes: Quote[]): Quote[] { if (!modal.isOpen()) return []; - const quoteLengthDropdown = $("#quoteSearchModal .quoteLengthFilter"); - const quoteLengthFilterValue = quoteLengthDropdown.val() as string[]; + const quoteLengthDropdown = qs( + "#quoteSearchModal .quoteLengthFilter", + ); + const selectedOptions = quoteLengthDropdown + ? Array.from(quoteLengthDropdown.native.selectedOptions) + : []; + const quoteLengthFilterValue = selectedOptions.map((el) => el.value); if (quoteLengthFilterValue.length === 0) { usingCustomLength = true; @@ -69,7 +75,7 @@ function applyQuoteLengthFilter(quotes: Quote[]): Quote[] { if (customFilterIndex !== -1) { if (QuoteFilterPopup.removeCustom) { QuoteFilterPopup.setRemoveCustom(false); - const selectElement = quoteLengthDropdown.get(0) as + const selectElement = quoteLengthDropdown?.native as | HTMLSelectElement | null | undefined; @@ -276,38 +282,38 @@ async function updateResults(searchText: string): Promise { applyQuoteFavFilter(searchText === "" ? quotes : matches), ); - const resultsList = $("#quoteSearchResults"); + const resultsList = qsr("#quoteSearchResults"); resultsList.empty(); const totalPages = Math.ceil(quotesToShow.length / pageSize); if (currentPageNumber >= totalPages) { - $("#quoteSearchPageNavigator .nextPage").prop("disabled", true); + qsr("#quoteSearchPageNavigator .nextPage").disable(); } else { - $("#quoteSearchPageNavigator .nextPage").prop("disabled", false); + qsr("#quoteSearchPageNavigator .nextPage").enable(); } if (currentPageNumber <= 1) { - $("#quoteSearchPageNavigator .prevPage").prop("disabled", true); + qsr("#quoteSearchPageNavigator .prevPage").disable(); } else { - $("#quoteSearchPageNavigator .prevPage").prop("disabled", false); + qsr("#quoteSearchPageNavigator .prevPage").enable(); } if (quotesToShow.length === 0) { - $("#quoteSearchModal .pageInfo").html("No search results"); + qsr("#quoteSearchModal .pageInfo").setHtml("No search results"); return; } const startIndex = (currentPageNumber - 1) * pageSize; const endIndex = Math.min(currentPageNumber * pageSize, quotesToShow.length); - $("#quoteSearchModal .pageInfo").html( + qsr("#quoteSearchModal .pageInfo").setHtml( `${startIndex + 1} - ${endIndex} of ${quotesToShow.length}`, ); quotesToShow.slice(startIndex, endIndex).forEach((quote) => { const quoteSearchResult = buildQuoteSearchResult(quote, matchedQueryTerms); - resultsList.append(quoteSearchResult); + resultsList.appendHtml(quoteSearchResult); }); const searchResults = modal @@ -358,11 +364,11 @@ export async function show(showOptions?: ShowOptions): Promise { focusFirstInput: true, beforeAnimation: async () => { if (!isAuthenticated()) { - $("#quoteSearchModal .goToQuoteSubmit").addClass("hidden"); - $("#quoteSearchModal .toggleFavorites").addClass("hidden"); + qsr("#quoteSearchModal .goToQuoteSubmit").hide(); + qsr("#quoteSearchModal .toggleFavorites").hide(); } else { - $("#quoteSearchModal .goToQuoteSubmit").removeClass("hidden"); - $("#quoteSearchModal .toggleFavorites").removeClass("hidden"); + qsr("#quoteSearchModal .goToQuoteSubmit").show(); + qsr("#quoteSearchModal .toggleFavorites").show(); } const quoteMod = DB.getSnapshot()?.quoteMod; @@ -371,9 +377,9 @@ export async function show(showOptions?: ShowOptions): Promise { (quoteMod === true || (quoteMod as string) !== ""); if (isQuoteMod) { - $("#quoteSearchModal .goToQuoteApprove").removeClass("hidden"); + qsr("#quoteSearchModal .goToQuoteApprove").show(); } else { - $("#quoteSearchModal .goToQuoteApprove").addClass("hidden"); + qsr("#quoteSearchModal .goToQuoteApprove").hide(); } lengthSelect = new SlimSelect({ @@ -461,7 +467,7 @@ async function toggleFavoriteForQuote(quoteId: string): Promise { const alreadyFavorited = QuotesController.isQuoteFavorite(quote); - const $button = $( + const $button = qsr( `#quoteSearchModal .searchResult[data-quote-id=${quoteId}] .textButton.favorite i`, ); const dbSnapshot = DB.getSnapshot(); @@ -507,7 +513,7 @@ async function setup(modalEl: HTMLElement): Promise { return; } - $(e.target as HTMLElement).toggleClass("active"); + (e.target as HTMLElement).classList.toggle("active"); searchForQuotes(); }); modalEl.querySelector(".goToQuoteApprove")?.addEventListener("click", (e) => { From 1c51556c676d63de4d3949be849ca6e4a7b84ad5 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 20:48:23 +0800 Subject: [PATCH 18/28] refactor: remove jquery in modals/quote-submit --- frontend/src/ts/modals/quote-submit.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/frontend/src/ts/modals/quote-submit.ts b/frontend/src/ts/modals/quote-submit.ts index 2a2f99fb337f..6de451498534 100644 --- a/frontend/src/ts/modals/quote-submit.ts +++ b/frontend/src/ts/modals/quote-submit.ts @@ -17,7 +17,7 @@ async function initDropdown(): Promise { for (const group of LanguageGroupNames) { if (group === "swiss_german") continue; - $("#quoteSubmitModal .newQuoteLanguage").append( + qsr("#quoteSubmitModal .newQuoteLanguage").appendHtml( ``, ); } @@ -27,9 +27,15 @@ async function initDropdown(): Promise { let select: SlimSelect | undefined = undefined; async function submitQuote(): Promise { - const text = $("#quoteSubmitModal .newQuoteText").val() as string; - const source = $("#quoteSubmitModal .newQuoteSource").val() as string; - const language = $("#quoteSubmitModal .newQuoteLanguage").val() as Language; + const text = qsr( + "#quoteSubmitModal .newQuoteText", + ).getValue() as string; + const source = qsr( + "#quoteSubmitModal .newQuoteSource", + ).getValue() as string; + const language = qsr( + "#quoteSubmitModal .newQuoteLanguage", + ).getValue() as Language; const captcha = CaptchaController.getResponse("submitQuote"); if (!text || !source || !language) { @@ -49,8 +55,8 @@ async function submitQuote(): Promise { } Notifications.add("Quote submitted.", 1); - $("#quoteSubmitModal .newQuoteText").val(""); - $("#quoteSubmitModal .newQuoteSource").val(""); + qsr("#quoteSubmitModal .newQuoteText").setValue(""); + qsr("#quoteSubmitModal .newQuoteSource").setValue(""); CaptchaController.reset("submitQuote"); } @@ -78,11 +84,13 @@ export async function show(showOptions: ShowOptions): Promise { select: "#quoteSubmitModal .newQuoteLanguage", }); - $("#quoteSubmitModal .newQuoteLanguage").val( + qsr("#quoteSubmitModal .newQuoteLanguage").setValue( Strings.removeLanguageSize(Config.language), ); - $("#quoteSubmitModal .newQuoteLanguage").trigger("change"); - $("#quoteSubmitModal input").val(""); + qsr("#quoteSubmitModal .newQuoteLanguage").dispatch( + "change", + ); + qsr("#quoteSubmitModal input").setValue(""); new CharacterCounter(qsr("#quoteSubmitModal .newQuoteText"), 250); }, From 4c9d998c2e295e2fbf42e91d8939a271f7272787 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 20:49:00 +0800 Subject: [PATCH 19/28] refactor: remove jquery in modals/save-custom-text --- frontend/src/ts/modals/save-custom-text.ts | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/frontend/src/ts/modals/save-custom-text.ts b/frontend/src/ts/modals/save-custom-text.ts index d5a13400da1d..83940087aad1 100644 --- a/frontend/src/ts/modals/save-custom-text.ts +++ b/frontend/src/ts/modals/save-custom-text.ts @@ -31,17 +31,17 @@ const validatedInput = new ValidatedHtmlInputElement( "Name can only contain letters, numbers, spaces, underscores and hyphens", }), isValid: async (value) => { - const checkbox = $("#saveCustomTextModal .isLongText").prop( - "checked", - ) as boolean; + const checkbox = qsr( + "#saveCustomTextModal .isLongText", + ).isChecked() as boolean; const names = CustomText.getCustomTextNames(checkbox); return !names.includes(value) ? true : "Duplicate name"; }, callback: (result) => { if (result.status === "success") { - $("#saveCustomTextModal button.save").prop("disabled", false); + qsr("#saveCustomTextModal button.save").enable(); } else { - $("#saveCustomTextModal button.save").prop("disabled", true); + qsr("#saveCustomTextModal button.save").disable(); } }, }, @@ -53,18 +53,20 @@ export async function show(options: ShowOptions): Promise { ...options, beforeAnimation: async (modalEl, modalChainData) => { state.textToSave = modalChainData?.text ?? []; - $("#saveCustomTextModal .textName").val(""); - $("#saveCustomTextModal .isLongText").prop("checked", false); - $("#saveCustomTextModal button.save").prop("disabled", true); + qsr("#saveCustomTextModal .textName").setValue(""); + qsr("#saveCustomTextModal .isLongText").removeAttribute("checked"); + qsr("#saveCustomTextModal button.save").disable(); }, }); } function save(): boolean { - const name = $("#saveCustomTextModal .textName").val() as string; - const checkbox = $("#saveCustomTextModal .isLongText").prop( - "checked", - ) as boolean; + const name = qsr( + "#saveCustomTextModal .textName", + ).getValue() as string; + const checkbox = qsr( + "#saveCustomTextModal .isLongText", + ).isChecked() as boolean; if (!name) { Notifications.add("Custom text needs a name", 0); From 06521f2b9850db6e3a131376e8445433455623d0 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 20:49:09 +0800 Subject: [PATCH 20/28] refactor: remove jquery in modals/saved-texts --- frontend/src/ts/modals/saved-texts.ts | 47 ++++++++++++++------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/frontend/src/ts/modals/saved-texts.ts b/frontend/src/ts/modals/saved-texts.ts index 24869352f7af..33f8136267bf 100644 --- a/frontend/src/ts/modals/saved-texts.ts +++ b/frontend/src/ts/modals/saved-texts.ts @@ -7,10 +7,11 @@ import AnimatedModal, { } from "../utils/animated-modal"; import { showPopup } from "./simple-modals"; import * as Notifications from "../elements/notifications"; +import { qs, qsr } from "../utils/dom"; async function fill(): Promise { const names = CustomText.getCustomTextNames(); - const listEl = $(`#savedTextsModal .list`).empty(); + const listEl = qsr(`#savedTextsModal .list`).empty(); let list = ""; if (names.length === 0) { list += "
No saved custom texts found
"; @@ -24,10 +25,10 @@ async function fill(): Promise {
`; } } - listEl.html(list); + listEl.setHtml(list); const longNames = CustomText.getCustomTextNames(true); - const longListEl = $(`#savedTextsModal .listLong`).empty(); + const longListEl = qsr(`#savedTextsModal .listLong`).empty(); let longList = ""; if (longNames.length === 0) { longList += "
No saved long custom texts found
"; @@ -44,13 +45,13 @@ async function fill(): Promise {
`; } } - longListEl.html(longList); + longListEl.setHtml(longList); - $("#savedTextsModal .list .savedText .button.delete").on("click", (e) => { - const name = $(e.target).closest(".savedText").data("name") as - | string - | undefined; - if (name === undefined) { + qs("#savedTextsModal .list .savedText .button.delete")?.on("click", (e) => { + const name = (e.target as HTMLElement) + .closest(".savedText") + ?.getAttribute("data-name"); + if (name === null || name === undefined) { Notifications.add("Failed to show delete modal: no name found", -1); return; } @@ -59,13 +60,13 @@ async function fill(): Promise { }); }); - $("#savedTextsModal .listLong .savedLongText .button.delete").on( + qs("#savedTextsModal .listLong .savedLongText .button.delete")?.on( "click", (e) => { - const name = $(e.target).closest(".savedLongText").data("name") as - | string - | undefined; - if (name === undefined) { + const name = (e.target as HTMLElement) + .closest(".savedLongText") + ?.getAttribute("data-name"); + if (name === null || name === undefined) { Notifications.add("Failed to show delete modal: no name found", -1); return; } @@ -75,13 +76,13 @@ async function fill(): Promise { }, ); - $("#savedTextsModal .listLong .savedLongText .button.resetProgress").on( + qs("#savedTextsModal .listLong .savedLongText .button.resetProgress")?.on( "click", (e) => { - const name = $(e.target).closest(".savedLongText").data("name") as - | string - | undefined; - if (name === undefined) { + const name = (e.target as HTMLElement) + .closest(".savedLongText") + ?.getAttribute("data-name"); + if (name === null || name === undefined) { Notifications.add("Failed to show delete modal: no name found", -1); return; } @@ -91,17 +92,17 @@ async function fill(): Promise { }, ); - $("#savedTextsModal .list .savedText .button.name").on("click", (e) => { - const name = $(e.target).text(); + qs("#savedTextsModal .list .savedText .button.name")?.on("click", (e) => { + const name = (e.target as HTMLElement).textContent; CustomTextState.setCustomTextName(name, false); const text = getSavedText(name, false); hide({ modalChainData: { text, long: false } }); }); - $("#savedTextsModal .listLong .savedLongText .button.name").on( + qs("#savedTextsModal .listLong .savedLongText .button.name")?.on( "click", (e) => { - const name = $(e.target).text(); + const name = (e.target as HTMLElement)?.textContent; CustomTextState.setCustomTextName(name, true); const text = getSavedText(name, true); hide({ modalChainData: { text, long: true } }); From 00c2dbc1bc22dbc97eab1541332d5867a11dbc64 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 20:49:20 +0800 Subject: [PATCH 21/28] refactor: remove jquery in modals/share-custom-theme --- frontend/src/ts/modals/share-custom-theme.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/modals/share-custom-theme.ts b/frontend/src/ts/modals/share-custom-theme.ts index 5fbfb8794bd9..67cd582b221b 100644 --- a/frontend/src/ts/modals/share-custom-theme.ts +++ b/frontend/src/ts/modals/share-custom-theme.ts @@ -2,6 +2,7 @@ import * as ThemeController from "../controllers/theme-controller"; import Config from "../config"; import * as Notifications from "../elements/notifications"; import AnimatedModal from "../utils/animated-modal"; +import { qsr } from "../utils/dom"; type State = { includeBackground: boolean; @@ -30,9 +31,9 @@ async function generateUrl(): Promise { } = { c: ThemeController.colorVars.map( (color) => - $(`.pageSettings .tabContent.customTheme #${color}[type='color']`).attr( - "value", - ) as string, + qsr( + `.pageSettings .tabContent.customTheme #${color}[type='color']`, + ).getValue() as string, ), }; From 0ce3d5cf002306101698e1d1edb084caa74afdd6 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 20:49:28 +0800 Subject: [PATCH 22/28] refactor: remove jquery in modals/share-test-settings --- frontend/src/ts/modals/share-test-settings.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/ts/modals/share-test-settings.ts b/frontend/src/ts/modals/share-test-settings.ts index c510bb4af965..fbcaeffe1113 100644 --- a/frontend/src/ts/modals/share-test-settings.ts +++ b/frontend/src/ts/modals/share-test-settings.ts @@ -6,11 +6,12 @@ import { compressToURI } from "lz-ts"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; import { Difficulty, FunboxName } from "@monkeytype/schemas/configs"; import { Mode, Mode2 } from "@monkeytype/schemas/shared"; +import { qsa, qsr } from "../utils/dom"; function getCheckboxValue(checkbox: string): boolean { - return $(`#shareTestSettingsModal label.${checkbox} input`).prop( - "checked", - ) as boolean; + return qsr( + `#shareTestSettingsModal label.${checkbox} input`, + ).isChecked() as boolean; } type SharedTestSettings = [ @@ -54,16 +55,16 @@ function updateURL(): void { } function updateShareModal(url: string): void { - const $modal = $(`#shareTestSettingsModal`); - $modal.find("textarea.url").val(url); - $modal.find(".tooLongWarning").toggleClass("hidden", url.length <= 2000); + const $modal = qsr(`#shareTestSettingsModal`); + $modal.qsr("textarea.url").setValue(url); + $modal.qsr(".tooLongWarning").toggleClass("hidden", url.length <= 2000); } function updateSubgroups(): void { if (getCheckboxValue("mode")) { - $(`#shareTestSettingsModal .subgroup`).removeClass("hidden"); + qsa(`#shareTestSettingsModal .subgroup`).show(); } else { - $(`#shareTestSettingsModal .subgroup`).addClass("hidden"); + qsa(`#shareTestSettingsModal .subgroup`).hide(); } } From ee11e16235d518e1fff83c7743843fb6f46a88a0 Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 20:49:35 +0800 Subject: [PATCH 23/28] refactor: remove jquery in modals/simple-modals --- frontend/src/ts/modals/simple-modals.ts | 73 +++++++++++++++---------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/frontend/src/ts/modals/simple-modals.ts b/frontend/src/ts/modals/simple-modals.ts index 1bf579504415..ea85b6f22f0a 100644 --- a/frontend/src/ts/modals/simple-modals.ts +++ b/frontend/src/ts/modals/simple-modals.ts @@ -46,6 +46,7 @@ import { goToPage } from "../pages/leaderboards"; import FileStorage from "../utils/file-storage"; import { z } from "zod"; import { remoteValidation } from "../utils/remote-validation"; +import { qs, qsr } from "../utils/dom"; type PopupKey = | "updateEmail" @@ -1141,9 +1142,9 @@ list.updateCustomTheme = new SimpleModal({ if (updateColors === "true") { for (const color of ThemeController.colorVars) { newColors.push( - $( + qsr( `.pageSettings .tabContent.customTheme #${color}[type='color']`, - ).attr("value") as string, + ).getValue() as string, ); } } else { @@ -1336,89 +1337,105 @@ export function showPopup( } //todo: move these event handlers to their respective files (either global event files or popup files) -$(".pageAccountSettings").on("click", "#unlinkDiscordButton", () => { +qs(".pageAccountSettings")?.onChild("click", "#unlinkDiscordButton", () => { showPopup("unlinkDiscord"); }); -$(".pageAccountSettings").on("click", "#removeGoogleAuth", () => { +qs(".pageAccountSettings")?.onChild("click", "#removeGoogleAuth", () => { showPopup("removeGoogleAuth"); }); -$(".pageAccountSettings").on("click", "#removeGithubAuth", () => { +qs(".pageAccountSettings")?.onChild("click", "#removeGithubAuth", () => { showPopup("removeGithubAuth"); }); -$(".pageAccountSettings").on("click", "#removePasswordAuth", () => { +qs(".pageAccountSettings")?.onChild("click", "#removePasswordAuth", () => { showPopup("removePasswordAuth"); }); -$("#resetSettingsButton").on("click", () => { +qs("#resetSettingsButton")?.on("click", () => { showPopup("resetSettings"); }); -$(".pageAccountSettings").on("click", "#revokeAllTokens", () => { +qs(".pageAccountSettings")?.onChild("click", "#revokeAllTokens", () => { showPopup("revokeAllTokens"); }); -$(".pageAccountSettings").on("click", "#resetPersonalBestsButton", () => { - showPopup("resetPersonalBests"); -}); +qs(".pageAccountSettings")?.onChild( + "click", + "#resetPersonalBestsButton", + () => { + showPopup("resetPersonalBests"); + }, +); -$(".pageAccountSettings").on("click", "#updateAccountName", () => { +qs(".pageAccountSettings")?.onChild("click", "#updateAccountName", () => { showPopup("updateName"); }); -$("#bannerCenter").on("click", ".banner .text .openNameChange", () => { +qs("#bannerCenter")?.onChild("click", ".banner .text .openNameChange", () => { showPopup("updateName"); }); -$(".pageAccountSettings").on("click", "#addPasswordAuth", () => { +qs(".pageAccountSettings")?.onChild("click", "#addPasswordAuth", () => { showPopup("addPasswordAuth"); }); -$(".pageAccountSettings").on("click", "#emailPasswordAuth", () => { +qs(".pageAccountSettings")?.onChild("click", "#emailPasswordAuth", () => { showPopup("updateEmail"); }); -$(".pageAccountSettings").on("click", "#passPasswordAuth", () => { +qs(".pageAccountSettings")?.onChild("click", "#passPasswordAuth", () => { showPopup("updatePassword"); }); -$(".pageAccountSettings").on("click", "#deleteAccount", () => { +qs(".pageAccountSettings")?.onChild("click", "#deleteAccount", () => { showPopup("deleteAccount"); }); -$(".pageAccountSettings").on("click", "#resetAccount", () => { +qs(".pageAccountSettings")?.onChild("click", "#resetAccount", () => { showPopup("resetAccount"); }); -$(".pageAccountSettings").on("click", "#optOutOfLeaderboardsButton", () => { - showPopup("optOutOfLeaderboards"); -}); +qs(".pageAccountSettings")?.onChild( + "click", + "#optOutOfLeaderboardsButton", + () => { + showPopup("optOutOfLeaderboards"); + }, +); -$(".pageSettings").on( +qs(".pageSettings")?.onChild( "click", ".section.themes .customTheme .delButton", (e) => { - const $parentElement = $(e.currentTarget).parent(".customTheme.button"); - const customThemeId = $parentElement.attr("customThemeId") as string; + const $parentElement = (e.childTarget as HTMLElement | null)?.closest( + ".customTheme.button", + ); + const customThemeId = $parentElement?.getAttribute( + "customThemeId", + ) as string; showPopup("deleteCustomTheme", [customThemeId]); }, ); -$(".pageSettings").on( +qs(".pageSettings")?.onChild( "click", ".section.themes .customTheme .editButton", (e) => { - const $parentElement = $(e.currentTarget).parent(".customTheme.button"); - const customThemeId = $parentElement.attr("customThemeId") as string; + const $parentElement = (e.childTarget as HTMLElement | null)?.closest( + ".customTheme.button", + ); + const customThemeId = $parentElement?.getAttribute( + "customThemeId", + ) as string; showPopup("updateCustomTheme", [customThemeId], { focusFirstInput: "focusAndSelect", }); }, ); -$(".pageSettings").on( +qs(".pageSettings")?.onChild( "click", ".section[data-config-name='fontFamily'] button[data-config-value='custom']", () => { From 2360d5c41c5e1b940dde35aa0767c120d68ebc7b Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 20:49:57 +0800 Subject: [PATCH 24/28] refactor: remove jquery in modals/user-report --- frontend/src/ts/modals/user-report.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/ts/modals/user-report.ts b/frontend/src/ts/modals/user-report.ts index 1878a09a7598..8a20a74058ec 100644 --- a/frontend/src/ts/modals/user-report.ts +++ b/frontend/src/ts/modals/user-report.ts @@ -83,8 +83,12 @@ async function submitReport(): Promise { return; } - const reason = $("#userReportModal .reason").val() as ReportUserReason; - const comment = $("#userReportModal .comment").val() as string; + const reason = qsr( + "#userReportModal .reason", + ).getValue() as ReportUserReason; + const comment = qsr( + "#userReportModal .comment", + ).getValue() as string; const captcha = captchaResponse; if (!reason) { From 3f58ffda6e365b70512f79ca9df3baebe2a6c19d Mon Sep 17 00:00:00 2001 From: Francis Eugene Casibu Date: Fri, 26 Dec 2025 20:50:09 +0800 Subject: [PATCH 25/28] refactor: remove jquery in modals/version-history --- frontend/src/ts/modals/version-history.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/ts/modals/version-history.ts b/frontend/src/ts/modals/version-history.ts index bc569b0652ab..5920ff6f1792 100644 --- a/frontend/src/ts/modals/version-history.ts +++ b/frontend/src/ts/modals/version-history.ts @@ -2,18 +2,21 @@ import { format } from "date-fns/format"; import { getReleasesFromGitHub } from "../utils/json-data"; import AnimatedModal from "../utils/animated-modal"; import { createErrorMessage } from "../utils/misc"; +import { qsr } from "../utils/dom"; export function show(): void { void modal.show({ beforeAnimation: async () => { - $("#versionHistoryModal .modal").html(` + qsr("#versionHistoryModal .modal").setHtml(`
{ - $("#versionHistoryModal .modal").html(`
{ if (!release.draft && !release.prerelease) { let body = release.body; @@ -32,7 +35,7 @@ export function show(): void { '$1', ); - $("#versionHistoryModal .modal .releases").append(` + qsr("#versionHistoryModal .modal .releases").appendHtml(`
${release.name}
${format( @@ -47,11 +50,11 @@ export function show(): void { }) .catch((e: unknown) => { const msg = createErrorMessage(e, "Failed to fetch version history"); - $("#versionHistoryModal .modal").html( + qsr("#versionHistoryModal .modal").setHtml( `
${msg} Date: Fri, 26 Dec 2025 20:50:16 +0800 Subject: [PATCH 26/28] refactor: remove jquery in modals/word-filter --- frontend/src/ts/modals/word-filter.ts | 83 +++++++++++++++++---------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/frontend/src/ts/modals/word-filter.ts b/frontend/src/ts/modals/word-filter.ts index ad0913a9dd64..16629e47a372 100644 --- a/frontend/src/ts/modals/word-filter.ts +++ b/frontend/src/ts/modals/word-filter.ts @@ -12,6 +12,7 @@ import { tryCatch } from "@monkeytype/util/trycatch"; import { LanguageList } from "../constants/languages"; import { Language } from "@monkeytype/schemas/languages"; import { LayoutObject } from "@monkeytype/schemas/layouts"; +import { qs, qsr } from "../utils/dom"; type FilterPreset = { display: string; @@ -26,7 +27,9 @@ type FilterPreset = { } ); -const exactMatchCheckbox = $("#wordFilterModal #exactMatchOnly"); +const exactMatchCheckbox = qs( + "#wordFilterModal #exactMatchOnly", +); const presets: Record = { homeKeys: { @@ -82,26 +85,26 @@ const presets: Record = { }; async function initSelectOptions(): Promise { - $("#wordFilterModal .languageInput").empty(); - $("#wordFilterModal .layoutInput").empty(); - $("wordFilterModal .presetInput").empty(); + qsr("#wordFilterModal .languageInput").empty(); + qsr("#wordFilterModal .layoutInput").empty(); + qsr("wordFilterModal .presetInput").empty(); LanguageList.forEach((language) => { const prettyLang = language.replace(/_/gi, " "); - $("#wordFilterModal .languageInput").append(` + qsr("#wordFilterModal .languageInput").appendHtml(` `); }); for (const layout of LayoutsList) { const prettyLayout = layout.replace(/_/gi, " "); - $("#wordFilterModal .layoutInput").append(` + qsr("#wordFilterModal .layoutInput").appendHtml(` `); } for (const [presetId, preset] of Object.entries(presets)) { - $("#wordFilterModal .presetInput").append( + qsr("#wordFilterModal .presetInput").appendHtml( ``, ); } @@ -133,7 +136,7 @@ export async function show(showOptions?: ShowOptions): Promise { contentLocation: modal.getModal(), }, }); - $("#wordFilterModal .loadingIndicator").removeClass("hidden"); + qsr("#wordFilterModal .loadingIndicator").removeClass("hidden"); enableButtons(); }, }); @@ -146,8 +149,10 @@ function hide(hideOptions?: HideOptions): void { } async function filter(language: Language): Promise { - const exactMatchOnly = exactMatchCheckbox.is(":checked"); - let filterin = $("#wordFilterModal .wordIncludeInput").val() as string; + const exactMatchOnly = exactMatchCheckbox?.isChecked() as boolean; + let filterin = qsr( + "#wordFilterModal .wordIncludeInput", + ).getValue() as string; filterin = Misc.escapeRegExp(filterin?.trim()); filterin = filterin.replace(/\s+/gi, "|"); let regincl; @@ -158,7 +163,9 @@ async function filter(language: Language): Promise { regincl = new RegExp(filterin, "i"); } - let filterout = $("#wordFilterModal .wordExcludeInput").val() as string; + let filterout = qsr( + "#wordFilterModal .wordExcludeInput", + ).getValue() as string; filterout = Misc.escapeRegExp(filterout.trim()); filterout = filterout.replace(/\s+/gi, "|"); const regexcl = new RegExp(filterout, "i"); @@ -175,8 +182,12 @@ async function filter(language: Language): Promise { return []; } - const maxLengthInput = $("#wordFilterModal .wordMaxInput").val() as string; - const minLengthInput = $("#wordFilterModal .wordMinInput").val() as string; + const maxLengthInput = qsr( + "#wordFilterModal .wordMaxInput", + ).getValue() as string; + const minLengthInput = qsr( + "#wordFilterModal .wordMinInput", + ).getValue() as string; let maxLength; let minLength; if (maxLengthInput === "") { @@ -204,7 +215,9 @@ async function filter(language: Language): Promise { } async function apply(set: boolean): Promise { - const language = $("#wordFilterModal .languageInput").val() as Language; + const language = qsr( + "#wordFilterModal .languageInput", + ).getValue() as Language; const filteredWords = await filter(language); if (filteredWords.length === 0) { @@ -226,16 +239,22 @@ async function apply(set: boolean): Promise { } function setExactMatchInput(disable: boolean): void { - const wordExcludeInputEl = $("#wordFilterModal #wordExcludeInput"); + const wordExcludeInputEl = qsr( + "#wordFilterModal #wordExcludeInput", + ); if (disable) { - $("#wordFilterModal #wordExcludeInput").val(""); - wordExcludeInputEl.attr("disabled", "disabled"); + wordExcludeInputEl.setValue(""); + wordExcludeInputEl.disable(); } else { - wordExcludeInputEl.removeAttr("disabled"); + wordExcludeInputEl.enable(); } - exactMatchCheckbox.prop("checked", disable); + if (disable) { + exactMatchCheckbox?.removeAttribute("checked"); + } else { + exactMatchCheckbox?.setAttribute("checked", "true"); + } } function disableButtons(): void { @@ -253,9 +272,13 @@ function enableButtons(): void { async function setup(): Promise { await initSelectOptions(); - $("#wordFilterModal button.generateButton").on("click", async () => { - const presetName = $("#wordFilterModal .presetInput").val() as string; - const layoutName = $("#wordFilterModal .layoutInput").val() as string; + qsr("#wordFilterModal button.generateButton").on("click", async () => { + const presetName = qsr( + "#wordFilterModal .presetInput", + ).getValue() as string; + const layoutName = qsr( + "#wordFilterModal .layoutInput", + ).getValue() as string; const presetToApply = presets[presetName]; @@ -266,7 +289,7 @@ async function setup(): Promise { const layout = await JSONData.getLayout(layoutName); - $("#wordIncludeInput").val( + qsr("#wordIncludeInput").setValue( presetToApply .getIncludeString(layout) .map((x) => x[0]) @@ -278,7 +301,7 @@ async function setup(): Promise { } else { setExactMatchInput(false); if (presetToApply.getExcludeString !== undefined) { - $("#wordExcludeInput").val( + qsr("#wordExcludeInput").setValue( presetToApply .getExcludeString(layout) .map((x) => x[0]) @@ -288,20 +311,20 @@ async function setup(): Promise { } }); - exactMatchCheckbox.on("change", () => { - setExactMatchInput(exactMatchCheckbox.is(":checked")); + exactMatchCheckbox?.on("change", () => { + setExactMatchInput(exactMatchCheckbox.isChecked() as boolean); }); - $("#wordFilterModal button.addButton").on("click", () => { - $("#wordFilterModal .loadingIndicator").removeClass("hidden"); + qsr("#wordFilterModal button.addButton").on("click", () => { + qsr("#wordFilterModal .loadingIndicator").show(); disableButtons(); setTimeout(() => { void apply(false); }, 0); }); - $("#wordFilterModal button.setButton").on("click", () => { - $("#wordFilterModal .loadingIndicator").removeClass("hidden"); + qsr("#wordFilterModal button.setButton").on("click", () => { + qsr("#wordFilterModal .loadingIndicator").show(); disableButtons(); setTimeout(() => { void apply(true); From 07ef2856456996d2650fdd28c041898bd43d5fb4 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 5 Jan 2026 15:26:01 +0100 Subject: [PATCH 27/28] setchecked --- frontend/src/ts/modals/custom-text.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/frontend/src/ts/modals/custom-text.ts b/frontend/src/ts/modals/custom-text.ts index 6d048e628b91..5cc880ef9638 100644 --- a/frontend/src/ts/modals/custom-text.ts +++ b/frontend/src/ts/modals/custom-text.ts @@ -135,10 +135,14 @@ function updateUI(): void { if (state.longCustomTextWarning) { qs(`${popup} .longCustomTextWarning`)?.show(); - qs(`${popup} .randomWordsCheckbox input`)?.removeAttribute("checked"); - qs(`${popup} .delimiterCheck input`)?.removeAttribute("checked"); - qs(`${popup} .typographyCheck`)?.setAttribute("checked", "true"); - qs(`${popup} .replaceNewlineWithSpace input`)?.removeAttribute("checked"); + qs(`${popup} .randomWordsCheckbox input`)?.setChecked( + false, + ); + qs(`${popup} .delimiterCheck input`)?.setChecked(false); + qs(`${popup} .typographyCheck`)?.setChecked(true); + qs(`${popup} .replaceNewlineWithSpace input`)?.setChecked( + false, + ); qs(`${popup} .inputs`)?.addClass("disabled"); } else { qs(`${popup} .longCustomTextWarning`)?.hide(); @@ -147,10 +151,14 @@ function updateUI(): void { if (state.challengeWarning) { qs(`${popup} .challengeWarning`)?.show(); - qs(`${popup} .randomWordsCheckbox input`)?.removeAttribute("checked"); - qs(`${popup} .delimiterCheck input`)?.removeAttribute("checked"); - qs(`${popup} .typographyCheck`)?.setAttribute("checked", "true"); - qs(`${popup} .replaceNewlineWithSpace input`)?.removeAttribute("checked"); + qs(`${popup} .randomWordsCheckbox input`)?.setChecked( + false, + ); + qs(`${popup} .delimiterCheck input`)?.setChecked(false); + qs(`${popup} .typographyCheck`)?.setChecked(true); + qs(`${popup} .replaceNewlineWithSpace input`)?.setChecked( + false, + ); qs(`${popup} .inputs`)?.addClass("disabled"); } else { qs(`${popup} .challengeWarning`)?.hide(); From 11c53d99106f3ee6e500371878f1c1739a7acf24 Mon Sep 17 00:00:00 2001 From: Miodec Date: Mon, 5 Jan 2026 15:26:57 +0100 Subject: [PATCH 28/28] focus --- frontend/src/ts/modals/custom-text.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/modals/custom-text.ts b/frontend/src/ts/modals/custom-text.ts index 5cc880ef9638..7a3789ac5e4f 100644 --- a/frontend/src/ts/modals/custom-text.ts +++ b/frontend/src/ts/modals/custom-text.ts @@ -219,7 +219,7 @@ async function beforeAnimation( async function afterAnimation(): Promise { if (!state.challengeWarning && !state.longCustomTextWarning) { - qs(`${popup} textarea`)?.native.focus(); + qs(`${popup} textarea`)?.focus(); } }