From 81829edde6709400ed695fb84eaa4b497c911ebd Mon Sep 17 00:00:00 2001 From: Leonabcd123 <156839416+Leonabcd123@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:54:34 +0200 Subject: [PATCH 1/7] refactor: remove jQuery from test directory (@Leonabcd123) (#7325) ### Description Closes #7186 --------- Co-authored-by: Miodec Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../__tests__/test/layout-emulator.spec.ts | 7 +- frontend/src/ts/test/focus.ts | 25 +- .../src/ts/test/funbox/funbox-functions.ts | 23 +- frontend/src/ts/test/funbox/funbox.ts | 23 +- .../test/funbox/layoutfluid-funbox-timer.ts | 16 +- .../src/ts/test/funbox/memory-funbox-timer.ts | 14 +- frontend/src/ts/test/layout-emulator.ts | 10 +- frontend/src/ts/test/live-acc.ts | 40 +- frontend/src/ts/test/live-burst.ts | 40 +- frontend/src/ts/test/live-speed.ts | 42 +- frontend/src/ts/test/monkey.ts | 94 ++- frontend/src/ts/test/out-of-focus.ts | 17 +- frontend/src/ts/test/pb-crown.ts | 24 +- frontend/src/ts/test/replay.ts | 24 +- frontend/src/ts/test/result.ts | 321 ++++----- frontend/src/ts/test/test-config.ts | 174 +++-- frontend/src/ts/test/test-logic.ts | 133 ++-- frontend/src/ts/test/test-screenshot.ts | 116 ++-- frontend/src/ts/test/test-ui.ts | 614 +++++++++--------- frontend/src/ts/utils/dom.ts | 70 +- 20 files changed, 949 insertions(+), 878 deletions(-) diff --git a/frontend/__tests__/test/layout-emulator.spec.ts b/frontend/__tests__/test/layout-emulator.spec.ts index 5dc0b902f5b9..e507ba663f8a 100644 --- a/frontend/__tests__/test/layout-emulator.spec.ts +++ b/frontend/__tests__/test/layout-emulator.spec.ts @@ -13,14 +13,11 @@ describe("LayoutEmulator", () => { updateAltGrState(event); }); - const createEvent = ( - code: string, - type: string, - ): JQuery.KeyboardEventBase => + const createEvent = (code: string, type: string): KeyboardEvent => ({ code, type, - }) as JQuery.KeyboardEventBase; + }) as KeyboardEvent; it("should set isAltGrPressed to true on AltRight keydown", () => { const event = createEvent("AltRight", "keydown"); diff --git a/frontend/src/ts/test/focus.ts b/frontend/src/ts/test/focus.ts index 7c9ee58cccbf..41cf2de3257b 100644 --- a/frontend/src/ts/test/focus.ts +++ b/frontend/src/ts/test/focus.ts @@ -6,13 +6,14 @@ import * as TimerProgress from "./timer-progress"; import * as PageTransition from "../states/page-transition"; import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame"; import { getFocus, setFocus } from "../signals/core"; +import { qsa, ElementsWithUtils } from "../utils/dom"; const unfocusPx = 3; let cacheReady = false; let cache: { - focus?: HTMLElement[]; - cursor?: HTMLElement[]; + focus?: ElementsWithUtils; + cursor?: ElementsWithUtils; } = {}; function initializeCache(): void { @@ -33,8 +34,8 @@ function initializeCache(): void { "#ad-footer-small-wrapper", ].join(","); - cache.cursor = [...document.querySelectorAll(cursorSelector)]; - cache.focus = [...document.querySelectorAll(elementsSelector)]; + cache.cursor = qsa(cursorSelector); + cache.focus = qsa(elementsSelector); cacheReady = true; } @@ -50,14 +51,10 @@ export function set(value: boolean, withCursor = false): void { // batch DOM operations for better performance if (cache.focus) { - for (const el of cache.focus) { - el.classList.add("focus"); - } + cache.focus.addClass("focus"); } if (!withCursor && cache.cursor) { - for (const el of cache.cursor) { - el.style.cursor = "none"; - } + cache.cursor.setStyle({ cursor: "none" }); } Caret.stopAnimation(); @@ -69,14 +66,10 @@ export function set(value: boolean, withCursor = false): void { setFocus(false); if (cache.focus) { - for (const el of cache.focus) { - el.classList.remove("focus"); - } + cache.focus.removeClass("focus"); } if (cache.cursor) { - for (const el of cache.cursor) { - el.style.cursor = ""; - } + cache.cursor.setStyle({ cursor: "" }); } Caret.startAnimation(); diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index 200720604cc0..564881d3644d 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -23,6 +23,7 @@ import * as TestState from "../test-state"; import { WordGenError } from "../../utils/word-gen-error"; import { FunboxName, KeymapLayout, Layout } from "@monkeytype/schemas/configs"; import { Language, LanguageObject } from "@monkeytype/schemas/languages"; +import { qs } from "../../utils/dom"; export type FunboxFunctions = { getWord?: (wordset?: Wordset, wordIndex?: number) => string; @@ -61,9 +62,9 @@ async function readAheadHandleKeydown(event: KeyboardEvent): Promise { TestWords.words.get(TestState.activeWordIndex - 1) || Config.freedomMode) ) { - $("#words").addClass("read_ahead_disabled"); + qs("#words")?.addClass("read_ahead_disabled"); } else if (event.key === " ") { - $("#words").removeClass("read_ahead_disabled"); + qs("#words")?.removeClass("read_ahead_disabled"); } } @@ -510,7 +511,7 @@ const list: Partial> = { }, memory: { applyConfig(): void { - $("#wordsWrapper").addClass("hidden"); + qs("#wordsWrapper")?.hide(); setConfig("showAllLines", true, { nosave: true, }); @@ -529,11 +530,11 @@ const list: Partial> = { }, start(): void { MemoryTimer.reset(); - $("#words").addClass("hidden"); + qs("#words")?.hide(); }, restart(): void { MemoryTimer.start(Math.round(Math.pow(TestWords.words.length, 1.2))); - $("#words").removeClass("hidden"); + qs("#words")?.show(); if (Config.keymapMode === "next") { setConfig("keymapMode", "react"); } @@ -672,14 +673,14 @@ const list: Partial> = { return; } } - $("body").append('
'); - $("body").addClass("crtmode"); - $("#globalFunBoxTheme").attr("href", `funbox/crt.css`); + qs("body")?.appendHtml('
'); + qs("body")?.addClass("crtmode"); + qs("#globalFunBoxTheme")?.setAttribute("href", `funbox/crt.css`); }, clearGlobal(): void { - $("#scanline").remove(); - $("body").removeClass("crtmode"); - $("#globalFunBoxTheme").attr("href", ``); + qs("#scanline")?.remove(); + qs("body")?.removeClass("crtmode"); + qs("#globalFunBoxTheme")?.setAttribute("href", ``); }, }, ALL_CAPS: { diff --git a/frontend/src/ts/test/funbox/funbox.ts b/frontend/src/ts/test/funbox/funbox.ts index f3e20af3a687..8bfb56e0cce4 100644 --- a/frontend/src/ts/test/funbox/funbox.ts +++ b/frontend/src/ts/test/funbox/funbox.ts @@ -22,6 +22,7 @@ import { } from "./list"; import { checkForcedConfig } from "./funbox-validation"; import { tryCatch } from "@monkeytype/util/trycatch"; +import { qs } from "../../utils/dom"; export function toggleScript(...params: string[]): void { if (Config.funbox.length === 0) return; @@ -66,18 +67,18 @@ export function toggleFunbox(funbox: FunboxName): void { } export async function clear(): Promise { - $("body").attr( + qs("body")?.setAttribute( "class", - $("body") - ?.attr("class") + qs("body") + ?.getAttribute("class") ?.split(/\s+/) ?.filter((it) => !it.startsWith("fb-")) ?.join(" ") ?? "", ); - $(".funBoxTheme").remove(); + qs(".funBoxTheme")?.remove(); - $("#wordsWrapper").removeClass("hidden"); + qs("#wordsWrapper")?.show(); MemoryTimer.reset(); ManualRestart.set(); return true; @@ -115,7 +116,7 @@ export async function activate( await setFunboxBodyClasses(); await applyFunboxCSS(); - $("#wordsWrapper").removeClass("hidden"); + qs("#wordsWrapper")?.show(); const { data: language, error } = await tryCatch( JSONData.getCurrentLanguage(Config.language), @@ -216,15 +217,15 @@ export async function rememberSettings(): Promise { } async function setFunboxBodyClasses(): Promise { - const $body = $("body"); + const body = qs("body"); const activeFbClasses = getActiveFunboxNames().map( (name) => "fb-" + name.replaceAll("_", "-"), ); const currentClasses = - $body - ?.attr("class") + body + ?.getAttribute("class") ?.split(/\s+/) .filter((it) => !it.startsWith("fb-")) ?? []; @@ -232,7 +233,7 @@ async function setFunboxBodyClasses(): Promise { currentClasses.push("ignore-reduced-motion"); } - $body.attr( + body?.setAttribute( "class", [...new Set([...currentClasses, ...activeFbClasses]).keys()].join(" "), ); @@ -241,7 +242,7 @@ async function setFunboxBodyClasses(): Promise { } async function applyFunboxCSS(): Promise { - $(".funBoxTheme").remove(); + qs(".funBoxTheme")?.remove(); for (const funbox of getActiveFunboxesWithProperty("hasCssFile")) { const css = document.createElement("link"); css.classList.add("funBoxTheme"); diff --git a/frontend/src/ts/test/funbox/layoutfluid-funbox-timer.ts b/frontend/src/ts/test/funbox/layoutfluid-funbox-timer.ts index f80d95a025a1..aa3fce6c22ea 100644 --- a/frontend/src/ts/test/funbox/layoutfluid-funbox-timer.ts +++ b/frontend/src/ts/test/funbox/layoutfluid-funbox-timer.ts @@ -1,31 +1,29 @@ -import { animate } from "animejs"; import { capitalizeFirstLetter } from "../../utils/strings"; import { applyReducedMotion } from "../../utils/misc"; +import { qs } from "../../utils/dom"; -const timerEl = document.querySelector( - "#typingTest #layoutfluidTimer", -) as HTMLElement; +const timerEl = qs("#typingTest #layoutfluidTimer"); export function show(): void { - animate(timerEl, { + timerEl?.animate({ opacity: 1, duration: applyReducedMotion(125), }); } export function hide(): void { - animate(timerEl, { + timerEl?.animate({ opacity: 0, duration: applyReducedMotion(125), }); } export function instantHide(): void { - timerEl.style.opacity = "0"; + timerEl?.setStyle({ opacity: "0" }); } export function updateTime(sec: number, layout: string): void { - timerEl.textContent = `${capitalizeFirstLetter(layout)} in: ${sec}s`; + timerEl?.setText(`${capitalizeFirstLetter(layout)} in: ${sec}s`); } export function updateWords(words: number, layout: string): void { @@ -34,5 +32,5 @@ export function updateWords(words: number, layout: string): void { if (words === 1) { str = `${layoutName} starting next word`; } - timerEl.textContent = str; + timerEl?.setText(str); } diff --git a/frontend/src/ts/test/funbox/memory-funbox-timer.ts b/frontend/src/ts/test/funbox/memory-funbox-timer.ts index e5dba6f4a65e..9cca426fc038 100644 --- a/frontend/src/ts/test/funbox/memory-funbox-timer.ts +++ b/frontend/src/ts/test/funbox/memory-funbox-timer.ts @@ -1,22 +1,20 @@ -import { animate } from "animejs"; import { applyReducedMotion } from "../../utils/misc"; +import { qs } from "../../utils/dom"; let memoryTimer: number | null = null; let memoryInterval: NodeJS.Timeout | null = null; -const timerEl = document.querySelector( - "#typingTest #memoryTimer", -) as HTMLElement; +const timerEl = qs("#typingTest #memoryTimer"); export function show(): void { - animate(timerEl, { + timerEl?.animate({ opacity: 1, duration: applyReducedMotion(125), }); } export function hide(): void { - animate(timerEl, { + timerEl?.animate({ opacity: 0, duration: applyReducedMotion(125), }); @@ -42,11 +40,11 @@ export function start(time: number): void { memoryTimer === 0 ? hide() : update(memoryTimer); if (memoryTimer <= 0) { reset(); - $("#wordsWrapper").addClass("hidden"); + qs("#wordsWrapper")?.hide(); } }, 1000); } export function update(sec: number): void { - timerEl.textContent = `Timer left to memorise all words: ${sec}s`; + timerEl?.setText(`Timer left to memorise all words: ${sec}s`); } diff --git a/frontend/src/ts/test/layout-emulator.ts b/frontend/src/ts/test/layout-emulator.ts index 998da28717f2..bb8f421f15c6 100644 --- a/frontend/src/ts/test/layout-emulator.ts +++ b/frontend/src/ts/test/layout-emulator.ts @@ -11,10 +11,10 @@ let isAltGrPressed = false; const isPunctuationPattern = /\p{P}/u; export async function getCharFromEvent( - event: JQuery.KeyDownEvent | JQuery.KeyUpEvent | KeyboardEvent, + event: KeyboardEvent, ): Promise { function emulatedLayoutGetVariant( - event: JQuery.KeyDownEvent | JQuery.KeyUpEvent | KeyboardEvent, + event: KeyboardEvent, keyVariants: string[], ): string | undefined { let isCapitalized = event.shiftKey; @@ -238,7 +238,7 @@ export async function getCharFromEvent( } } -export function updateAltGrState(event: JQuery.KeyboardEventBase): void { +export function updateAltGrState(event: KeyboardEvent): void { const shouldHandleLeftAlt = event.code === "AltLeft" && navigator.userAgent.includes("Mac"); if (event.code !== "AltRight" && !shouldHandleLeftAlt) return; @@ -250,5 +250,5 @@ export function getIsAltGrPressed(): boolean { return isAltGrPressed; } -$(document).on("keydown", updateAltGrState); -$(document).on("keyup", updateAltGrState); +document.addEventListener("keydown", updateAltGrState); +document.addEventListener("keyup", updateAltGrState); diff --git a/frontend/src/ts/test/live-acc.ts b/frontend/src/ts/test/live-acc.ts index 3158d0e36928..e780c94f5a76 100644 --- a/frontend/src/ts/test/live-acc.ts +++ b/frontend/src/ts/test/live-acc.ts @@ -2,13 +2,11 @@ import Config from "../config"; import * as TestState from "../test/test-state"; import * as ConfigEvent from "../observables/config-event"; import { applyReducedMotion } from "../utils/misc"; -import { animate } from "animejs"; import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame"; +import { qs } from "../utils/dom"; -const textEl = document.querySelector( - "#liveStatsTextBottom .liveAcc", -) as HTMLElement; -const miniEl = document.querySelector("#liveStatsMini .acc") as HTMLElement; +const textEl = qs("#liveStatsTextBottom .liveAcc"); +const miniEl = qs("#liveStatsMini .acc"); export function update(acc: number): void { requestDebouncedAnimationFrame("live-acc.update", () => { @@ -16,15 +14,15 @@ export function update(acc: number): void { if (Config.blindMode) { number = 100; } - miniEl.innerHTML = number + "%"; - textEl.innerHTML = number + "%"; + miniEl?.setHtml(number + "%"); + textEl?.setHtml(number + "%"); }); } export function reset(): void { requestDebouncedAnimationFrame("live-acc.reset", () => { - miniEl.innerHTML = "100%"; - textEl.innerHTML = "100%"; + miniEl?.setHtml("100%"); + textEl?.setHtml("100%"); }); } @@ -36,14 +34,14 @@ export function show(): void { if (state) return; requestDebouncedAnimationFrame("live-acc.show", () => { if (Config.liveAccStyle === "mini") { - miniEl.classList.remove("hidden"); - animate(miniEl, { + miniEl?.show(); + miniEl?.animate({ opacity: [0, 1], duration: applyReducedMotion(125), }); } else { - textEl.classList.remove("hidden"); - animate(textEl, { + textEl?.show(); + textEl?.animate({ opacity: [0, 1], duration: applyReducedMotion(125), }); @@ -55,18 +53,18 @@ export function show(): void { export function hide(): void { if (!state) return; requestDebouncedAnimationFrame("live-acc.hide", () => { - animate(textEl, { + textEl?.animate({ opacity: [1, 0], duration: applyReducedMotion(125), onComplete: () => { - textEl.classList.add("hidden"); + textEl?.hide(); }, }); - animate(miniEl, { + miniEl?.animate({ opacity: [1, 0], duration: applyReducedMotion(125), onComplete: () => { - miniEl.classList.add("hidden"); + miniEl?.hide(); }, }); state = false; @@ -76,10 +74,10 @@ export function hide(): void { export function instantHide(): void { if (!state) return; - textEl.classList.add("hidden"); - textEl.style.opacity = "0"; - miniEl.classList.add("hidden"); - miniEl.style.opacity = "0"; + textEl?.hide(); + textEl?.setStyle({ opacity: "0" }); + miniEl?.hide(); + miniEl?.setStyle({ opacity: "0" }); state = false; } diff --git a/frontend/src/ts/test/live-burst.ts b/frontend/src/ts/test/live-burst.ts index b37537d5de30..be9d9190d04e 100644 --- a/frontend/src/ts/test/live-burst.ts +++ b/frontend/src/ts/test/live-burst.ts @@ -3,26 +3,24 @@ import * as TestState from "../test/test-state"; import * as ConfigEvent from "../observables/config-event"; import Format from "../utils/format"; import { applyReducedMotion } from "../utils/misc"; -import { animate } from "animejs"; import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame"; +import { qs } from "../utils/dom"; -const textEl = document.querySelector( - "#liveStatsTextBottom .liveBurst", -) as HTMLElement; -const miniEl = document.querySelector("#liveStatsMini .burst") as HTMLElement; +const textEl = qs("#liveStatsTextBottom .liveBurst"); +const miniEl = qs("#liveStatsMini .burst"); export function reset(): void { requestDebouncedAnimationFrame("live-burst.reset", () => { - textEl.innerHTML = "0"; - miniEl.innerHTML = "0"; + textEl?.setHtml("0"); + miniEl?.setHtml("0"); }); } export async function update(burst: number): Promise { requestDebouncedAnimationFrame("live-burst.update", () => { const burstText = Format.typingSpeed(burst, { showDecimalPlaces: false }); - miniEl.innerHTML = burstText; - textEl.innerHTML = burstText; + miniEl?.setHtml(burstText); + textEl?.setHtml(burstText); }); } @@ -34,14 +32,14 @@ export function show(): void { if (state) return; requestDebouncedAnimationFrame("live-burst.show", () => { if (Config.liveBurstStyle === "mini") { - miniEl.classList.remove("hidden"); - animate(miniEl, { + miniEl?.show(); + miniEl?.animate({ opacity: [0, 1], duration: applyReducedMotion(125), }); } else { - textEl.classList.remove("hidden"); - animate(textEl, { + textEl?.show(); + textEl?.animate({ opacity: [0, 1], duration: applyReducedMotion(125), }); @@ -53,18 +51,18 @@ export function show(): void { export function hide(): void { if (!state) return; requestDebouncedAnimationFrame("live-burst.hide", () => { - animate(textEl, { + textEl?.animate({ opacity: [1, 0], duration: applyReducedMotion(125), onComplete: () => { - textEl.classList.add("hidden"); + textEl?.hide(); }, }); - animate(miniEl, { + miniEl?.animate({ opacity: [1, 0], duration: applyReducedMotion(125), onComplete: () => { - miniEl.classList.add("hidden"); + miniEl?.hide(); }, }); state = false; @@ -74,10 +72,10 @@ export function hide(): void { export function instantHide(): void { if (!state) return; - textEl.classList.add("hidden"); - textEl.style.opacity = "0"; - miniEl.classList.add("hidden"); - miniEl.style.opacity = "0"; + textEl?.hide(); + textEl?.setStyle({ opacity: "0" }); + miniEl?.hide(); + miniEl?.setStyle({ opacity: "0" }); state = false; } diff --git a/frontend/src/ts/test/live-speed.ts b/frontend/src/ts/test/live-speed.ts index cef8273e4c04..a5a929bd6034 100644 --- a/frontend/src/ts/test/live-speed.ts +++ b/frontend/src/ts/test/live-speed.ts @@ -4,19 +4,15 @@ import * as ConfigEvent from "../observables/config-event"; import Format from "../utils/format"; import { applyReducedMotion } from "../utils/misc"; import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame"; -import { animate } from "animejs"; +import { qs } from "../utils/dom"; -const textElement = document.querySelector( - "#liveStatsTextBottom .liveSpeed", -) as HTMLElement; -const miniElement = document.querySelector( - "#liveStatsMini .speed", -) as HTMLElement; +const textElement = qs("#liveStatsTextBottom .liveSpeed"); +const miniElement = qs("#liveStatsMini .speed"); export function reset(): void { requestDebouncedAnimationFrame("live-speed.reset", () => { - textElement.innerHTML = "0"; - miniElement.innerHTML = "0"; + textElement?.setHtml("0"); + miniElement?.setHtml("0"); }); } @@ -27,8 +23,8 @@ export function update(wpm: number, raw: number): void { number = raw; } const numberText = Format.typingSpeed(number, { showDecimalPlaces: false }); - textElement.innerHTML = numberText; - miniElement.innerHTML = numberText; + textElement?.setHtml(numberText); + miniElement?.setHtml(numberText); }); } @@ -40,14 +36,14 @@ export function show(): void { if (state) return; requestDebouncedAnimationFrame("live-speed.show", () => { if (Config.liveSpeedStyle === "mini") { - miniElement.classList.remove("hidden"); - animate(miniElement, { + miniElement?.show(); + miniElement?.animate({ opacity: [0, 1], duration: applyReducedMotion(125), }); } else { - textElement.classList.remove("hidden"); - animate(textElement, { + textElement?.show(); + textElement?.animate({ opacity: [0, 1], duration: applyReducedMotion(125), }); @@ -59,18 +55,18 @@ export function show(): void { export function hide(): void { if (!state) return; requestDebouncedAnimationFrame("live-speed.hide", () => { - animate(miniElement, { + miniElement?.animate({ opacity: [1, 0], duration: applyReducedMotion(125), onComplete: () => { - miniElement.classList.add("hidden"); + miniElement?.hide(); }, }); - animate(textElement, { + textElement?.animate({ opacity: [1, 0], duration: applyReducedMotion(125), onComplete: () => { - textElement.classList.add("hidden"); + textElement?.hide(); }, }); state = false; @@ -79,10 +75,10 @@ export function hide(): void { export function instantHide(): void { if (!state) return; - miniElement.classList.add("hidden"); - miniElement.style.opacity = "0"; - textElement.classList.add("hidden"); - textElement.style.opacity = "0"; + miniElement?.hide(); + miniElement?.setStyle({ opacity: "0" }); + textElement?.hide(); + textElement?.setStyle({ opacity: "0" }); state = false; } diff --git a/frontend/src/ts/test/monkey.ts b/frontend/src/ts/test/monkey.ts index b1966c3e4c86..17a19ba8adab 100644 --- a/frontend/src/ts/test/monkey.ts +++ b/frontend/src/ts/test/monkey.ts @@ -3,17 +3,17 @@ import Config from "../config"; import * as ConfigEvent from "../observables/config-event"; import * as TestState from "../test/test-state"; import * as KeyConverter from "../utils/key-converter"; -import { animate } from "animejs"; +import { qs } from "../utils/dom"; -const monkeyEl = document.querySelector("#monkey") as HTMLElement; -const monkeyFastEl = document.querySelector("#monkey .fast") as HTMLElement; +const monkeyEl = qs("#monkey"); +const monkeyFastEl = qs("#monkey .fast"); ConfigEvent.subscribe(({ key }) => { if (key === "monkey" && TestState.isActive) { if (Config.monkey) { - monkeyEl.classList.remove("hidden"); + monkeyEl?.show(); } else { - monkeyEl.classList.add("hidden"); + monkeyEl?.hide(); } } }); @@ -22,62 +22,44 @@ let left = false; let right = false; const middleKeysState = { left: false, right: false, last: "right" }; -// 0 hand up -// 1 hand down - -// 00 both hands up -// 01 right hand down -// 10 left hand down -// 11 both hands down - -const elements = { - "00": monkeyEl.querySelector(".up"), - "01": monkeyEl.querySelector(".right"), - "10": monkeyEl.querySelector(".left"), - "11": monkeyEl.querySelector(".both"), -}; - -const elementsFast = { - "00": monkeyFastEl.querySelector(".up"), - "01": monkeyFastEl.querySelector(".right"), - "10": monkeyFastEl.querySelector(".left"), - "11": monkeyFastEl.querySelector(".both"), -}; - -function toBit(b: boolean): "1" | "0" { - return b ? "1" : "0"; -} +const upEls = monkeyEl?.qsa(".up"); +const rightEls = monkeyEl?.qsa(".right"); +const leftEls = monkeyEl?.qsa(".left"); +const bothEls = monkeyEl?.qsa(".both"); function update(): void { if (!Config.monkey) return; - if (!monkeyEl?.classList.contains("hidden")) { - (Object.keys(elements) as (keyof typeof elements)[]).forEach((key) => { - elements[key]?.classList.add("hidden"); - }); - (Object.keys(elementsFast) as (keyof typeof elements)[]).forEach((key) => { - elementsFast[key]?.classList.add("hidden"); - }); - - const id: keyof typeof elements = `${toBit(left)}${toBit(right)}`; - - elements[id]?.classList.remove("hidden"); - elementsFast[id]?.classList.remove("hidden"); + if (!monkeyEl?.hasClass("hidden")) { + upEls?.hide(); + rightEls?.hide(); + leftEls?.hide(); + bothEls?.hide(); + + if (left && right) { + bothEls?.show(); + } else if (right) { + rightEls?.show(); + } else if (left) { + leftEls?.show(); + } else { + upEls?.show(); + } } } export function updateFastOpacity(num: number): void { if (!Config.monkey) return; const opacity = mapRange(num, 130, 180, 0, 1); - animate(monkeyFastEl, { + monkeyFastEl?.animate({ opacity: opacity, duration: 1000, }); let animDuration = mapRange(num, 130, 180, 0.25, 0.01); if (animDuration === 0.25) animDuration = 0; - monkeyEl.style.animationDuration = animDuration + "s"; + monkeyEl?.setStyle({ animationDuration: animDuration + "s" }); } -export function type(event: JQuery.KeyDownEvent | KeyboardEvent): void { +export function type(event: KeyboardEvent): void { if (!Config.monkey) return; const { leftSide, rightSide } = KeyConverter.keycodeToKeyboardSide( @@ -115,7 +97,7 @@ export function type(event: JQuery.KeyDownEvent | KeyboardEvent): void { update(); } -export function stop(event: JQuery.KeyUpEvent | KeyboardEvent): void { +export function stop(event: KeyboardEvent): void { if (!Config.monkey) return; const { leftSide, rightSide } = KeyConverter.keycodeToKeyboardSide( @@ -144,28 +126,28 @@ export function stop(event: JQuery.KeyUpEvent | KeyboardEvent): void { export function show(): void { if (!Config.monkey) return; - monkeyEl.classList.remove("hidden"); - animate(monkeyEl, { + monkeyEl?.show(); + monkeyEl?.animate({ opacity: [0, 1], duration: 125, }); } export function hide(): void { - animate(monkeyEl, { + monkeyEl?.animate({ opacity: [1, 0], duration: 125, onComplete: () => { - monkeyEl.classList.add("hidden"); - monkeyEl.style.animationDuration = "0s"; - monkeyFastEl.style.opacity = "0"; + monkeyEl?.hide(); + monkeyEl?.setStyle({ animationDuration: "0s" }); + monkeyFastEl?.setStyle({ opacity: "0" }); }, }); } export function instantHide(): void { - monkeyEl.classList.add("hidden"); - monkeyEl.style.opacity = "0"; - monkeyEl.style.animationDuration = "0s"; - monkeyFastEl.style.opacity = "0"; + monkeyEl?.hide(); + monkeyEl?.setStyle({ opacity: "0" }); + monkeyEl?.setStyle({ animationDuration: "0s" }); + monkeyFastEl?.setStyle({ opacity: "0" }); } diff --git a/frontend/src/ts/test/out-of-focus.ts b/frontend/src/ts/test/out-of-focus.ts index 5a9050583954..05d4ee05db5b 100644 --- a/frontend/src/ts/test/out-of-focus.ts +++ b/frontend/src/ts/test/out-of-focus.ts @@ -1,13 +1,14 @@ import * as Misc from "../utils/misc"; import Config from "../config"; +import { qs, qsa } from "../utils/dom"; const outOfFocusTimeouts: (number | NodeJS.Timeout)[] = []; export function hide(): void { - $("#words, #compositionDisplay") - .css("transition", "none") - .removeClass("blurred"); - $(".outOfFocusWarning").addClass("hidden"); + qsa("#words, #compositionDisplay") + ?.setStyle({ transition: "none" }) + ?.removeClass("blurred"); + qs(".outOfFocusWarning")?.hide(); Misc.clearTimeouts(outOfFocusTimeouts); } @@ -15,10 +16,10 @@ export function show(): void { if (!Config.showOutOfFocusWarning) return; outOfFocusTimeouts.push( setTimeout(() => { - $("#words, #compositionDisplay") - .css("transition", "0.25s") - .addClass("blurred"); - $(".outOfFocusWarning").removeClass("hidden"); + qsa("#words, #compositionDisplay") + ?.setStyle({ transition: "0.25s" }) + ?.addClass("blurred"); + qs(".outOfFocusWarning")?.show(); }, 1000), ); } diff --git a/frontend/src/ts/test/pb-crown.ts b/frontend/src/ts/test/pb-crown.ts index 5eefc560bde1..d64c2f9218cc 100644 --- a/frontend/src/ts/test/pb-crown.ts +++ b/frontend/src/ts/test/pb-crown.ts @@ -1,9 +1,9 @@ -import { animate } from "animejs"; import { applyReducedMotion } from "../utils/misc"; +import { qs } from "../utils/dom"; export function hide(): void { visible = false; - $("#result .stats .wpm .crown").css("opacity", 0).addClass("hidden"); + qs("#result .stats .wpm .crown")?.setStyle({ opacity: "0" })?.hide(); } export type CrownType = @@ -23,25 +23,23 @@ export function getCurrentType(): CrownType { export function show(): void { if (visible) return; visible = true; - const el = document.querySelector( - "#result .stats .wpm .crown", - ) as HTMLElement; + const el = qs("#result .stats .wpm .crown"); - animate(el, { + el?.animate({ opacity: [0, 1], duration: applyReducedMotion(125), onBegin: () => { - el.classList.remove("hidden"); + el?.show(); }, }); } export function update(type: CrownType): void { currentType = type; - const el = $("#result .stats .wpm .crown"); - el.removeClass("ineligible"); - el.removeClass("pending"); - el.removeClass("error"); - el.removeClass("warning"); - el.addClass(type); + qs("#result .stats .wpm .crown") + ?.removeClass("ineligible") + ?.removeClass("pending") + ?.removeClass("error") + ?.removeClass("warning") + ?.addClass(type); } diff --git a/frontend/src/ts/test/replay.ts b/frontend/src/ts/test/replay.ts index 81659e268c1d..b6dee822a5dc 100644 --- a/frontend/src/ts/test/replay.ts +++ b/frontend/src/ts/test/replay.ts @@ -2,7 +2,7 @@ import config from "../config"; import * as Sound from "../controllers/sound-controller"; import * as TestInput from "./test-input"; import * as Arrays from "../utils/arrays"; -import { qsr } from "../utils/dom"; +import { qs, qsr } from "../utils/dom"; type ReplayAction = | "correctLetter" @@ -231,7 +231,7 @@ function addReplayEvent(action: ReplayAction, value?: number | string): void { function updateStatsString(time: number): void { const wpm = TestInput.wpmHistory[time - 1] ?? 0; const statsString = `${wpm}wpm\t${time}s`; - $("#replayStats").text(statsString); + qs("#replayStats")?.setText(statsString); } function playReplay(): void { @@ -301,7 +301,7 @@ function getReplayExport(): string { }); } -$(".pageTest #playpauseReplayButton").on("click", () => { +qs(".pageTest #playpauseReplayButton")?.on("click", () => { if (toggleButton?.className === "fas fa-play") { playReplay(); } else if (toggleButton?.className === "fas fa-pause") { @@ -309,23 +309,25 @@ $(".pageTest #playpauseReplayButton").on("click", () => { } }); -$("#replayWords").on("click", "letter", (event) => { +qs("#replayWords")?.onChild("click", "letter", (event) => { //allows user to click on the place they want to start their replay at pauseReplay(); - const replayWords = document.querySelector("#replayWords"); + const replayWords = qs("#replayWords"); + + const words = [...(replayWords?.native?.children ?? [])]; + targetWordPos = + words?.indexOf( + (event.childTarget as HTMLElement).parentNode as HTMLElement, + ) ?? 0; - const words = [...(replayWords?.children ?? [])]; - targetWordPos = words.indexOf( - (event.target as HTMLElement).parentNode as HTMLElement, - ); const letters = [...(words[targetWordPos] as HTMLElement).children]; - targetCurPos = letters.indexOf(event.target as HTMLElement); + targetCurPos = letters?.indexOf(event.childTarget as HTMLElement) ?? 0; initializeReplayPrompt(); loadOldReplay(); }); -$(".pageTest").on("click", "#watchReplayButton", () => { +qs(".pageTest")?.onChild("click", "#watchReplayButton", () => { toggleReplayDisplay(); }); diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index f87a752c2cca..8da4653d464f 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -48,6 +48,7 @@ import * as TestState from "./test-state"; import { blurInputElement } from "../input/input-element"; import * as ConnectionState from "../states/connection"; import { currentQuote } from "./test-words"; +import { qs, qsa } from "../utils/dom"; import { getTheme } from "../signals/theme"; let result: CompletedEvent; @@ -333,41 +334,41 @@ function updateWpmAndAcc(): void { inf = true; } - $("#result .stats .wpm .top .text").text(Config.typingSpeedUnit); + qs("#result .stats .wpm .top .text")?.setText(Config.typingSpeedUnit); if (inf) { - $("#result .stats .wpm .bottom").text("Infinite"); + qs("#result .stats .wpm .bottom")?.setText("Infinite"); } else { - $("#result .stats .wpm .bottom").text(Format.typingSpeed(result.wpm)); + qs("#result .stats .wpm .bottom")?.setText(Format.typingSpeed(result.wpm)); } - $("#result .stats .raw .bottom").text(Format.typingSpeed(result.rawWpm)); - $("#result .stats .acc .bottom").text( + qs("#result .stats .raw .bottom")?.setText(Format.typingSpeed(result.rawWpm)); + qs("#result .stats .acc .bottom")?.setText( result.acc === 100 ? "100%" : Format.accuracy(result.acc), ); if (Config.alwaysShowDecimalPlaces) { if (Config.typingSpeedUnit !== "wpm") { - $("#result .stats .wpm .bottom").attr( + qs("#result .stats .wpm .bottom")?.setAttribute( "aria-label", result.wpm.toFixed(2) + " wpm", ); - $("#result .stats .raw .bottom").attr( + qs("#result .stats .raw .bottom")?.setAttribute( "aria-label", result.rawWpm.toFixed(2) + " wpm", ); } else { - $("#result .stats .wpm .bottom").removeAttr("aria-label"); - $("#result .stats .raw .bottom").removeAttr("aria-label"); + qs("#result .stats .wpm .bottom")?.removeAttribute("aria-label"); + qs("#result .stats .raw .bottom")?.removeAttribute("aria-label"); } let time = Numbers.roundTo2(result.testDuration).toFixed(2) + "s"; if (result.testDuration > 61) { time = DateTime.secondsToString(Numbers.roundTo2(result.testDuration)); } - $("#result .stats .time .bottom .text").text(time); - // $("#result .stats .acc .bottom").removeAttr("aria-label"); + qs("#result .stats .time .bottom .text")?.setText(time); + // qs("#result .stats .acc .bottom")?.removeAttribute("aria-label"); - $("#result .stats .acc .bottom").attr( + qs("#result .stats .acc .bottom")?.setAttribute( "aria-label", `${TestInput.accuracy.correct} correct\n${TestInput.accuracy.incorrect} incorrect`, ); @@ -385,11 +386,11 @@ function updateWpmAndAcc(): void { rawWpmHover += " (" + result.rawWpm.toFixed(2) + " wpm)"; } - $("#result .stats .wpm .bottom").attr("aria-label", wpmHover); - $("#result .stats .raw .bottom").attr("aria-label", rawWpmHover); + qs("#result .stats .wpm .bottom")?.setAttribute("aria-label", wpmHover); + qs("#result .stats .raw .bottom")?.setAttribute("aria-label", rawWpmHover); - $("#result .stats .acc .bottom") - .attr( + qs("#result .stats .acc .bottom") + ?.setAttribute( "aria-label", `${ result.acc === 100 @@ -399,16 +400,16 @@ function updateWpmAndAcc(): void { TestInput.accuracy.incorrect } incorrect`, ) - .attr("data-balloon-break", ""); + ?.setAttribute("data-balloon-break", ""); } } function updateConsistency(): void { - $("#result .stats .consistency .bottom").text( + qs("#result .stats .consistency .bottom")?.setText( Format.percentage(result.consistency), ); if (Config.alwaysShowDecimalPlaces) { - $("#result .stats .consistency .bottom").attr( + qs("#result .stats .consistency .bottom")?.setAttribute( "aria-label", Format.percentage(result.keyConsistency, { showDecimalPlaces: true, @@ -416,7 +417,7 @@ function updateConsistency(): void { }), ); } else { - $("#result .stats .consistency .bottom").attr( + qs("#result .stats .consistency .bottom")?.setAttribute( "aria-label", `${result.consistency}% (${result.keyConsistency}% key)`, ); @@ -427,11 +428,13 @@ function updateTime(): void { const afkSecondsPercent = Numbers.roundTo2( (result.afkDuration / result.testDuration) * 100, ); - $("#result .stats .time .bottom .afk").text(""); + qs("#result .stats .time .bottom .afk")?.setText(""); if (afkSecondsPercent > 0) { - $("#result .stats .time .bottom .afk").text(afkSecondsPercent + "% afk"); + qs("#result .stats .time .bottom .afk")?.setText( + afkSecondsPercent + "% afk", + ); } - $("#result .stats .time .bottom").attr( + qs("#result .stats .time .bottom")?.setAttribute( "aria-label", `${result.afkDuration}s afk ${afkSecondsPercent}%`, ); @@ -441,14 +444,14 @@ function updateTime(): void { if (result.testDuration > 61) { time = DateTime.secondsToString(Numbers.roundTo2(result.testDuration)); } - $("#result .stats .time .bottom .text").text(time); + qs("#result .stats .time .bottom .text")?.setText(time); } else { let time = Math.round(result.testDuration) + "s"; if (result.testDuration > 61) { time = DateTime.secondsToString(Math.round(result.testDuration)); } - $("#result .stats .time .bottom .text").text(time); - $("#result .stats .time .bottom").attr( + qs("#result .stats .time .bottom .text")?.setText(time); + qs("#result .stats .time .bottom")?.setAttribute( "aria-label", `${Numbers.roundTo2(result.testDuration)}s (${ result.afkDuration @@ -458,11 +461,13 @@ function updateTime(): void { } export function updateTodayTracker(): void { - $("#result .stats .time .bottom .timeToday").text(TodayTracker.getString()); + qs("#result .stats .time .bottom .timeToday")?.setText( + TodayTracker.getString(), + ); } function updateKey(): void { - $("#result .stats .key .bottom").text( + qs("#result .stats .key .bottom")?.setText( result.charStats[0] + "/" + result.charStats[1] + @@ -479,8 +484,8 @@ export function showCrown(type: PbCrown.CrownType): void { } export function updateCrownText(text: string, wide = false): void { - $("#result .stats .wpm .crown").attr("aria-label", text); - $("#result .stats .wpm .crown").attr( + qs("#result .stats .wpm .crown")?.setAttribute("aria-label", text); + qs("#result .stats .wpm .crown")?.setAttribute( "data-balloon-length", wide ? "medium" : "", ); @@ -662,21 +667,26 @@ async function updateTags(dontSave: boolean): Promise { } catch (e) {} if (userTagsCount === 0) { - $("#result .stats .tags").addClass("hidden"); + qs("#result .stats .tags")?.hide(); } else { - $("#result .stats .tags").removeClass("hidden"); + qs("#result .stats .tags")?.show(); } if (activeTags.length === 0) { - $("#result .stats .tags .bottom").html("
no tags
"); + qs("#result .stats .tags .bottom")?.setHtml( + "
no tags
", + ); } else { - $("#result .stats .tags .bottom").text(""); + qs("#result .stats .tags .bottom")?.setText(""); } - $("#result .stats .tags .editTagsButton").attr("data-result-id", ""); - $("#result .stats .tags .editTagsButton").attr( + qs("#result .stats .tags .editTagsButton")?.setAttribute( + "data-result-id", + "", + ); + qs("#result .stats .tags .editTagsButton")?.setAttribute( "data-active-tag-ids", activeTags.map((t) => t._id).join(","), ); - $("#result .stats .tags .editTagsButton").addClass("invisible"); + qs("#result .stats .tags .editTagsButton")?.addClass("invisible"); let annotationSide: LabelPosition = "start"; let labelAdjust = 15; @@ -691,7 +701,7 @@ async function updateTags(dontSave: boolean): Promise { Config.difficulty, Config.lazyMode, ); - $("#result .stats .tags .bottom").append(` + qs("#result .stats .tags .bottom")?.appendHtml(`
${tag.display}
`); const typingSpeedUnit = getTypingSpeedUnit(Config.typingSpeedUnit); @@ -716,13 +726,10 @@ async function updateTags(dontSave: boolean): Promise { result.rawWpm, result.consistency, ); - $( - `#result .stats .tags .bottom div[tagid="${tag._id}"] .fas`, - ).removeClass("hidden"); - $(`#result .stats .tags .bottom div[tagid="${tag._id}"]`).attr( - "aria-label", - "+" + Numbers.roundTo2(result.wpm - tpb), - ); + qs(`#result .stats .tags .bottom div[tagid="${tag._id}"] .fas`)?.show(); + qs( + `#result .stats .tags .bottom div[tagid="${tag._id}"]`, + )?.setAttribute("aria-label", "+" + Numbers.roundTo2(result.wpm - tpb)); // console.log("new pb for tag " + tag.display); } else { const themecolors = getTheme(); @@ -810,7 +817,7 @@ function updateTestType(randomQuote: Quote | null): void { testType += `
stop on ${Config.stopOnError}`; } - $("#result .stats .testType .bottom").html(testType); + qsa("#result .stats .testType .bottom")?.setHtml(testType); } function updateOther( @@ -864,11 +871,11 @@ function updateOther( } if (otherText === "") { - $("#result .stats .info").addClass("hidden"); + qs("#result .stats .info")?.hide(); } else { - $("#result .stats .info").removeClass("hidden"); + qs("#result .stats .info")?.show(); otherText = otherText.substring(4); - $("#result .stats .info .bottom").html(otherText); + qs("#result .stats .info .bottom")?.setHtml(otherText); } } @@ -884,32 +891,32 @@ export function updateRateQuote(randomQuote: Quote | null): void { const userqr = DB.getSnapshot()?.quoteRatings?.[randomQuote.language]?.[randomQuote.id]; if (Numbers.isSafeNumber(userqr)) { - $(".pageTest #result #rateQuoteButton .icon") - .removeClass("far") - .addClass("fas"); + qs(".pageTest #result #rateQuoteButton .icon") + ?.removeClass("far") + ?.addClass("fas"); } quoteRateModal .getQuoteStats(randomQuote) .then((quoteStats) => { - $(".pageTest #result #rateQuoteButton .rating").text( + qs(".pageTest #result #rateQuoteButton .rating")?.setText( quoteStats?.average?.toFixed(1) ?? "", ); }) .catch((_e: unknown) => { - $(".pageTest #result #rateQuoteButton .rating").text("?"); + qs(".pageTest #result #rateQuoteButton .rating")?.setText("?"); }); - $(".pageTest #result #rateQuoteButton") - .css({ opacity: 0 }) - .removeClass("hidden") - .css({ opacity: 1 }); + qs(".pageTest #result #rateQuoteButton") + ?.setStyle({ opacity: "0" }) + ?.show() + ?.setStyle({ opacity: "1" }); } } function updateQuoteFavorite(randomQuote: Quote | null): void { - const icon = $(".pageTest #result #favoriteQuoteButton .icon"); + const icon = qs(".pageTest #result #favoriteQuoteButton .icon"); if (Config.mode !== "quote" || !isAuthenticated()) { - icon.parent().addClass("hidden"); + icon?.getParent()?.hide(); return; } @@ -924,18 +931,18 @@ function updateQuoteFavorite(randomQuote: Quote | null): void { quoteId = Config.mode === "quote" ? randomQuote.id.toString() : ""; const userFav = QuotesController.isQuoteFavorite(randomQuote); - icon.removeClass(userFav ? "far" : "fas").addClass(userFav ? "fas" : "far"); - icon.parent().removeClass("hidden"); + icon?.removeClass(userFav ? "far" : "fas")?.addClass(userFav ? "fas" : "far"); + icon?.getParent()?.show(); } function updateQuoteSource(randomQuote: Quote | null): void { if (Config.mode === "quote") { - $("#result .stats .source").removeClass("hidden"); - $("#result .stats .source .bottom").html( + qs("#result .stats .source")?.show(); + qs("#result .stats .source .bottom")?.setHtml( randomQuote?.source ?? "Error: Source unknown", ); } else { - $("#result .stats .source").addClass("hidden"); + qs("#result .stats .source")?.hide(); } } @@ -952,29 +959,29 @@ export async function update( resultAnnotation = []; result = structuredClone(res); hideCrown(); - $("#resultWordsHistory .words").empty(); - $("#result #resultWordsHistory").addClass("hidden"); - $("#result #replayStats").text(""); - $("#result #resultReplay").addClass("hidden"); - $("#result #replayWords").empty(); - $("#retrySavingResultButton").addClass("hidden"); - $(".pageTest #result #rateQuoteButton .icon") - .removeClass("fas") - .addClass("far"); - $(".pageTest #result #rateQuoteButton .rating").text(""); - $(".pageTest #result #rateQuoteButton").addClass("hidden"); - $("#words").removeClass("blurred"); + qs("#resultWordsHistory .words")?.empty(); + qs("#result #resultWordsHistory")?.hide(); + qs("#result #replayStats")?.setText(""); + qs("#result #resultReplay")?.hide(); + qs("#result #replayWords")?.empty(); + qs("#retrySavingResultButton")?.hide(); + qs(".pageTest #result #rateQuoteButton .icon") + ?.removeClass("fas") + ?.addClass("far"); + qs(".pageTest #result #rateQuoteButton .rating")?.setText(""); + qs(".pageTest #result #rateQuoteButton")?.hide(); + qs("#words")?.removeClass("blurred"); blurInputElement(); - $("#result .stats .time .bottom .afk").text(""); + qs("#result .stats .time .bottom .afk")?.setText(""); if (isAuthenticated()) { - $("#result .loginTip").addClass("hidden"); + qs("#result .loginTip")?.hide(); } else { - $("#result .loginTip").removeClass("hidden"); + qs("#result .loginTip")?.show(); } if (Config.ads === "off" || Config.ads === "result") { - $("#result #watchVideoAdButton").addClass("hidden"); + qs("#result #watchVideoAdButton")?.hide(); } else { - $("#result #watchVideoAdButton").removeClass("hidden"); + qs("#result #watchVideoAdButton")?.show(); } if (!ConnectionState.get()) { @@ -1003,17 +1010,17 @@ export async function update( ChartController.result.resize(); if ( - $("#result .stats .tags").hasClass("hidden") && - $("#result .stats .info").hasClass("hidden") + qs("#result .stats .tags")?.hasClass("hidden") && + qs("#result .stats .info")?.hasClass("hidden") ) { - $("#result .stats .infoAndTags").addClass("hidden"); + qs("#result .stats .infoAndTags")?.hide(); } else { - $("#result .stats .infoAndTags").removeClass("hidden"); + qs("#result .stats .infoAndTags")?.show(); } if (GlarsesMode.get()) { - $("main #result .noStressMessage").remove(); - $("main #result").prepend(` + qs("main #result .noStressMessage")?.remove(); + qs("main #result")?.prependHtml(`
= 5) { @@ -1095,10 +1102,10 @@ export async function update( AdController.updateFooterAndVerticalAds(true); void Funbox.clear(); - $(".pageTest .loading").addClass("hidden"); - $("#result").removeClass("hidden"); + qs(".pageTest .loading")?.hide(); + qs("#result")?.show(); - const resultEl = document.querySelector("#result"); + const resultEl = qs("#result"); resultEl?.focus({ preventScroll: true, }); @@ -1108,10 +1115,10 @@ export async function update( duration: Misc.applyReducedMotion(125), }); - Misc.scrollToCenterOrTop(resultEl); + Misc.scrollToCenterOrTop(resultEl?.native ?? null); void AdController.renderResult(); TestUI.setResultCalculating(false); - $("#words").empty(); + qs("#words")?.empty(); ChartController.result.resize(); } @@ -1215,7 +1222,7 @@ function updateResultChartDataVisibility(): void { } } - const buttons = $(".pageTest #result .chart .chartLegend button"); + const buttons = qsa(".pageTest #result .chart .chartLegend button"); // Check if there are any tag PB annotations const hasTagPbAnnotations = resultAnnotation.some( @@ -1223,7 +1230,7 @@ function updateResultChartDataVisibility(): void { ); for (const button of buttons) { - const id = $(button).data("id") as string; + const id = button?.getAttribute("data-id") as string; if (id === "scale") { continue; @@ -1239,15 +1246,12 @@ function updateResultChartDataVisibility(): void { continue; } - $(button).toggleClass("active", vis[id]); + button.toggleClass("active", vis[id]); if (id === "pbLine") { - $(button).toggleClass("hidden", !isAuthenticated()); + button.toggleClass("hidden", !isAuthenticated()); } else if (id === "tagPbLine") { - $(button).toggleClass( - "hidden", - !isAuthenticated() || !hasTagPbAnnotations, - ); + button.toggleClass("hidden", !isAuthenticated() || !hasTagPbAnnotations); } } } @@ -1269,18 +1273,18 @@ export function updateTagsAfterEdit( } if (tagIds.length === 0) { - $(`.pageTest #result .tags .bottom`).html( + qs(`.pageTest #result .tags .bottom`)?.setHtml( "
no tags
", ); } else { - $(`.pageTest #result .tags .bottom div.noTags`).remove(); - const currentElements = $(`.pageTest #result .tags .bottom div[tagid]`); + qs(`.pageTest #result .tags .bottom div.noTags`)?.remove(); + const currentElements = qsa(`.pageTest #result .tags .bottom div[tagid]`); const checked: string[] = []; - currentElements.each((_, element) => { - const tagId = $(element).attr("tagid") as string; + currentElements.forEach((element) => { + const tagId = element.getAttribute("tagid") ?? ""; if (!tagIds.includes(tagId)) { - $(element).remove(); + element?.remove(); } else { checked.push(tagId); } @@ -1297,55 +1301,58 @@ export function updateTagsAfterEdit( } }); - // $(`.pageTest #result .tags .bottom`).html(tagNames.join("
")); - $(`.pageTest #result .tags .bottom`).append(html); + // qs(`.pageTest #result .tags .bottom`)?.setHtml(tagNames.join("
")); + qs(`.pageTest #result .tags .bottom`)?.appendHtml(html); } - $(`.pageTest #result .tags .top .editTagsButton`).attr( + qs(`.pageTest #result .tags .top .editTagsButton`)?.setAttribute( "data-active-tag-ids", tagIds.join(","), ); } -$(".pageTest #result .chart .chartLegend button").on("click", async (event) => { - const $target = $(event.target); - const id = $target.data("id") as string; +qsa(".pageTest #result .chart .chartLegend button")?.on( + "click", + async (event) => { + const $target = event.target as HTMLElement; + const id = $target.getAttribute("data-id"); - if (id === "scale") { - setConfig("startGraphsAtZero", !Config.startGraphsAtZero); - return; - } + if (id === "scale") { + setConfig("startGraphsAtZero", !Config.startGraphsAtZero); + return; + } - if ( - id !== "raw" && - id !== "burst" && - id !== "errors" && - id !== "pbLine" && - id !== "tagPbLine" - ) { - return; - } - const vis = resultChartDataVisibility.get(); - vis[id] = !vis[id]; - resultChartDataVisibility.set(vis); + if ( + id !== "raw" && + id !== "burst" && + id !== "errors" && + id !== "pbLine" && + id !== "tagPbLine" + ) { + return; + } + const vis = resultChartDataVisibility.get(); + vis[id] = !vis[id]; + resultChartDataVisibility.set(vis); - updateResultChartDataVisibility(); - updateMinMaxChartValues(); - applyMinMaxChartValues(); - ChartController.result.update(); -}); + updateResultChartDataVisibility(); + updateMinMaxChartValues(); + applyMinMaxChartValues(); + ChartController.result.update(); + }, +); -$(".pageTest #favoriteQuoteButton").on("click", async () => { +qs(".pageTest #favoriteQuoteButton")?.on("click", async () => { if (quoteLang === undefined || quoteId === "") { Notifications.add("Could not get quote stats!", -1); return; } - const $button = $(".pageTest #favoriteQuoteButton .icon"); + const $button = qs(".pageTest #favoriteQuoteButton .icon"); const dbSnapshot = DB.getSnapshot(); if (!dbSnapshot) return; - if ($button.hasClass("fas")) { + if ($button?.hasClass("fas")) { // Remove from showLoaderBar(); const response = await Ape.users.removeQuoteFromFavorites({ @@ -1359,7 +1366,7 @@ $(".pageTest #favoriteQuoteButton").on("click", async () => { Notifications.add(response.body.message, response.status === 200 ? 1 : -1); if (response.status === 200) { - $button.removeClass("fas").addClass("far"); + $button?.removeClass("fas")?.addClass("far"); const quoteIndex = dbSnapshot.favoriteQuotes?.[quoteLang]?.indexOf( quoteId, ) as number; @@ -1376,7 +1383,7 @@ $(".pageTest #favoriteQuoteButton").on("click", async () => { Notifications.add(response.body.message, response.status === 200 ? 1 : -1); if (response.status === 200) { - $button.removeClass("far").addClass("fas"); + $button?.removeClass("far")?.addClass("fas"); dbSnapshot.favoriteQuotes ??= {}; dbSnapshot.favoriteQuotes[quoteLang] ??= []; dbSnapshot.favoriteQuotes[quoteLang]?.push(quoteId); diff --git a/frontend/src/ts/test/test-config.ts b/frontend/src/ts/test/test-config.ts index 64d82659b815..d15bbf5091e9 100644 --- a/frontend/src/ts/test/test-config.ts +++ b/frontend/src/ts/test/test-config.ts @@ -3,68 +3,68 @@ import { Mode } from "@monkeytype/schemas/shared"; import Config from "../config"; import * as ConfigEvent from "../observables/config-event"; import { getActivePage } from "../signals/core"; -import { applyReducedMotion, promiseAnimate } from "../utils/misc"; +import { applyReducedMotion } from "../utils/misc"; import { areUnsortedArraysEqual } from "../utils/arrays"; import * as AuthEvent from "../observables/auth-event"; -import { animate } from "animejs"; +import { qs, qsa } from "../utils/dom"; export function show(): void { - $("#testConfig").removeClass("invisible"); - $("#mobileTestConfigButton").removeClass("invisible"); + qs("#testConfig")?.removeClass("invisible"); + qs("#mobileTestConfigButton")?.removeClass("invisible"); } export function hide(): void { - $("#testConfig").addClass("invisible"); - $("#mobileTestConfigButton").addClass("invisible"); + qs("#testConfig")?.addClass("invisible"); + qs("#mobileTestConfigButton")?.addClass("invisible"); } export async function instantUpdate(): Promise { - $("#testConfig .mode .textButton").removeClass("active"); - $("#testConfig .mode .textButton[mode='" + Config.mode + "']").addClass( + qsa("#testConfig .mode .textButton")?.removeClass("active"); + qs("#testConfig .mode .textButton[mode='" + Config.mode + "']")?.addClass( "active", ); - $("#testConfig .puncAndNum").addClass("hidden"); - $("#testConfig .spacer").addClass("hidden"); - $("#testConfig .time").addClass("hidden"); - $("#testConfig .wordCount").addClass("hidden"); - $("#testConfig .customText").addClass("hidden"); - $("#testConfig .quoteLength").addClass("hidden"); - $("#testConfig .zen").addClass("hidden"); + qs("#testConfig .puncAndNum")?.hide(); + qsa("#testConfig .spacer")?.hide(); + qs("#testConfig .time")?.hide(); + qs("#testConfig .wordCount")?.hide(); + qs("#testConfig .customText")?.hide(); + qs("#testConfig .quoteLength")?.hide(); + qs("#testConfig .zen")?.hide(); if (Config.mode === "time") { - $("#testConfig .puncAndNum").removeClass("hidden").css({ + qs("#testConfig .puncAndNum")?.show()?.setStyle({ width: "", opacity: "", }); - $("#testConfig .leftSpacer").removeClass("hidden"); - $("#testConfig .rightSpacer").removeClass("hidden"); - $("#testConfig .time").removeClass("hidden"); + qs("#testConfig .leftSpacer")?.show(); + qs("#testConfig .rightSpacer")?.show(); + qs("#testConfig .time")?.show(); updateActiveExtraButtons("time", Config.time); } else if (Config.mode === "words") { - $("#testConfig .puncAndNum").removeClass("hidden").css({ + qs("#testConfig .puncAndNum")?.show()?.setStyle({ width: "", opacity: "", }); - $("#testConfig .leftSpacer").removeClass("hidden"); - $("#testConfig .rightSpacer").removeClass("hidden"); - $("#testConfig .wordCount").removeClass("hidden"); + qs("#testConfig .leftSpacer")?.show(); + qs("#testConfig .rightSpacer")?.show(); + qs("#testConfig .wordCount")?.show(); updateActiveExtraButtons("words", Config.words); } else if (Config.mode === "quote") { - $("#testConfig .rightSpacer").removeClass("hidden"); - $("#testConfig .quoteLength").removeClass("hidden"); + qs("#testConfig .rightSpacer")?.show(); + qs("#testConfig .quoteLength")?.show(); updateActiveExtraButtons("quoteLength", Config.quoteLength); } else if (Config.mode === "custom") { - $("#testConfig .puncAndNum").removeClass("hidden").css({ + qs("#testConfig .puncAndNum")?.show()?.setStyle({ width: "", opacity: "", }); - $("#testConfig .leftSpacer").removeClass("hidden"); - $("#testConfig .rightSpacer").removeClass("hidden"); - $("#testConfig .customText").removeClass("hidden"); + qs("#testConfig .leftSpacer")?.show(); + qs("#testConfig .rightSpacer")?.show(); + qs("#testConfig .customText")?.show(); } updateActiveExtraButtons("quoteLength", Config.quoteLength); @@ -113,21 +113,21 @@ async function update(previous: Mode, current: Mode): Promise { zen: false, }; - const puncAndNumEl = $("#testConfig .puncAndNum"); + const puncAndNumEl = qs("#testConfig .puncAndNum"); if (puncAndNumVisible[current] !== puncAndNumVisible[previous]) { puncAndNumEl - .css({ + ?.setStyle({ width: "unset", - opacity: 1, + opacity: "1", }) - .removeClass("hidden"); + ?.show(); const width = Math.round( - puncAndNumEl[0]?.getBoundingClientRect().width ?? 0, + puncAndNumEl?.native.getBoundingClientRect().width ?? 0, ); - animate(puncAndNumEl[0] as HTMLElement, { + puncAndNumEl?.animate({ width: [ (puncAndNumVisible[previous] ? width : 0) + "px", (puncAndNumVisible[current] ? width : 0) + "px", @@ -142,22 +142,20 @@ async function update(previous: Mode, current: Mode): Promise { ease: easing.both, onComplete: () => { if (puncAndNumVisible[current]) { - puncAndNumEl.css("width", "unset"); + puncAndNumEl?.setStyle({ width: "unset" }); } else { - puncAndNumEl.addClass("hidden"); + puncAndNumEl?.hide(); } }, }); - const leftSpacerEl = document.querySelector( - "#testConfig .leftSpacer", - ) as HTMLElement; + const leftSpacerEl = qs("#testConfig .leftSpacer"); - leftSpacerEl.style.width = "0.5em"; - leftSpacerEl.style.opacity = "1"; - leftSpacerEl.classList.remove("hidden"); + leftSpacerEl?.setStyle({ width: "0.5em" }); + leftSpacerEl?.setStyle({ opacity: "1" }); + leftSpacerEl?.show(); - animate(leftSpacerEl, { + leftSpacerEl?.animate({ width: [ puncAndNumVisible[previous] ? "0.5em" : 0, puncAndNumVisible[current] ? "0.5em" : 0, @@ -172,23 +170,21 @@ async function update(previous: Mode, current: Mode): Promise { ease: easing.both, onComplete: () => { if (puncAndNumVisible[current]) { - leftSpacerEl.style.width = ""; + leftSpacerEl?.setStyle({ width: "" }); } else { - leftSpacerEl.classList.add("hidden"); + leftSpacerEl?.hide(); } }, }); } - const rightSpacerEl = document.querySelector( - "#testConfig .rightSpacer", - ) as HTMLElement; + const rightSpacerEl = qs("#testConfig .rightSpacer"); - rightSpacerEl.style.width = "0.5em"; - rightSpacerEl.style.opacity = "1"; - rightSpacerEl.classList.remove("hidden"); + rightSpacerEl?.setStyle({ width: "0.5em" }); + rightSpacerEl?.setStyle({ opacity: "1" }); + rightSpacerEl?.show(); - animate(rightSpacerEl, { + rightSpacerEl?.animate({ width: [ previous === "zen" ? "0px" : "0.5em", current === "zen" ? "0px" : "0.5em", @@ -202,34 +198,34 @@ async function update(previous: Mode, current: Mode): Promise { ease: easing.both, onComplete: () => { if (current === "zen") { - rightSpacerEl.classList.add("hidden"); + rightSpacerEl?.hide(); } else { - rightSpacerEl.style.width = ""; + rightSpacerEl?.setStyle({ width: "" }); } }, }); - const currentEl = $(`#testConfig .${submenu[current]}`); - const previousEl = $(`#testConfig .${submenu[previous]}`); + const currentEl = qs(`#testConfig .${submenu[current]}`); + const previousEl = qs(`#testConfig .${submenu[previous]}`); const previousWidth = Math.round( - previousEl[0]?.getBoundingClientRect().width ?? 0, + previousEl?.native.getBoundingClientRect().width ?? 0, ); - previousEl.addClass("hidden"); - currentEl.removeClass("hidden"); + previousEl?.hide(); + currentEl?.show(); const currentWidth = Math.round( - currentEl[0]?.getBoundingClientRect().width ?? 0, + currentEl?.native.getBoundingClientRect().width ?? 0, ); - previousEl.removeClass("hidden"); - currentEl.addClass("hidden"); + previousEl?.show(); + currentEl?.hide(); const widthDifference = currentWidth - previousWidth; const widthStep = widthDifference / 2; - await promiseAnimate(previousEl[0] as HTMLElement, { + await previousEl?.promiseAnimate({ opacity: [1, 0], width: [previousWidth + "px", previousWidth + widthStep + "px"], duration: animTime / 2, @@ -237,19 +233,19 @@ async function update(previous: Mode, current: Mode): Promise { }); previousEl - .css({ - opacity: 1, + ?.setStyle({ + opacity: "1", width: "unset", }) - .addClass("hidden"); + ?.hide(); currentEl - .css({ - opacity: 0, + ?.setStyle({ + opacity: "0", width: previousWidth + widthStep + "px", }) - .removeClass("hidden"); + ?.show(); - await promiseAnimate(currentEl[0] as HTMLElement, { + await currentEl?.promiseAnimate({ opacity: [0, 1], width: [previousWidth + widthStep + "px", currentWidth + "px"], duration: animTime / 2, @@ -258,64 +254,64 @@ async function update(previous: Mode, current: Mode): Promise { } function updateActiveModeButtons(mode: Mode): void { - $("#testConfig .mode .textButton").removeClass("active"); - $("#testConfig .mode .textButton[mode='" + mode + "']").addClass("active"); + qsa("#testConfig .mode .textButton")?.removeClass("active"); + qs("#testConfig .mode .textButton[mode='" + mode + "']")?.addClass("active"); } function updateActiveExtraButtons(key: string, value: ConfigValue): void { if (key === "time") { - $("#testConfig .time .textButton").removeClass("active"); + qsa("#testConfig .time .textButton")?.removeClass("active"); const timeCustom = ![15, 30, 60, 120].includes(value as number) ? "custom" : (value as number); - $( + qs( "#testConfig .time .textButton[timeConfig='" + timeCustom + "']", - ).addClass("active"); + )?.addClass("active"); } else if (key === "words") { - $("#testConfig .wordCount .textButton").removeClass("active"); + qsa("#testConfig .wordCount .textButton")?.removeClass("active"); const wordCustom = ![10, 25, 50, 100, 200].includes(value as number) ? "custom" : (value as number); - $( + qs( "#testConfig .wordCount .textButton[wordCount='" + wordCustom + "']", - ).addClass("active"); + )?.addClass("active"); } else if (key === "quoteLength") { - $("#testConfig .quoteLength .textButton").removeClass("active"); + qsa("#testConfig .quoteLength .textButton")?.removeClass("active"); if (areUnsortedArraysEqual(value as QuoteLength[], [0, 1, 2, 3])) { - $("#testConfig .quoteLength .textButton[quotelength='all']").addClass( + qs("#testConfig .quoteLength .textButton[quotelength='all']")?.addClass( "active", ); } else { (value as QuoteLength[]).forEach((ql) => { - $( + qs( "#testConfig .quoteLength .textButton[quoteLength='" + ql + "']", - ).addClass("active"); + )?.addClass("active"); }); } } else if (key === "numbers") { if (value === false) { - $("#testConfig .numbersMode.textButton").removeClass("active"); + qs("#testConfig .numbersMode.textButton")?.removeClass("active"); } else { - $("#testConfig .numbersMode.textButton").addClass("active"); + qs("#testConfig .numbersMode.textButton")?.addClass("active"); } } else if (key === "punctuation") { if (value === false) { - $("#testConfig .punctuationMode.textButton").removeClass("active"); + qs("#testConfig .punctuationMode.textButton")?.removeClass("active"); } else { - $("#testConfig .punctuationMode.textButton").addClass("active"); + qs("#testConfig .punctuationMode.textButton")?.addClass("active"); } } } export function showFavoriteQuoteLength(): void { - $("#testConfig .quoteLength .favorite").removeClass("hidden"); + qs("#testConfig .quoteLength .favorite")?.show(); } export function hideFavoriteQuoteLength(): void { - $("#testConfig .quoteLength .favorite").addClass("hidden"); + qs("#testConfig .quoteLength .favorite")?.hide(); } let ignoreConfigEvent = false; diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index d8c5be184470..0b316305bd3a 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -68,6 +68,7 @@ import { animate } from "animejs"; import { setInputElementValue } from "../input/input-element"; import { debounce } from "throttle-debounce"; import * as Time from "../states/time"; +import { qs } from "../utils/dom"; let failReason = ""; @@ -125,7 +126,7 @@ export function startTest(now: number): boolean { type RestartOptions = { withSameWordset?: boolean; nosave?: boolean; - event?: JQuery.KeyDownEvent; + event?: KeyboardEvent; practiseMissed?: boolean; noAnim?: boolean; }; @@ -707,7 +708,7 @@ export async function retrySavingResult(): Promise { } retrySaving.canRetry = false; - $("#retrySavingResultButton").addClass("hidden"); + qs("#retrySavingResultButton")?.hide(); Notifications.add("Retrying to save..."); @@ -866,8 +867,8 @@ export async function finish(difficultyFailed = false): Promise { opacity: 0, duration: Misc.applyReducedMotion(125), }); - $(".pageTest #typingTest").addClass("hidden"); - $(".pageTest .loading").removeClass("hidden"); + qs(".pageTest #typingTest")?.hide(); + qs(".pageTest .loading")?.show(); await Misc.sleep(0); //allow ui update TestUI.onTestFinish(); @@ -1215,7 +1216,7 @@ async function saveResult( }); AccountButton.loading(false); retrySaving.canRetry = true; - $("#retrySavingResultButton").removeClass("hidden"); + qs("#retrySavingResultButton")?.show(); if (!isRetrying) { retrySaving.completedEvent = completedEvent; } @@ -1243,7 +1244,7 @@ async function saveResult( //only allow retry if status is not in this list if (![460, 461, 463, 464, 465, 466].includes(response.status)) { retrySaving.canRetry = true; - $("#retrySavingResultButton").removeClass("hidden"); + qs("#retrySavingResultButton")?.show(); if (!isRetrying) { retrySaving.completedEvent = result; } @@ -1264,11 +1265,11 @@ async function saveResult( } const data = response.body.data; - $("#result .stats .tags .editTagsButton").attr( + qs("#result .stats .tags .editTagsButton")?.setAttribute( "data-result-id", data.insertedId, ); - $("#result .stats .tags .editTagsButton").removeClass("invisible"); + qs("#result .stats .tags .editTagsButton")?.removeClass("invisible"); const localDataToSave: DB.SaveLocalResultData = {}; @@ -1339,12 +1340,12 @@ async function saveResult( duration: Misc.applyReducedMotion(250), }); - $("#result .stats .dailyLeaderboard .bottom").html( + qs("#result .stats .dailyLeaderboard .bottom")?.setHtml( Format.rank(data.dailyLeaderboardRank, { fallback: "" }), ); } - $("#retrySavingResultButton").addClass("hidden"); + qs("#retrySavingResultButton")?.hide(); if (isRetrying) { Notifications.add("Result saved", 1, { important: true }); } @@ -1397,15 +1398,19 @@ const debouncedZipfCheck = debounce(250, async () => { } }); -$(".pageTest").on("click", "#testModesNotice .textButton.restart", () => { - restart(); -}); +qs(".pageTest")?.onChild( + "click", + "#testModesNotice .textButton.restart", + () => { + restart(); + }, +); -$(".pageTest").on("click", "#testInitFailed button.restart", () => { +qs(".pageTest")?.onChild("click", "#testInitFailed button.restart", () => { restart(); }); -$(".pageTest").on("click", "#restartTestButton", () => { +qs(".pageTest")?.onChild("click", "#restartTestButton", () => { ManualRestart.set(); if (TestUI.resultCalculating) return; if ( @@ -1421,14 +1426,18 @@ $(".pageTest").on("click", "#restartTestButton", () => { } }); -$(".pageTest").on("click", "#retrySavingResultButton", retrySavingResult); +qs(".pageTest")?.onChild( + "click", + "#retrySavingResultButton", + retrySavingResult, +); -$(".pageTest").on("click", "#nextTestButton", () => { +qs(".pageTest")?.onChild("click", "#nextTestButton", () => { ManualRestart.set(); restart(); }); -$(".pageTest").on("click", "#restartTestButtonWithSameWordset", () => { +qs(".pageTest")?.onChild("click", "#restartTestButtonWithSameWordset", () => { if (Config.mode === "zen") { Notifications.add("Repeat test disabled in zen mode"); return; @@ -1439,10 +1448,11 @@ $(".pageTest").on("click", "#restartTestButtonWithSameWordset", () => { }); }); -$(".pageTest").on("click", "#testConfig .mode .textButton", (e) => { +qs(".pageTest")?.onChild("click", "#testConfig .mode .textButton", (e) => { if (TestState.testRestarting) return; - if ($(e.currentTarget).hasClass("active")) return; - const mode = ($(e.currentTarget).attr("mode") ?? "time") as Mode; + if ((e.childTarget as HTMLElement).classList.contains("active")) return; + const mode = ((e.childTarget as HTMLElement)?.getAttribute("mode") ?? + "time") as Mode; if (mode === undefined) return; if (setConfig("mode", mode)) { ManualRestart.set(); @@ -1450,9 +1460,9 @@ $(".pageTest").on("click", "#testConfig .mode .textButton", (e) => { } }); -$(".pageTest").on("click", "#testConfig .wordCount .textButton", (e) => { +qs(".pageTest")?.onChild("click", "#testConfig .wordCount .textButton", (e) => { if (TestState.testRestarting) return; - const wrd = $(e.currentTarget).attr("wordCount") ?? "15"; + const wrd = (e.childTarget as HTMLElement)?.getAttribute("wordCount") ?? "15"; if (wrd !== "custom") { if (setConfig("words", parseInt(wrd))) { ManualRestart.set(); @@ -1461,9 +1471,10 @@ $(".pageTest").on("click", "#testConfig .wordCount .textButton", (e) => { } }); -$(".pageTest").on("click", "#testConfig .time .textButton", (e) => { +qs(".pageTest")?.onChild("click", "#testConfig .time .textButton", (e) => { if (TestState.testRestarting) return; - const mode = $(e.currentTarget).attr("timeConfig") ?? "10"; + const mode = + (e.childTarget as HTMLElement)?.getAttribute("timeConfig") ?? "10"; if (mode !== "custom") { if (setConfig("time", parseInt(mode))) { ManualRestart.set(); @@ -1472,43 +1483,51 @@ $(".pageTest").on("click", "#testConfig .time .textButton", (e) => { } }); -$(".pageTest").on("click", "#testConfig .quoteLength .textButton", (e) => { - if (TestState.testRestarting) return; - const lenAttr = $(e.currentTarget).attr("quoteLength"); - if (lenAttr === "all") { - if (setQuoteLengthAll()) { - ManualRestart.set(); - restart(); - } - } else { - const len = parseInt(lenAttr ?? "1") as QuoteLength; +qs(".pageTest")?.onChild( + "click", + "#testConfig .quoteLength .textButton", + (e) => { + if (TestState.testRestarting) return; + const lenAttr = (e.childTarget as HTMLElement)?.getAttribute("quoteLength"); + if (lenAttr === "all") { + if (setQuoteLengthAll()) { + ManualRestart.set(); + restart(); + } + } else { + const len = parseInt(lenAttr ?? "1") as QuoteLength; - if (len !== -2) { - let arr: QuoteLengthConfig = []; + if (len !== -2) { + let arr: QuoteLengthConfig = []; - if (e.shiftKey) { - arr = [...Config.quoteLength, len]; - } else { - arr = [len]; - } + if (e.shiftKey) { + arr = [...Config.quoteLength, len]; + } else { + arr = [len]; + } - if (setConfig("quoteLength", arr)) { - ManualRestart.set(); - restart(); + if (setConfig("quoteLength", arr)) { + ManualRestart.set(); + restart(); + } } } - } -}); - -$(".pageTest").on("click", "#testConfig .punctuationMode.textButton", () => { - if (TestState.testRestarting) return; - if (setConfig("punctuation", !Config.punctuation)) { - ManualRestart.set(); - restart(); - } -}); + }, +); + +qs(".pageTest")?.onChild( + "click", + "#testConfig .punctuationMode.textButton", + () => { + if (TestState.testRestarting) return; + if (setConfig("punctuation", !Config.punctuation)) { + ManualRestart.set(); + restart(); + } + }, +); -$(".pageTest").on("click", "#testConfig .numbersMode.textButton", () => { +qs(".pageTest")?.onChild("click", "#testConfig .numbersMode.textButton", () => { if (TestState.testRestarting) return; if (setConfig("numbers", !Config.numbers)) { ManualRestart.set(); @@ -1516,7 +1535,7 @@ $(".pageTest").on("click", "#testConfig .numbersMode.textButton", () => { } }); -$("header").on("click", "nav #startTestButton, #logo", () => { +qs("header")?.onChild("click", "nav #startTestButton, #logo", () => { if (getActivePage() === "test") restart(); // Result.showConfetti(); }); diff --git a/frontend/src/ts/test/test-screenshot.ts b/frontend/src/ts/test/test-screenshot.ts index 4c660c1ea6c0..87cf8085258f 100644 --- a/frontend/src/ts/test/test-screenshot.ts +++ b/frontend/src/ts/test/test-screenshot.ts @@ -10,6 +10,7 @@ import { getHtmlByUserFlags } from "../controllers/user-flag-controller"; import * as Notifications from "../elements/notifications"; import { convertRemToPixels } from "../utils/numbers"; import * as TestState from "./test-state"; +import { qs, qsa } from "../utils/dom"; import { getTheme } from "../signals/theme"; let revealReplay = false; @@ -17,28 +18,27 @@ let revertCookie = false; function revert(): void { hideLoaderBar(); - $("#ad-result-wrapper").removeClass("hidden"); - $("#ad-result-small-wrapper").removeClass("hidden"); - $("#testConfig").removeClass("hidden"); - $(".pageTest .screenshotSpacer").remove(); - $("#notificationCenter").removeClass("hidden"); - $("#commandLineMobileButton").removeClass("hidden"); - $(".pageTest .ssWatermark").addClass("hidden"); - $(".pageTest .ssWatermark").text("monkeytype.com"); // Reset watermark text - $(".pageTest .buttons").removeClass("hidden"); - $("noscript").removeClass("hidden"); - $("#nocss").removeClass("hidden"); - $("header, footer").removeClass("invisible"); - $("#result").removeClass("noBalloons"); - $(".wordInputHighlight").removeClass("hidden"); - $(".highlightContainer").removeClass("hidden"); - if (revertCookie) $("#cookiesModal").removeClass("hidden"); - if (revealReplay) $("#resultReplay").removeClass("hidden"); + qs("#ad-result-wrapper")?.show(); + qs("#ad-result-small-wrapper")?.show(); + qs("#testConfig")?.show(); + qs(".pageTest .screenshotSpacer")?.remove(); + qs("#notificationCenter")?.show(); + qs("#commandLineMobileButton")?.show(); + qs(".pageTest .ssWatermark")?.hide(); + qs(".pageTest .ssWatermark")?.setText("monkeytype.com"); // Reset watermark text + qs(".pageTest .buttons")?.show(); + qs("noscript")?.show(); + qs("#nocss")?.show(); + qsa("header, footer")?.show(); + qs("#result")?.removeClass("noBalloons"); + qs(".wordInputHighlight")?.show(); + qs(".highlightContainer")?.show(); + if (revertCookie) qs("#cookiesModal")?.show(); + if (revealReplay) qs("#resultReplay")?.show(); if (!isAuthenticated()) { - $(".pageTest .loginTip").removeClass("hidden"); + qs(".pageTest .loginTip")?.removeClass("hidden"); } - (document.querySelector("html") as HTMLElement).style.scrollBehavior = - "smooth"; + qs("html")?.setStyle({ scrollBehavior: "smooth" }); for (const fb of getActiveFunboxesWithFunction("applyGlobalCSS")) { fb.functions.applyGlobalCSS(); } @@ -55,7 +55,7 @@ async function generateCanvas(): Promise { const { domToCanvas } = await import("modern-screenshot"); showLoaderBar(true); - if (!$("#resultReplay").hasClass("hidden")) { + if (!qs("#resultReplay")?.hasClass("hidden")) { revealReplay = true; Replay.pauseReplay(); } @@ -68,8 +68,8 @@ async function generateCanvas(): Promise { // --- UI Preparation --- const dateNow = new Date(Date.now()); - $("#resultReplay").addClass("hidden"); - $(".pageTest .ssWatermark").removeClass("hidden"); + qs("#resultReplay")?.hide(); + qs(".pageTest .ssWatermark")?.show(); const snapshot = DB.getSnapshot(); const ssWatermark = [format(dateNow, "dd MMM yyyy HH:mm"), "monkeytype.com"]; @@ -79,28 +79,28 @@ async function generateCanvas(): Promise { })}`; ssWatermark.unshift(userText); } - $(".pageTest .ssWatermark").html( + qs(".pageTest .ssWatermark")?.setHtml( ssWatermark .map((el) => `${el}`) .join("|"), ); - $(".pageTest .buttons").addClass("hidden"); - $("#notificationCenter").addClass("hidden"); - $("#commandLineMobileButton").addClass("hidden"); - $(".pageTest .loginTip").addClass("hidden"); - $("noscript").addClass("hidden"); - $("#nocss").addClass("hidden"); - $("#ad-result-wrapper").addClass("hidden"); - $("#ad-result-small-wrapper").addClass("hidden"); - $("#testConfig").addClass("hidden"); + qs(".pageTest .buttons")?.hide(); + qs("#notificationCenter")?.hide(); + qs("#commandLineMobileButton")?.hide(); + qs(".pageTest .loginTip")?.hide(); + qs("noscript")?.hide(); + qs("#nocss")?.hide(); + qs("#ad-result-wrapper")?.hide(); + qs("#ad-result-small-wrapper")?.hide(); + qs("#testConfig")?.hide(); // Ensure spacer is removed before adding a new one if function is called rapidly - $(".pageTest .screenshotSpacer").remove(); - $(".page.pageTest").prepend("
"); - $("header, footer").addClass("invisible"); - $("#result").addClass("noBalloons"); - $(".wordInputHighlight").addClass("hidden"); - $(".highlightContainer").addClass("hidden"); - if (revertCookie) $("#cookiesModal").addClass("hidden"); + qs(".pageTest .screenshotSpacer")?.remove(); + qs(".page.pageTest")?.prependHtml("
"); + qsa("header, footer")?.addClass("invisible"); + qs("#result")?.addClass("noBalloons"); + qs(".wordInputHighlight")?.hide(); + qs(".highlightContainer")?.hide(); + if (revertCookie) qs("#cookiesModal")?.hide(); for (const fb of getActiveFunboxesWithFunction("clearGlobal")) { fb.functions.clearGlobal(); @@ -110,8 +110,8 @@ async function generateCanvas(): Promise { window.scrollTo({ top: 0, behavior: "auto" }); // --- Target Element Calculation --- - const src = $("#result .wrapper"); - if (!src.length) { + const src = qs("#result .wrapper"); + if (src === null) { console.error("Result wrapper not found for screenshot"); Notifications.add("Screenshot target element not found", -1); revert(); @@ -119,10 +119,10 @@ async function generateCanvas(): Promise { } await Misc.sleep(50); // Small delay for render updates - const sourceX = src.offset()?.left ?? 0; - const sourceY = src.offset()?.top ?? 0; - const sourceWidth = src.outerWidth(true) as number; - const sourceHeight = src.outerHeight(true) as number; + const sourceX = src.getOffsetLeft() ?? 0; + const sourceY = src.getOffsetTop() ?? 0; + const sourceWidth = src.getOuterWidth(); + const sourceHeight = src.getOuterHeight(); const paddingX = convertRemToPixels(2); const paddingY = convertRemToPixels(2); @@ -350,7 +350,7 @@ export async function download(): Promise { } } -$(".pageTest").on("click", "#saveScreenshotButton", (event) => { +qs(".pageTest")?.onChild("click", "#saveScreenshotButton", (event) => { if (event.shiftKey) { void download(); } else { @@ -358,23 +358,23 @@ $(".pageTest").on("click", "#saveScreenshotButton", (event) => { } // reset save screenshot button icon - $("#saveScreenshotButton i") - .removeClass("fas fa-download") - .addClass("far fa-image"); + qs("#saveScreenshotButton i") + ?.removeClass(["fas", "fa-download"]) + ?.addClass(["far", "fa-image"]); }); -$(document).on("keydown", (event) => { +document.addEventListener("keydown", (event) => { if (!(TestState.resultVisible && getActivePage() === "test")) return; if (event.key !== "Shift") return; - $("#result #saveScreenshotButton i") - .removeClass("far fa-image") - .addClass("fas fa-download"); + qs("#result #saveScreenshotButton i") + ?.removeClass(["far", "fa-image"]) + ?.addClass(["fas", "fa-download"]); }); -$(document).on("keyup", (event) => { +document.addEventListener("keyup", (event) => { if (!(TestState.resultVisible && getActivePage() === "test")) return; if (event.key !== "Shift") return; - $("#result #saveScreenshotButton i") - .removeClass("fas fa-download") - .addClass("far fa-image"); + qs("#result #saveScreenshotButton i") + ?.removeClass(["fas", "fa-download"]) + ?.addClass(["far", "fa-image"]); }); diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index 9087193af650..c65e26ab74a5 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -56,7 +56,13 @@ import * as XPBar from "../elements/xp-bar"; import * as ModesNotice from "../elements/modes-notice"; import * as Last10Average from "../elements/last-10-average"; import * as MemoryFunboxTimer from "./funbox/memory-funbox-timer"; -import { qsr } from "../utils/dom"; +import { + ElementsWithUtils, + ElementWithUtils, + qs, + qsa, + qsr, +} from "../utils/dom"; import { getTheme } from "../signals/theme"; export const updateHintsPositionDebounced = Misc.debounceUntilResolved( @@ -64,10 +70,8 @@ export const updateHintsPositionDebounced = Misc.debounceUntilResolved( { rejectSkippedCalls: false }, ); -const wordsEl = document.querySelector(".pageTest #words") as HTMLElement; -const wordsWrapperEl = document.querySelector( - ".pageTest #wordsWrapper", -) as HTMLElement; +const wordsEl = qsr(".pageTest #words"); +const wordsWrapperEl = qsr(".pageTest #wordsWrapper"); const resultWordsHistoryEl = qsr(".pageTest #resultWordsHistory"); export let activeWordTop = 0; @@ -97,7 +101,7 @@ export function keepWordsInputInTheCenter(force = false): void { const wordsInput = getInputElement(); if (wordsInput === null || wordsWrapperEl === null) return; - const wordsWrapperHeight = wordsWrapperEl.offsetHeight; + const wordsWrapperHeight = wordsWrapperEl.getOffsetHeight(); const windowHeight = window.innerHeight; // dont do anything if the wrapper can fit on screen @@ -114,14 +118,12 @@ export function keepWordsInputInTheCenter(force = false): void { }); } -export function getWordElement(index: number): HTMLElement | null { - const el = wordsEl.querySelector( - `.word[data-wordindex='${index}']`, - ); +export function getWordElement(index: number): ElementWithUtils | null { + const el = wordsEl.qs(`.word[data-wordindex='${index}']`); return el; } -export function getActiveWordElement(): HTMLElement | null { +export function getActiveWordElement(): ElementWithUtils | null { return getWordElement(TestState.activeWordIndex); } @@ -135,16 +137,16 @@ export function updateActiveElement( let previousActiveWordTop: number | null = null; if (initial === undefined) { - const previousActiveWord = wordsEl.querySelector(".active"); + const previousActiveWord = wordsEl.qs(".active"); // in zen mode, because of the animation frame, previousActiveWord will be removed at this point, so check for null if (previousActiveWord !== null) { if (direction === "forward") { - previousActiveWord.classList.add("typed"); + previousActiveWord.addClass("typed"); } else if (direction === "back") { // } - previousActiveWord.classList.remove("active"); - previousActiveWordTop = previousActiveWord.offsetTop; + previousActiveWord.removeClass("active"); + previousActiveWordTop = previousActiveWord.getOffsetTop(); } } @@ -153,13 +155,12 @@ export function updateActiveElement( throw new Error("activeWord is null - can't update active element"); } - newActiveWord.classList.add("active"); - newActiveWord.classList.remove("error"); - newActiveWord.classList.remove("typed"); + newActiveWord.addClass("active"); + newActiveWord.removeClass("error"); + newActiveWord.removeClass("typed"); - activeWordTop = newActiveWord.offsetTop; - activeWordHeight = newActiveWord.offsetHeight; - console.log("activewordtopupdated"); + activeWordTop = newActiveWord.getOffsetTop(); + activeWordHeight = newActiveWord.getOffsetHeight(); updateWordsInputPosition(); @@ -171,7 +172,7 @@ export function updateActiveElement( (Config.mode === "custom" && CustomText.getLimitValue() === 0); if (isTimedTest || !Config.showAllLines) { - const newActiveWordTop = newActiveWord.offsetTop; + const newActiveWordTop = newActiveWord.getOffsetTop(); if (newActiveWordTop > previousActiveWordTop) { await lineJump(previousActiveWordTop); } @@ -185,7 +186,7 @@ export function updateActiveElement( function createHintsHtml( incorrectLettersIndices: number[][], - activeWordLetters: NodeListOf, + activeWordLetters: ElementsWithUtils, input: string | string[], wrapWithDiv: boolean = true, ): string { @@ -199,14 +200,14 @@ function createHintsHtml( for (const adjacentLetters of incorrectLettersIndices) { for (const letterIndex of adjacentLetters) { - const letter = activeWordLetters[letterIndex] as HTMLElement; + const letter = activeWordLetters[letterIndex] as ElementWithUtils; const blockIndices = `${letterIndex}`; const blockChars = isFullWord ? inputChars[letterIndex] : inputChars[currentHint++]; hintsHtml += `${blockChars}`; } } @@ -216,7 +217,7 @@ function createHintsHtml( async function joinOverlappingHints( incorrectLettersIndices: number[][], - activeWordLetters: NodeListOf, + activeWordLetters: ElementsWithUtils, hintElements: HTMLCollection, ): Promise { const [isWordRightToLeft, _isFullMatch] = Strings.isWordRightToLeft( @@ -250,10 +251,15 @@ async function joinOverlappingHints( continue; } - const block1Letter1 = activeWordLetters[block1Letter1Indx] as HTMLElement; - const block2Letter1 = activeWordLetters[block2Letter1Indx] as HTMLElement; + const block1Letter1 = activeWordLetters[ + block1Letter1Indx + ] as ElementWithUtils; + const block2Letter1 = activeWordLetters[ + block2Letter1Indx + ] as ElementWithUtils; - const sameTop = block1Letter1.offsetTop === block2Letter1.offsetTop; + const sameTop = + block1Letter1.getOffsetTop() === block2Letter1.getOffsetTop(); const leftBlock = isWordRightToLeft ? hintBlock2 : hintBlock1; const rightBlock = isWordRightToLeft ? hintBlock1 : hintBlock2; @@ -270,8 +276,8 @@ async function joinOverlappingHints( ].join(","); const block1Letter1Pos = - block1Letter1.offsetLeft + - (isWordRightToLeft ? block1Letter1.offsetWidth : 0); + block1Letter1.getOffsetLeft() + + (isWordRightToLeft ? block1Letter1.getOffsetWidth() : 0); const bothBlocksLettersWidthHalved = hintBlock2.offsetLeft - hintBlock1.offsetLeft; hintBlock1.style.left = @@ -345,8 +351,10 @@ async function updateHintsPosition(): Promise { ): Promise { if (!hintsContainer || hintIndices.length === 0) return; - const wordElement = hintsContainer.parentElement as HTMLElement; - const letterElements = wordElement.querySelectorAll("letter"); + const wordElement = new ElementWithUtils( + hintsContainer.parentElement as HTMLElement, + ); + const letterElements = wordElement.qsa("letter"); hintsContainer.innerHTML = createHintsHtml( hintIndices, @@ -354,7 +362,7 @@ async function updateHintsPosition(): Promise { hintText, false, ); - const wordHintsElements = wordElement.getElementsByTagName("hint"); + const wordHintsElements = wordElement.native.getElementsByTagName("hint"); await joinOverlappingHints(hintIndices, letterElements, wordHintsElements); } } @@ -390,71 +398,72 @@ function updateWordWrapperClasses(): void { OutOfFocus.hide(); if (Config.tapeMode !== "off") { - wordsEl.classList.add("tape"); - wordsWrapperEl.classList.add("tape"); + wordsEl.addClass("tape"); + wordsWrapperEl.addClass("tape"); } else { - wordsEl.classList.remove("tape"); - wordsWrapperEl.classList.remove("tape"); + wordsEl.removeClass("tape"); + wordsWrapperEl.removeClass("tape"); } if (Config.blindMode) { - wordsEl.classList.add("blind"); - wordsWrapperEl.classList.add("blind"); + wordsEl.addClass("blind"); + wordsWrapperEl.addClass("blind"); } else { - wordsEl.classList.remove("blind"); - wordsWrapperEl.classList.remove("blind"); + wordsEl.removeClass("blind"); + wordsWrapperEl.removeClass("blind"); } if (Config.indicateTypos === "below" || Config.indicateTypos === "both") { - wordsEl.classList.add("indicateTyposBelow"); - wordsWrapperEl.classList.add("indicateTyposBelow"); + wordsEl.addClass("indicateTyposBelow"); + wordsWrapperEl.addClass("indicateTyposBelow"); } else { - wordsEl.classList.remove("indicateTyposBelow"); - wordsWrapperEl.classList.remove("indicateTyposBelow"); + wordsEl.removeClass("indicateTyposBelow"); + wordsWrapperEl.removeClass("indicateTyposBelow"); } if (Config.hideExtraLetters) { - wordsEl.classList.add("hideExtraLetters"); - wordsWrapperEl.classList.add("hideExtraLetters"); + wordsEl.addClass("hideExtraLetters"); + wordsWrapperEl.addClass("hideExtraLetters"); } else { - wordsEl.classList.remove("hideExtraLetters"); - wordsWrapperEl.classList.remove("hideExtraLetters"); + wordsEl.removeClass("hideExtraLetters"); + wordsWrapperEl.removeClass("hideExtraLetters"); } if (Config.flipTestColors) { - wordsEl.classList.add("flipped"); + wordsEl.addClass("flipped"); } else { - wordsEl.classList.remove("flipped"); + wordsEl.removeClass("flipped"); } if (Config.colorfulMode) { - wordsEl.classList.add("colorfulMode"); + wordsEl.addClass("colorfulMode"); } else { - wordsEl.classList.remove("colorfulMode"); + wordsEl.removeClass("colorfulMode"); } - $( + qsa( "#caret, #paceCaret, #liveStatsMini, #typingTest, #wordsInput, #compositionDisplay", - ).css("fontSize", Config.fontSize + "rem"); + ).setStyle({ fontSize: Config.fontSize + "rem" }); if (TestState.isLanguageRightToLeft) { - wordsEl.classList.add("rightToLeftTest"); - $("#resultWordsHistory .words").addClass("rightToLeftTest"); - $("#resultReplay .words").addClass("rightToLeftTest"); + wordsEl.addClass("rightToLeftTest"); + qs("#resultWordsHistory .words")?.addClass("rightToLeftTest"); + qs("#resultReplay .words")?.addClass("rightToLeftTest"); } else { - wordsEl.classList.remove("rightToLeftTest"); - $("#resultWordsHistory .words").removeClass("rightToLeftTest"); - $("#resultReplay .words").removeClass("rightToLeftTest"); + wordsEl.removeClass("rightToLeftTest"); + qs("#resultWordsHistory .words")?.removeClass("rightToLeftTest"); + qs("#resultReplay .words")?.removeClass("rightToLeftTest"); } const existing = - wordsEl?.className + wordsEl.native.className .split(/\s+/) .filter((className) => !className.startsWith("highlight-")) ?? []; if (Config.highlightMode !== null) { existing.push("highlight-" + Config.highlightMode.replaceAll("_", "-")); } - wordsEl.className = existing.join(" "); + + wordsEl.native.className = existing.join(" "); updateWordsWidth(); updateWordsWrapperHeight(true); @@ -472,7 +481,7 @@ function updateWordWrapperClasses(): void { } function showWords(): void { - wordsEl.innerHTML = ""; + wordsEl.setHtml(""); if (Config.mode === "zen") { appendEmptyWordElement(); @@ -481,7 +490,7 @@ function showWords(): void { for (let i = 0; i < TestWords.words.length; i++) { wordsHTML += buildWordHTML(TestWords.words.get(i), i); } - wordsEl.innerHTML = wordsHTML; + wordsEl.setHtml(wordsHTML); } updateActiveElement({ @@ -494,8 +503,7 @@ function showWords(): void { export function appendEmptyWordElement( index = TestInput.input.getHistory().length, ): void { - wordsEl.insertAdjacentHTML( - "beforeend", + wordsEl.appendHtml( `
`, ); } @@ -520,30 +528,30 @@ export function updateWordsInputPosition(): void { const letterHeight = convertRemToPixels(Config.fontSize); const targetTop = - activeWord.offsetTop + letterHeight / 2 - el.offsetHeight / 2 + 1; //+1 for half of border + activeWord.getOffsetTop() + letterHeight / 2 - el.offsetHeight / 2 + 1; //+1 for half of border if (Config.tapeMode !== "off") { el.style.maxWidth = `${100 - Config.tapeMargin}%`; } else { el.style.maxWidth = ""; } - if (activeWord.offsetWidth < letterHeight) { + if (activeWord.getOffsetWidth() < letterHeight) { el.style.width = letterHeight + "px"; } else { - el.style.width = activeWord.offsetWidth + "px"; + el.style.width = activeWord.getOffsetWidth() + "px"; } el.style.top = targetTop + "px"; if (Config.tapeMode !== "off") { el.style.left = `${ - wordsWrapperEl.offsetWidth * (Config.tapeMargin / 100) + wordsWrapperEl.getOffsetWidth() * (Config.tapeMargin / 100) }px`; } else { - if (activeWord.offsetWidth < letterHeight && isTestRightToLeft) { - el.style.left = activeWord.offsetLeft - letterHeight + "px"; + if (activeWord.getOffsetWidth() < letterHeight && isTestRightToLeft) { + el.style.left = activeWord.getOffsetLeft() - letterHeight + "px"; } else { - el.style.left = Math.max(0, activeWord.offsetLeft) + "px"; + el.style.left = Math.max(0, activeWord.getOffsetLeft()) + "px"; } } @@ -565,11 +573,11 @@ export async function centerActiveLine(): Promise { resolve(); return; } - const currentTop = activeWordEl.offsetTop; + const currentTop = activeWordEl.getOffsetTop(); let previousLineTop = currentTop; for (let i = TestState.activeWordIndex - 1; i >= 0; i--) { - previousLineTop = getWordElement(i)?.offsetTop ?? currentTop; + previousLineTop = getWordElement(i)?.getOffsetTop() ?? currentTop; if (previousLineTop < currentTop) { await lineJump(previousLineTop, true); resolve(); @@ -589,13 +597,13 @@ export function updateWordsWrapperHeight(force = false): void { const activeWordEl = getActiveWordElement(); if (!activeWordEl) return; - wordsWrapperEl.classList.remove("hidden"); + wordsWrapperEl.removeClass("hidden"); - const wordComputedStyle = window.getComputedStyle(activeWordEl); + const wordComputedStyle = window.getComputedStyle(activeWordEl.native); const wordMargin = parseInt(wordComputedStyle.marginTop) + parseInt(wordComputedStyle.marginBottom); - const wordHeight = activeWordEl.offsetHeight + wordMargin; + const wordHeight = activeWordEl.getOffsetHeight() + wordMargin; const timedTest = Config.mode === "time" || @@ -606,14 +614,14 @@ export function updateWordsWrapperHeight(force = false): void { if (showAllLines) { //allow the wrapper to grow and shink with the words - wordsWrapperEl.style.height = ""; + wordsWrapperEl.setStyle({ height: "" }); } else if (Config.mode === "zen") { //zen mode, showAllLines off - wordsWrapperEl.style.height = wordHeight * 2 + "px"; + wordsWrapperEl.setStyle({ height: wordHeight * 2 + "px" }); } else { if (Config.tapeMode === "off") { //tape off, showAllLines off, non-zen mode - const wordElements = wordsEl.querySelectorAll(".word"); + const wordElements = wordsEl.qsa(".word"); let lines = 0; let lastTop = 0; let wordIndex = 0; @@ -622,10 +630,10 @@ export function updateWordsWrapperHeight(force = false): void { while (lines < 3) { const word = wordElements[wordIndex]; if (!word) break; - const top = word.offsetTop; + const top = word.getOffsetTop(); if (top > lastTop) { lines++; - wrapperHeight += word.offsetHeight + wordMargin; + wrapperHeight += word.getOffsetHeight() + wordMargin; lastTop = top; } wordIndex++; @@ -633,14 +641,14 @@ export function updateWordsWrapperHeight(force = false): void { if (lines < 3) wrapperHeight = wrapperHeight * (3 / lines); //limit to 3 lines - wordsWrapperEl.style.height = wrapperHeight + "px"; + wordsWrapperEl.setStyle({ height: wrapperHeight + "px" }); } else { //show 3 lines if tape mode is on and has newlines, otherwise use words height (because of indicate typos: below) if (TestWords.hasNewline) { - wordsWrapperEl.style.height = wordHeight * 3 + "px"; + wordsWrapperEl.setStyle({ height: wordHeight * 3 + "px" }); } else { - const wordsHeight = wordsEl.offsetHeight ?? wordHeight; - wordsWrapperEl.style.height = wordsHeight + "px"; + const wordsHeight = wordsEl.getOffsetHeight() ?? wordHeight; + wordsWrapperEl.setStyle({ height: wordsHeight + "px" }); } } } @@ -650,15 +658,15 @@ export function updateWordsWrapperHeight(force = false): void { function updateWordsMargin(): void { if (Config.tapeMode !== "off") { - wordsEl.style.marginLeft = "0"; + wordsEl.setStyle({ marginLeft: "0" }); void scrollTape(true); } else { - const afterNewlineEls = - wordsEl.querySelectorAll(".afterNewline"); - wordsEl.style.marginLeft = `0`; - wordsEl.style.marginTop = `0`; + const afterNewlineEls = wordsEl.qsa(".afterNewline"); + wordsEl.setStyle({ marginLeft: "0", marginTop: "0" }); for (const afterNewline of afterNewlineEls) { - afterNewline.style.marginLeft = `0`; + afterNewline.setStyle({ + marginLeft: "0", + }); } } } @@ -670,10 +678,10 @@ export function addWord( // if the current active word is the last word, we need to NOT use raf // because other ui parts depend on the word existing if (TestState.activeWordIndex === wordIndex - 1) { - wordsEl.insertAdjacentHTML("beforeend", buildWordHTML(word, wordIndex)); + wordsEl.appendHtml(buildWordHTML(word, wordIndex)); } else { requestAnimationFrame(async () => { - wordsEl.insertAdjacentHTML("beforeend", buildWordHTML(word, wordIndex)); + wordsEl.appendHtml(buildWordHTML(word, wordIndex)); }); } @@ -840,10 +848,10 @@ export async function updateWordLetters({ } } - wordAtIndex.innerHTML = ret; + wordAtIndex.setHtml(ret); if (hintIndices?.length) { - const wordAtIndexLetters = wordAtIndex.querySelectorAll("letter"); + const wordAtIndexLetters = wordAtIndex.qsa("letter"); let hintsHtml; if (Config.indicateTypos === "both") { hintsHtml = createHintsHtml( @@ -854,8 +862,8 @@ export async function updateWordLetters({ } else { hintsHtml = createHintsHtml(hintIndices, wordAtIndexLetters, input); } - wordAtIndex.insertAdjacentHTML("beforeend", hintsHtml); - const hintElements = wordAtIndex.getElementsByTagName("hint"); + wordAtIndex.appendHtml(hintsHtml); + const hintElements = wordAtIndex.native.getElementsByTagName("hint"); await joinOverlappingHints( hintIndices, wordAtIndexLetters, @@ -864,7 +872,7 @@ export async function updateWordLetters({ } if (newlineafter) { - wordAtIndex.insertAdjacentHTML( + wordAtIndex.native.insertAdjacentHTML( "afterend", "
", ); @@ -878,7 +886,7 @@ export async function updateWordLetters({ // unless slow timer is on, then it needs to happen // because the word jump check is disabled if (!Config.showAllLines) { - const wordTopAfterUpdate = wordAtIndex.offsetTop; + const wordTopAfterUpdate = wordAtIndex.getOffsetTop(); if (wordTopAfterUpdate > activeWordTop) { await lineJump(activeWordTop, true); } @@ -892,24 +900,22 @@ export async function updateWordLetters({ // and sometimes we want it to be shifted to the left // (for example if the newline is typed incorrectly, or there are any extra letters after it) function getNlCharWidth( - lastWordInLine?: Element | HTMLElement, + lastWordInLine?: ElementWithUtils, checkIfIncorrect = true, ): number { - let nlChar: HTMLElement | null; + let nlChar: ElementWithUtils | null; if (lastWordInLine) { - nlChar = lastWordInLine.querySelector("letter.nlChar"); + nlChar = lastWordInLine.qs("letter.nlChar"); } else { - nlChar = document.querySelector( - "#words > .word > letter.nlChar", - ); + nlChar = qs("#words > .word > letter.nlChar"); } if (!nlChar) return 0; - if (checkIfIncorrect && nlChar.classList.contains("incorrect")) return 0; - const letterComputedStyle = window.getComputedStyle(nlChar); + if (checkIfIncorrect && nlChar.hasClass("incorrect")) return 0; + const letterComputedStyle = window.getComputedStyle(nlChar.native); const letterMargin = parseFloat(letterComputedStyle.marginLeft) + parseFloat(letterComputedStyle.marginRight); - return nlChar.offsetWidth + letterMargin; + return nlChar.getOffsetWidth() + letterMargin; } export async function scrollTape(noAnimation = false): Promise { @@ -921,11 +927,11 @@ export async function scrollTape(noAnimation = false): Promise { ? !TestState.isLanguageRightToLeft : TestState.isLanguageRightToLeft; - const wordsWrapperWidth = wordsWrapperEl.offsetWidth; - const wordsChildrenArr = [...wordsEl.children] as HTMLElement[]; + const wordsWrapperWidth = wordsWrapperEl.getOffsetWidth(); + const wordsChildrenArr = wordsEl.getChildren(); const activeWordEl = getActiveWordElement(); if (!activeWordEl) return; - const afterNewLineEls = wordsEl.getElementsByClassName("afterNewline"); + const afterNewLineEls = wordsEl.qsa(".afterNewline"); let wordsWidthBeforeActive = 0; let fullLineWidths = 0; @@ -934,17 +940,19 @@ export async function scrollTape(noAnimation = false): Promise { let widthRemoved = 0; const widthRemovedFromLine: number[] = []; const afterNewlinesNewMargins: number[] = []; - const toRemove: HTMLElement[] = []; + const toRemove: ElementWithUtils[] = []; /* remove leading `.afterNewline` elements */ for (const child of wordsChildrenArr) { - if (child.classList.contains("word")) { + if (child.hasClass("word")) { // only last leading `.afterNewline` element pushes `.word`s to right if (lastAfterNewLineElement) { - widthRemoved += parseFloat(lastAfterNewLineElement.style.marginLeft); + widthRemoved += parseFloat( + lastAfterNewLineElement.getStyle().marginLeft, + ); } break; - } else if (child.classList.contains("afterNewline")) { + } else if (child.hasClass("afterNewline")) { toRemove.push(child); leadingNewLine = true; lastAfterNewLineElement = child; @@ -959,17 +967,13 @@ export async function scrollTape(noAnimation = false): Promise { // this will be 0 or 1 const newLinesBeforeActiveWord = wordsChildrenArr .slice(0, activeWordIndex) - .filter((child) => child.classList.contains("afterNewline")).length; + .filter((child) => child.hasClass("afterNewline")).length; // the second `.afterNewline` after active word is visible during line jump - let lastVisibleAfterNewline = afterNewLineEls[newLinesBeforeActiveWord + 1] as - | HTMLElement - | undefined; + let lastVisibleAfterNewline = afterNewLineEls[newLinesBeforeActiveWord + 1]; if (lastVisibleAfterNewline) { lastElementIndex = wordsChildrenArr.indexOf(lastVisibleAfterNewline); } else { - lastVisibleAfterNewline = afterNewLineEls[newLinesBeforeActiveWord] as - | HTMLElement - | undefined; + lastVisibleAfterNewline = afterNewLineEls[newLinesBeforeActiveWord]; if (lastVisibleAfterNewline) { lastElementIndex = wordsChildrenArr.indexOf(lastVisibleAfterNewline); } else { @@ -978,21 +982,21 @@ export async function scrollTape(noAnimation = false): Promise { } const wordRightMargin = parseFloat( - window.getComputedStyle(activeWordEl).marginRight, + window.getComputedStyle(activeWordEl.native).marginRight, ); /*calculate .afterNewline & #words new margins + determine elements to remove*/ for (let i = 0; i <= lastElementIndex; i++) { - const child = wordsChildrenArr[i] as HTMLElement; - if (child.classList.contains("word")) { + const child = wordsChildrenArr[i] as ElementWithUtils; + if (child.hasClass("word")) { leadingNewLine = false; - const childComputedStyle = window.getComputedStyle(child); + const childComputedStyle = window.getComputedStyle(child.native); const wordOuterWidth = - child.offsetWidth + + child.getOffsetWidth() + parseFloat(childComputedStyle.marginRight) + parseFloat(childComputedStyle.marginLeft); - const forWordLeft = Math.floor(child.offsetLeft); - const forWordWidth = Math.floor(child.offsetWidth); + const forWordLeft = Math.floor(child.getOffsetLeft()); + const forWordWidth = Math.floor(child.getOffsetWidth()); if ( (!isTestRightToLeft && forWordLeft < 0 - forWordWidth) || (isTestRightToLeft && forWordLeft > wordsWrapperWidth) @@ -1003,7 +1007,7 @@ export async function scrollTape(noAnimation = false): Promise { fullLineWidths += wordOuterWidth; if (i < activeWordIndex) wordsWidthBeforeActive = fullLineWidths; } - } else if (child.classList.contains("afterNewline")) { + } else if (child.hasClass("afterNewline")) { if (leadingNewLine) continue; const nlCharWidth = getNlCharWidth(wordsChildrenArr[i - 3]); fullLineWidths -= nlCharWidth + wordRightMargin; @@ -1013,7 +1017,7 @@ export async function scrollTape(noAnimation = false): Promise { * increase limit if that ever happens, but keep the limit because browsers hate * ridiculously wide margins which may cause the words to not be displayed */ - const limit = 3 * wordsEl.offsetWidth; + const limit = 3 * wordsEl.getOffsetWidth(); if (fullLineWidths < limit) { afterNewlinesNewMargins.push(fullLineWidths); widthRemovedFromLine.push(widthRemoved); @@ -1034,16 +1038,16 @@ export async function scrollTape(noAnimation = false): Promise { if (toRemove.length > 0) { for (const el of toRemove) el.remove(); for (let i = 0; i < widthRemovedFromLine.length; i++) { - const afterNewlineEl = afterNewLineEls[i] as HTMLElement; + const afterNewlineEl = afterNewLineEls[i] as ElementWithUtils; const currentLineIndent = - parseFloat(afterNewlineEl.style.marginLeft) || 0; - afterNewlineEl.style.marginLeft = `${ - currentLineIndent - (widthRemovedFromLine[i] ?? 0) - }px`; + parseFloat(afterNewlineEl.getStyle().marginLeft) || 0; + afterNewlineEl.setStyle({ + marginLeft: `${currentLineIndent - (widthRemovedFromLine[i] ?? 0)}px`, + }); } if (isTestRightToLeft) widthRemoved *= -1; - const currentWordsMargin = parseFloat(wordsEl.style.marginLeft) || 0; - wordsEl.style.marginLeft = `${currentWordsMargin + widthRemoved}px`; + const currentWordsMargin = parseFloat(wordsEl.native.style.marginLeft) || 0; + wordsEl.setStyle({ marginLeft: `${currentWordsMargin + widthRemoved}px` }); Caret.caret.handleTapeWordsRemoved(widthRemoved); PaceCaret.caret.handleTapeWordsRemoved(widthRemoved); } @@ -1052,22 +1056,22 @@ export async function scrollTape(noAnimation = false): Promise { let currentWordWidth = 0; const inputLength = TestInput.input.current.length; if (Config.tapeMode === "letter" && inputLength > 0) { - const letters = activeWordEl.querySelectorAll("letter"); + const letters = activeWordEl.qsa("letter"); let lastPositiveLetterWidth = 0; for (let i = 0; i < inputLength; i++) { const letter = letters[i]; if ( (Config.blindMode || Config.hideExtraLetters) && - letter?.classList.contains("extra") + letter?.hasClass("extra") ) { continue; } - const letterOuterWidth = letter?.offsetWidth ?? 0; + const letterOuterWidth = letter?.getOffsetWidth() ?? 0; currentWordWidth += letterOuterWidth; if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth; } // if current letter has zero width move the tape to previous positive width letter - if (letters[inputLength]?.offsetWidth === 0) { + if (letters[inputLength]?.getOffsetWidth() === 0) { currentWordWidth -= lastPositiveLetterWidth; } } @@ -1102,17 +1106,17 @@ export async function scrollTape(noAnimation = false): Promise { for (let i = 0; i < afterNewlinesNewMargins.length; i++) { const newMargin = afterNewlinesNewMargins[i] ?? 0; - animate(afterNewLineEls[i] as Element, { + animate(afterNewLineEls[i] as ElementWithUtils, { marginLeft: newMargin, duration, ease, }); } } else { - wordsEl.style.marginLeft = `${newMargin}px`; + wordsEl.setStyle({ marginLeft: `${newMargin}px` }); for (let i = 0; i < afterNewlinesNewMargins.length; i++) { const newMargin = afterNewlinesNewMargins[i] ?? 0; - (afterNewLineEls[i] as HTMLElement).style.marginLeft = `${newMargin}px`; + afterNewLineEls[i]?.setStyle({ marginLeft: `${newMargin}px` }); } } } @@ -1123,22 +1127,22 @@ export function updatePremid(): void { if (Config.funbox.length > 0) { fbtext = " " + Config.funbox.join(" "); } - $(".pageTest #premidTestMode").text( + qs(".pageTest #premidTestMode")?.setText( `${Config.mode} ${mode2} ${Strings.getLanguageDisplayString( Config.language, )}${fbtext}`, ); - $(".pageTest #premidSecondsLeft").text(Config.time); + qs(".pageTest #premidSecondsLeft")?.setText(`${Config.time}`); } function removeTestElements(lastElementIndexToRemove: number): void { - const wordsChildren = wordsEl.children; + const wordsChildren = wordsEl.getChildren(); if (wordsChildren === undefined) return; for (let i = lastElementIndexToRemove; i >= 0; i--) { const child = wordsChildren[i]; - if (!child || !child.isConnected) continue; + if (!child || !child.native.isConnected) continue; child.remove(); } } @@ -1161,18 +1165,18 @@ export async function lineJump( // index of the active word in all #words.children // (which contains .word/.newline/.beforeNewline/.afterNewline elements) - const wordsChildren = [...wordsEl.children]; + const wordsChildren = wordsEl.getChildren(); const activeWordElementIndex = wordsChildren.indexOf(activeWordEl); let lastElementIndexToRemove: number | undefined = undefined; for (let i = activeWordElementIndex - 1; i >= 0; i--) { - const child = wordsChildren[i] as HTMLElement; - if (child.classList.contains("hidden")) continue; - if (Math.floor(child.offsetTop) < hideBound) { - if (child.classList.contains("word")) { + const child = wordsChildren[i] as ElementWithUtils; + if (child.hasClass("hidden")) continue; + if (Math.floor(child.getOffsetTop()) < hideBound) { + if (child.hasClass("word")) { lastElementIndexToRemove = i; break; - } else if (child.classList.contains("beforeNewline")) { + } else if (child.hasClass("beforeNewline")) { // set it to .newline but check .beforeNewline.offsetTop // because it's more reliable lastElementIndexToRemove = i + 1; @@ -1189,7 +1193,7 @@ export async function lineJump( currentLinesJumping++; - const wordHeight = $(activeWordEl).outerHeight(true) as number; + const wordHeight = activeWordEl.getOuterHeight(); const newMarginTop = -1 * wordHeight * currentLinesJumping; const duration = 125; @@ -1202,15 +1206,15 @@ export async function lineJump( if (Config.smoothLineScroll) { lineTransition = true; - await Misc.promiseAnimate(wordsEl, { + await Misc.promiseAnimate(wordsEl.native, { marginTop: newMarginTop, duration, }); currentLinesJumping = 0; - activeWordTop = activeWordEl.offsetTop; - activeWordHeight = activeWordEl.offsetHeight; + activeWordTop = activeWordEl.getOffsetTop(); + activeWordHeight = activeWordEl.getOffsetHeight(); removeTestElements(lastElementIndexToRemove); - wordsEl.style.marginTop = "0"; + wordsEl.setStyle({ marginTop: "0" }); lineTransition = false; } else { currentLinesJumping = 0; @@ -1224,13 +1228,13 @@ export async function lineJump( export function setLigatures(isEnabled: boolean): void { if (isEnabled || Config.mode === "custom" || Config.mode === "zen") { - wordsEl.classList.add("withLigatures"); - $("#resultWordsHistory .words").addClass("withLigatures"); - $("#resultReplay .words").addClass("withLigatures"); + wordsEl.addClass("withLigatures"); + qs("#resultWordsHistory .words")?.addClass("withLigatures"); + qs("#resultReplay .words")?.addClass("withLigatures"); } else { - wordsEl.classList.remove("withLigatures"); - $("#resultWordsHistory .words").removeClass("withLigatures"); - $("#resultReplay .words").removeClass("withLigatures"); + wordsEl.removeClass("withLigatures"); + qs("#resultWordsHistory .words")?.removeClass("withLigatures"); + qs("#resultReplay .words")?.removeClass("withLigatures"); } } @@ -1301,8 +1305,9 @@ function buildWordLettersHTML( } async function loadWordsHistory(): Promise { - $("#resultWordsHistory .words").empty(); - let wordsHTML = ""; + const wordsContainer = qs("#resultWordsHistory .words"); + wordsContainer?.empty(); + const inputHistoryLength = TestInput.input.getHistory().length; for (let i = 0; i < inputHistoryLength + 2; i++) { const input = TestInput.input.getHistory(i); @@ -1315,7 +1320,14 @@ async function loadWordsHistory(): Promise { word?.match( /[\uac00-\ud7af]|[\u1100-\u11ff]|[\u3130-\u318f]|[\ua960-\ua97f]|[\ud7b0-\ud7ff]/g, ) !== null; - let wordEl = ""; + + const wordEl = document.createElement("div"); + wordEl.className = "word"; + + if (input !== "" && input !== undefined) { + wordEl.classList.add("nocursor"); + } + try { if (input === undefined || input === "") { throw new Error("empty input word"); @@ -1333,21 +1345,22 @@ async function loadWordsHistory(): Promise { Config.mode !== "zen" && !(isLastWord && isTimedTest && isPartiallyCorrect); - const errorClass = isIncorrectWord && shouldShowError ? "error" : ""; + if (isIncorrectWord && shouldShowError) { + wordEl.classList.add("error"); + } + + const burstValue = TestInput.burstHistory[i]; + if (burstValue !== undefined) { + wordEl.setAttribute("burst", String(burstValue)); + } if (corrected !== undefined && corrected !== "") { const correctedChar = !containsKorean ? corrected : Hangul.assemble(corrected.split("")); - wordEl = `
`; + wordEl.setAttribute("input", correctedChar.replace(/ /g, "_")); } else { - wordEl = `
`; + wordEl.setAttribute("input", input.replace(/ /g, "_")); } const inputCharacters = Strings.splitIntoCharacters(input); @@ -1365,7 +1378,7 @@ async function loadWordsHistory(): Promise { if (corrected === undefined) throw new Error("empty corrected word"); - wordEl += buildWordLettersHTML( + wordEl.innerHTML = buildWordLettersHTML( loop, input, corrected, @@ -1374,22 +1387,55 @@ async function loadWordsHistory(): Promise { correctedCharacters, containsKorean, ); - wordEl += "
"; } catch (e) { try { - wordEl = "
"; for (const char of word) { - wordEl += "" + char + ""; + const letterEl = document.createElement("letter"); + letterEl.textContent = char; + wordEl.appendChild(letterEl); } - wordEl += "
"; } catch { - wordEl += "
"; + // wordEl is already created, just leave it empty or with partial content } } - wordsHTML += wordEl; + + wordEl.addEventListener("mouseenter", (e) => { + // if (noHover) return; + if (!TestState.resultVisible) return; + const input = + (e.currentTarget as HTMLElement).getAttribute("input") ?? ""; + const burst = parseInt( + (e.currentTarget as HTMLElement).getAttribute("burst") as string, + ); + if (input === "") return; + (e.currentTarget as HTMLElement).insertAdjacentHTML( + "beforeend", + `
+
+ ${input + .replace(/\t/g, "_") + .replace(/\n/g, "_") + .replace(//g, ">")} +
+
+ ${isNaN(burst) || burst >= 1000 ? "Infinite" : Format.typingSpeed(burst, { showDecimalPlaces: false })} + ${Config.typingSpeedUnit} +
+
`, + ); + }); + + wordEl.addEventListener("mouseleave", (e) => { + wordEl.querySelector(".wordInputHighlight")?.remove(); + }); + + // Append each word element individually to the DOM + // This ensures elements are immediately available for event listeners + wordsContainer?.native.appendChild(wordEl); } - $("#resultWordsHistory .words").html(wordsHTML); - $("#showWordHistoryButton").addClass("loaded"); + + qs("#showWordHistoryButton")?.addClass("loaded"); return true; } @@ -1410,7 +1456,7 @@ export async function toggleResultWords(noAnimation = false): Promise { export async function applyBurstHeatmap(): Promise { if (Config.burstHeatmap) { - $("#resultWordsHistory .heatmapLegend").removeClass("hidden"); + qsa("#resultWordsHistory .heatmapLegend")?.removeClass("hidden"); let burstlist = [...TestInput.burstHistory]; @@ -1484,15 +1530,15 @@ export async function applyBurstHeatmap(): Promise { } } - $("#resultWordsHistory .heatmapLegend .box" + index).html( + qs("#resultWordsHistory .heatmapLegend .box" + index)?.setHtml( `
${Misc.escapeHTML(string)}
`, ); }); - $("#resultWordsHistory .words .word").each((_, word) => { - const wordBurstAttr = $(word).attr("burst"); - if (wordBurstAttr === undefined) { - $(word).css("color", unreachedColor); + for (const word of qsa("#resultWordsHistory .words .word")) { + const wordBurstAttr = word.getAttribute("burst"); + if (wordBurstAttr === undefined || wordBurstAttr === null) { + word.setStyle({ color: unreachedColor }); } else { let wordBurstVal = parseInt(wordBurstAttr); wordBurstVal = Math.round( @@ -1500,28 +1546,33 @@ export async function applyBurstHeatmap(): Promise { ); steps.forEach((step) => { if (wordBurstVal >= step.val) { - $(word).addClass("heatmapInherit"); - $(word).css("color", colors[step.colorId] as string); + word.addClass("heatmapInherit"); + word.setStyle({ color: colors[step.colorId] as string }); } }); } - }); + } - $("#resultWordsHistory .heatmapLegend .boxes .box").each((index, box) => { - $(box).css("background", colors[index] as string); - }); + const boxes = qsa("#resultWordsHistory .heatmapLegend .boxes .box"); + for (let i = 0; i < boxes.length; i++) { + (boxes[i] as ElementWithUtils).setStyle({ + background: colors[i] as string, + }); + } } else { - $("#resultWordsHistory .heatmapLegend").addClass("hidden"); - $("#resultWordsHistory .words .word").removeClass("heatmapInherit"); - $("#resultWordsHistory .words .word").css("color", ""); + qs("#resultWordsHistory .heatmapLegend")?.addClass("hidden"); + qsa("#resultWordsHistory .words .word")?.removeClass("heatmapInherit"); + qsa("#resultWordsHistory .words .word")?.setStyle({ color: "" }); - $("#resultWordsHistory .heatmapLegend .boxes .box").css("color", ""); + qsa("#resultWordsHistory .heatmapLegend .boxes .box")?.setStyle({ + color: "", + }); } } export function highlightBadWord(index: number): void { requestDebouncedAnimationFrame(`test-ui.highlightBadWord.${index}`, () => { - getWordElement(index)?.classList.add("error"); + getWordElement(index)?.addClass("error"); }); } @@ -1529,9 +1580,9 @@ export function highlightAllLettersAsCorrect(wordIndex: number): void { requestDebouncedAnimationFrame( `test-ui.highlightAllLettersAsCorrect.${wordIndex}`, () => { - const letters = getWordElement(wordIndex)?.children; + const letters = getWordElement(wordIndex)?.getChildren(); for (const letter of letters ?? []) { - letter.classList.add("correct"); + letter.addClass("correct"); } }, ); @@ -1560,76 +1611,77 @@ function updateWordsWidth(): void { }; } } - const el = $("#typingTest"); - el.css(css); + const el = qs("#typingTest"); + el?.setStyle(css); if (Config.maxLineWidth === 0) { - el.removeClass("full-width-padding").addClass("content"); + el?.removeClass("full-width-padding").addClass("content"); } else { - el.removeClass("content").addClass("full-width-padding"); + el?.removeClass("content").addClass("full-width-padding"); } } function updateLiveStatsMargin(): void { if (Config.tapeMode === "off") { - $("#liveStatsMini").css({ - "justify-content": "start", - "margin-left": "0.25em", + qs("#liveStatsMini")?.setStyle({ + justifyContent: "start", + marginLeft: "0.25em", }); } else { - $("#liveStatsMini").css({ - "justify-content": "center", - "margin-left": Config.tapeMargin + "%", + qs("#liveStatsMini")?.setStyle({ + justifyContent: "center", + marginLeft: Config.tapeMargin + "%", }); } } function updateLiveStatsOpacity(value: TimerOpacity): void { - $("#barTimerProgress").css("opacity", parseFloat(value as string)); - $("#liveStatsTextTop").css("opacity", parseFloat(value as string)); - $("#liveStatsTextBottom").css("opacity", parseFloat(value as string)); - $("#liveStatsMini").css("opacity", parseFloat(value as string)); + qs("#barTimerProgress")?.setStyle({ opacity: value }); + qs("#liveStatsTextTop")?.setStyle({ opacity: value }); + qs("#liveStatsTextBottom")?.setStyle({ + opacity: value, + }); + qs("#liveStatsMini")?.setStyle({ opacity: value }); } function updateLiveStatsColor(value: TimerColor): void { - $("#barTimerProgress").removeClass("timerSub"); - $("#barTimerProgress").removeClass("timerText"); - $("#barTimerProgress").removeClass("timerMain"); - - $("#liveStatsTextTop").removeClass("timerSub"); - $("#liveStatsTextTop").removeClass("timerText"); - $("#liveStatsTextTop").removeClass("timerMain"); + qs("#barTimerProgress")?.removeClass("timerSub"); + qs("#barTimerProgress")?.removeClass("timerText"); + qs("#barTimerProgress")?.removeClass("timerMain"); - $("#liveStatsTextBottom").removeClass("timerSub"); - $("#liveStatsTextBottom").removeClass("timerText"); - $("#liveStatsTextBottom").removeClass("timerMain"); + qs("#liveStatsTextTop")?.removeClass("timerSub"); + qs("#liveStatsTextTop")?.removeClass("timerText"); + qs("#liveStatsTextTop")?.removeClass("timerMain"); + qs("#liveStatsTextBottom")?.removeClass("timerSub"); + qs("#liveStatsTextBottom")?.removeClass("timerText"); + qs("#liveStatsTextBottom")?.removeClass("timerMain"); - $("#liveStatsMini").removeClass("timerSub"); - $("#liveStatsMini").removeClass("timerText"); - $("#liveStatsMini").removeClass("timerMain"); + qs("#liveStatsMini")?.removeClass("timerSub"); + qs("#liveStatsMini")?.removeClass("timerText"); + qs("#liveStatsMini")?.removeClass("timerMain"); if (value === "main") { - $("#barTimerProgress").addClass("timerMain"); - $("#liveStatsTextTop").addClass("timerMain"); - $("#liveStatsTextBottom").addClass("timerMain"); - $("#liveStatsMini").addClass("timerMain"); + qs("#barTimerProgress")?.addClass("timerMain"); + qs("#liveStatsTextTop")?.addClass("timerMain"); + qs("#liveStatsTextBottom")?.addClass("timerMain"); + qs("#liveStatsMini")?.addClass("timerMain"); } else if (value === "sub") { - $("#barTimerProgress").addClass("timerSub"); - $("#liveStatsTextTop").addClass("timerSub"); - $("#liveStatsTextBottom").addClass("timerSub"); - $("#liveStatsMini").addClass("timerSub"); + qs("#barTimerProgress")?.addClass("timerSub"); + qs("#liveStatsTextTop")?.addClass("timerSub"); + qs("#liveStatsTextBottom")?.addClass("timerSub"); + qs("#liveStatsMini")?.addClass("timerSub"); } else if (value === "text") { - $("#barTimerProgress").addClass("timerText"); - $("#liveStatsTextTop").addClass("timerText"); - $("#liveStatsTextBottom").addClass("timerText"); - $("#liveStatsMini").addClass("timerText"); + qs("#barTimerProgress")?.addClass("timerText"); + qs("#liveStatsTextTop")?.addClass("timerText"); + qs("#liveStatsTextBottom")?.addClass("timerText"); + qs("#liveStatsMini")?.addClass("timerText"); } } function showHideTestRestartButton(showHide: boolean): void { if (showHide) { - $(".pageTest #restartTestButton").removeClass("hidden"); + qs(".pageTest #restartTestButton")?.removeClass("hidden"); } else { - $(".pageTest #restartTestButton").addClass("hidden"); + qs(".pageTest #restartTestButton")?.addClass("hidden"); } } @@ -1642,17 +1694,17 @@ export function getActiveWordTopAndHeightWithDifferentData(data: string): { if (!activeWord) throw new Error("No active word element found"); const nodes = []; - for (let i = activeWord.children.length; i < data.length; i++) { + for (let i = activeWord.getChildren().length; i < data.length; i++) { const tempLetter = document.createElement("letter"); const displayData = data[i] === " " ? "_" : data[i]; tempLetter.textContent = displayData as string; nodes.push(tempLetter); } - activeWord.append(...nodes); + activeWord.append(nodes); - const top = activeWord.offsetTop; - const height = activeWord.offsetHeight; + const top = activeWord.getOffsetTop(); + const height = activeWord.getOffsetHeight(); for (const node of nodes) { node.remove(); } @@ -1793,7 +1845,7 @@ export async function afterTestWordChange( // because we need to delete newline, beforenewline and afternewline elements which dont have wordindex attributes // we need to do this loop thingy and delete all elements after the active word let deleteElements = false; - for (const child of wordsEl.children) { + for (const child of wordsEl.getChildren()) { if (deleteElements) { child.remove(); continue; @@ -1820,8 +1872,8 @@ export function onTestStart(): void { } export function onTestRestart(source: "testPage" | "resultPage"): void { - $("#result").addClass("hidden"); - $("#typingTest").css("opacity", 0).removeClass("hidden"); + qs("#result")?.addClass("hidden"); + qs("#typingTest")?.setStyle({ opacity: "0" }).removeClass("hidden"); getInputElement().style.left = "0"; TestConfig.show(); Focus.set(false); @@ -1886,7 +1938,7 @@ export function onTestFinish(): void { } } -$(".pageTest #copyWordsListButton").on("click", async () => { +qs(".pageTest #copyWordsListButton")?.on("click", async () => { let words; if (Config.mode === "zen") { words = TestInput.input.getHistory().join(" "); @@ -1899,7 +1951,7 @@ $(".pageTest #copyWordsListButton").on("click", async () => { await copyToClipboard(words); }); -$(".pageTest #copyMissedWordsListButton").on("click", async () => { +qs(".pageTest #copyMissedWordsListButton")?.on("click", async () => { let words; if (Config.mode === "zen") { words = TestInput.input.getHistory().join(" "); @@ -1921,53 +1973,25 @@ async function copyToClipboard(content: string): Promise { } } -$(".pageTest #toggleBurstHeatmap").on("click", async () => { +qs(".pageTest #toggleBurstHeatmap")?.on("click", async () => { setConfig("burstHeatmap", !Config.burstHeatmap); ResultWordHighlight.destroy(); }); -$(".pageTest #resultWordsHistory").on("mouseleave", ".words .word", () => { - $(".wordInputHighlight").remove(); -}); - -$(".pageTest #result #wpmChart").on("mouseleave", () => { +qs(".pageTest #result #wpmChart")?.on("mouseleave", () => { ResultWordHighlight.setIsHoverChart(false); ResultWordHighlight.clear(); }); -$(".pageTest #result #wpmChart").on("mouseenter", () => { +qs(".pageTest #result #wpmChart")?.on("mouseenter", () => { ResultWordHighlight.setIsHoverChart(true); }); -$(".pageTest #resultWordsHistory").on("mouseenter", ".words .word", (e) => { - if (TestState.resultVisible) { - const input = $(e.currentTarget).attr("input"); - const burst = parseInt($(e.currentTarget).attr("burst") as string); - if (input !== undefined) { - $(e.currentTarget).append( - `
-
- ${input - .replace(/\t/g, "_") - .replace(/\n/g, "_") - .replace(//g, ">")} -
-
- ${isNaN(burst) || burst >= 1000 ? "Infinite" : Format.typingSpeed(burst, { showDecimalPlaces: false })} - ${Config.typingSpeedUnit} -
-
`, - ); - } - } -}); - addEventListener("resize", () => { ResultWordHighlight.destroy(); }); -$("#wordsInput").on("focus", (e) => { +qs("#wordsInput")?.on("focus", (e) => { if (!isInputElementFocused()) return; if (!TestState.resultVisible && Config.showOutOfFocusWarning) { OutOfFocus.hide(); @@ -1975,18 +1999,18 @@ $("#wordsInput").on("focus", (e) => { Caret.show(true); }); -$("#wordsInput").on("focusout", () => { +qs("#wordsInput")?.on("focusout", () => { if (!isInputElementFocused()) { OutOfFocus.show(); } Caret.hide(); }); -$(".pageTest").on("click", "#showWordHistoryButton", () => { +qs(".pageTest")?.onChild("click", "#showWordHistoryButton", () => { void toggleResultWords(); }); -$("#wordsWrapper").on("click", () => { +qs("#wordsWrapper")?.on("click", () => { focusWords(); }); diff --git a/frontend/src/ts/utils/dom.ts b/frontend/src/ts/utils/dom.ts index 7092aa549a09..0df6f0db4c8f 100644 --- a/frontend/src/ts/utils/dom.ts +++ b/frontend/src/ts/utils/dom.ts @@ -272,9 +272,8 @@ export class ElementWithUtils { /** * Make element visible by scrolling the element's ancestor containers */ - scrollIntoView(options: ScrollIntoViewOptions): this { + scrollIntoView(options?: ScrollIntoViewOptions): this { this.native.scrollIntoView(options); - return this; } @@ -285,6 +284,11 @@ export class ElementWithUtils { if (Array.isArray(className)) { this.native.classList.add(...className); } else { + if (className.includes(" ")) { + return this.addClass( + className.split(" ").filter((cn) => cn.length > 0), + ); + } this.native.classList.add(className); } return this; @@ -297,6 +301,11 @@ export class ElementWithUtils { if (Array.isArray(className)) { this.native.classList.remove(...className); } else { + if (className.includes(" ")) { + return this.removeClass( + className.split(" ").filter((cn) => cn.length > 0), + ); + } this.native.classList.remove(className); } return this; @@ -306,6 +315,12 @@ export class ElementWithUtils { * Check if the element has a class */ hasClass(className: string): boolean { + if (className.includes(" ")) { + return className + .split(" ") + .filter((cn) => cn.length > 0) + .every((cn) => this.hasClass(cn)); + } return this.native.classList.contains(className); } @@ -717,6 +732,34 @@ export class ElementWithUtils { return this; } + /** + * Get the element's height + margin + */ + + getOuterHeight(): number { + const style = getComputedStyle(this.native); + + return ( + this.native.getBoundingClientRect().height + + parseFloat(style.marginTop) + + parseFloat(style.marginBottom) + ); + } + + /** + * Get The element's width + margin + */ + + getOuterWidth(): number { + const style = getComputedStyle(this.native); + + return ( + this.native.getBoundingClientRect().width + + parseFloat(style.marginLeft) + + parseFloat(style.marginRight) + ); + } + /** * Get the element's width */ @@ -745,6 +788,24 @@ export class ElementWithUtils { return this.native.offsetLeft; } + /** + * Get the element's children wrapped in ElementWithUtils instances. + * + * Note: This method returns a new array of wrappers, but each wrapper maintains + * a reference to the actual DOM element. Any operations performed on the returned + * children (e.g., addClass, remove, setHtml) will modify the actual DOM elements + * and reflect their live DOM state. + * + * @returns An ElementsWithUtils array containing wrapped child elements + */ + getChildren(): ElementsWithUtils { + const children = Array.from(this.native.children); + const convertedChildren = new ElementsWithUtils( + ...children.map((child) => new ElementWithUtils(child as HTMLElement)), + ); + return convertedChildren; + } + /** * Animate the element using Anime.js * @param animationParams The Anime.js animation parameters @@ -855,8 +916,9 @@ export class ElementWithUtils { /** * Focus the element */ - focus(): void { - this.native.focus(); + focus(options?: FocusOptions): this { + this.native.focus(options); + return this; } /** From 0d089241c168af42e7b1a36eb3b2de7d06c77faa Mon Sep 17 00:00:00 2001 From: Leonabcd123 <156839416+Leonabcd123@users.noreply.github.com> Date: Wed, 21 Jan 2026 22:07:28 +0200 Subject: [PATCH 2/7] docs(jquery): Remove dom utils check (@Leonabcd123) (#7411) --- .github/pull_request_template.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 44a5bb933664..951dca15f0b3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,8 +4,6 @@ ### Checks -- [ ] Adding/modifying Typescript code? - - [ ] I have used `qs`, `qsa` or `qsr` instead of JQuery selectors. - [ ] Adding quotes? - [ ] Make sure to include translations for the quotes in the description (or another comment) so we can verify their content. - [ ] Adding a language? From 11212962eca181de489853f1c9a3eee34bbcd3c3 Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 21 Jan 2026 21:15:26 +0100 Subject: [PATCH 3/7] chore: remove last bits of jquery (@miodec) (#7410) image --- docs/CONTRIBUTING_ADVANCED.md | 2 -- .../__tests__/__harness__/setup-jquery.ts | 6 ---- frontend/__tests__/utils/dom.jsdom-spec.ts | 5 --- frontend/package.json | 2 -- frontend/src/ts/index.ts | 3 -- frontend/src/ts/test/test-logic.ts | 6 ---- frontend/src/ts/utils/dom.ts | 6 ++-- frontend/src/ts/utils/misc.ts | 34 ------------------- frontend/tsconfig.json | 2 +- frontend/vite-plugins/jquery-inject.ts | 29 ---------------- frontend/vite.config.ts | 6 ---- frontend/vitest.config.ts | 3 -- pnpm-lock.yaml | 23 ------------- 13 files changed, 3 insertions(+), 124 deletions(-) delete mode 100644 frontend/__tests__/__harness__/setup-jquery.ts delete mode 100644 frontend/vite-plugins/jquery-inject.ts diff --git a/docs/CONTRIBUTING_ADVANCED.md b/docs/CONTRIBUTING_ADVANCED.md index 5eb1f71220f5..b9b855a986b2 100644 --- a/docs/CONTRIBUTING_ADVANCED.md +++ b/docs/CONTRIBUTING_ADVANCED.md @@ -150,8 +150,6 @@ If you are on a UNIX system and you get a spawn error, run npm with `sudo`. Code formatting is enforced by [Prettier](https://prettier.io/docs/en/install.html), which automatically runs every time you make a commit. -We are currently in the process of converting from JQuery to vanilla JS. When submitting new code, please use the `qs`, `qsa` and `qsr` helper functions. These return a class with a lot of JQuery-like methods. You can read how they work and import them from `frontend/src/ts/utils/dom.ts`. - For guidelines on commit messages, adding themes, languages, or quotes, please refer to [CONTRIBUTING.md](./CONTRIBUTING.md). Following these guidelines will increase the chances of getting your change accepted. ## Questions diff --git a/frontend/__tests__/__harness__/setup-jquery.ts b/frontend/__tests__/__harness__/setup-jquery.ts deleted file mode 100644 index ae671c34afdb..000000000000 --- a/frontend/__tests__/__harness__/setup-jquery.ts +++ /dev/null @@ -1,6 +0,0 @@ -import $ from "jquery"; - -//@ts-expect-error add to global -global["$"] = $; -//@ts-expect-error add to global -global["jQuery"] = $; diff --git a/frontend/__tests__/utils/dom.jsdom-spec.ts b/frontend/__tests__/utils/dom.jsdom-spec.ts index 238623106bad..2f2797912172 100644 --- a/frontend/__tests__/utils/dom.jsdom-spec.ts +++ b/frontend/__tests__/utils/dom.jsdom-spec.ts @@ -26,7 +26,6 @@ describe("dom", () => { handler({ target: e.target, childTarget: e.childTarget, - //@ts-expect-error will be added later, check TODO on the ChildEvent currentTarget: e.currentTarget, }), ); @@ -130,10 +129,6 @@ describe("dom", () => { await userEvent.click(clickTarget); //THEN - - //This is the same behavior as jQuery `.on` with selector. - //The handler will be called two times, - //It does NOT call on the
or the parent element itself expect(handler).toHaveBeenCalledTimes(2); //First call is for childTarget inner2 (grand child of parent) diff --git a/frontend/package.json b/frontend/package.json index ac913df2bbf3..bac5f695da18 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,7 +47,6 @@ "hangul-js": "0.2.6", "howler": "2.2.3", "idb": "8.0.3", - "jquery": "3.7.1", "konami": "1.7.0", "lz-ts": "1.1.2", "modern-screenshot": "4.6.5", @@ -73,7 +72,6 @@ "@types/chartjs-plugin-trendline": "1.0.1", "@types/damerau-levenshtein": "1.0.0", "@types/howler": "2.2.7", - "@types/jquery": "3.5.14", "@types/node": "24.9.1", "@types/object-hash": "3.0.6", "@types/subset-font": "1.4.3", diff --git a/frontend/src/ts/index.ts b/frontend/src/ts/index.ts index f26714c95037..f26d9a3fd3d6 100644 --- a/frontend/src/ts/index.ts +++ b/frontend/src/ts/index.ts @@ -97,9 +97,6 @@ addToGlobal({ }); if (isDevEnvironment()) { - void import("jquery").then((jq) => { - addToGlobal({ $: jq.default }); - }); void getDevOptionsModal().then((module) => { module.appendButton(); }); diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 0b316305bd3a..83b8bb5b109e 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -131,12 +131,6 @@ type RestartOptions = { noAnim?: boolean; }; -// withSameWordset = false, -// _?: boolean, // this is nosave and should be renamed to nosave when needed -// event?: JQuery.KeyDownEvent, -// practiseMissed = false, -// noAnim = false - export function restart(options = {} as RestartOptions): void { const defaultOptions = { withSameWordset: false, diff --git a/frontend/src/ts/utils/dom.ts b/frontend/src/ts/utils/dom.ts index 0df6f0db4c8f..fcad7aa90a12 100644 --- a/frontend/src/ts/utils/dom.ts +++ b/frontend/src/ts/utils/dom.ts @@ -153,11 +153,9 @@ type ElementWithValue = 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 & { +export type OnChildEvent = T & { /** - * target element matching the selector. This emulates the behavior of `currentTarget` in jQuery events registered with `.on(events, selector, handler)` + * target element matching the selector. */ childTarget: EventTarget | null; }; diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 988e343e32a5..8666e9eece3a 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -409,40 +409,6 @@ export function isAnyPopupVisible(): boolean { return popupVisible; } -export type JQueryEasing = - | "linear" - | "swing" - | "easeInSine" - | "easeOutSine" - | "easeInOutSine" - | "easeInQuad" - | "easeOutQuad" - | "easeInOutQuad" - | "easeInCubic" - | "easeOutCubic" - | "easeInOutCubic" - | "easeInQuart" - | "easeOutQuart" - | "easeInOutQuart" - | "easeInQuint" - | "easeOutQuint" - | "easeInOutQuint" - | "easeInExpo" - | "easeOutExpo" - | "easeInOutExpo" - | "easeInCirc" - | "easeOutCirc" - | "easeInOutCirc" - | "easeInBack" - | "easeOutBack" - | "easeInOutBack" - | "easeInElastic" - | "easeOutElastic" - | "easeInOutElastic" - | "easeInBounce" - | "easeOutBounce" - | "easeInOutBounce"; - export async function promiseAnimate( el: HTMLElement | string, options: AnimationParams, diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index ddb6ee57af50..467adc1eec01 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@monkeytype/typescript-config/base.json", "compilerOptions": { "lib": ["ESNext", "DOM", "DOM.Iterable"], - "types": ["vite/client", "node", "jquery"], + "types": ["vite/client", "node"], "moduleResolution": "Bundler", "module": "ESNext", "allowUmdGlobalAccess": true, diff --git a/frontend/vite-plugins/jquery-inject.ts b/frontend/vite-plugins/jquery-inject.ts deleted file mode 100644 index 6577cbcdcc60..000000000000 --- a/frontend/vite-plugins/jquery-inject.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Plugin } from "vite"; -import MagicString from "magic-string"; - -export function jqueryInject(): Plugin { - return { - name: "simple-jquery-inject", - async transform(src: string, id: string) { - if (id.endsWith(".ts")) { - //check if file has a jQuery or $() call - if (/(?:jQuery|\$)\([^)]*\)/.test(src)) { - const s = new MagicString(src); - - //if file has "use strict"; at the top, add it below that line, if not, add it at the very top - if (src.startsWith(`"use strict";`)) { - s.appendRight(12, `\nimport $ from "jquery";`); - } else { - s.prepend(`import $ from "jquery";`); - } - - return { - code: s.toString(), - map: s.generateMap({ hires: true, source: id }), - }; - } - } - return; - }, - }; -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index a00f5ffcefbd..d56546ce1808 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -18,7 +18,6 @@ import { envConfig } from "./vite-plugins/env-config"; import { languageHashes } from "./vite-plugins/language-hashes"; import { minifyJson } from "./vite-plugins/minify-json"; import { versionFile } from "./vite-plugins/version-file"; -import { jqueryInject } from "./vite-plugins/jquery-inject"; import { oxlintChecker } from "./vite-plugins/oxlint-checker"; import Inspect from "vite-plugin-inspect"; import { ViteMinifyPlugin } from "vite-plugin-minify"; @@ -63,7 +62,6 @@ export default defineConfig(({ mode }): UserConfig => { root: "src", publicDir: "../static", optimizeDeps: { - include: ["jquery"], exclude: ["@fortawesome/fontawesome-free"], }, }; @@ -88,7 +86,6 @@ function getPlugins({ typeAware: true, overlay: isDevelopment, }), - jqueryInject(), injectHTML(), tailwindcss(), solidPlugin(), @@ -262,9 +259,6 @@ function getBuildOptions({ if (id.includes("@sentry")) { return "vendor-sentry"; } - if (id.includes("jquery")) { - return "vendor-jquery"; - } if (id.includes("@firebase")) { return "vendor-firebase"; } diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 72b4aedbfcfb..145cb0cbbd9e 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -18,7 +18,6 @@ export const projects: UserWorkspaceConfig[] = [ environment: "happy-dom", globalSetup: "__tests__/global-setup.ts", setupFiles: [ - "__tests__/__harness__/setup-jquery.ts", "__tests__/__harness__/mock-dom.ts", "__tests__/__harness__/mock-firebase.ts", "__tests__/__harness__/mock-env-config.ts", @@ -32,7 +31,6 @@ export const projects: UserWorkspaceConfig[] = [ include: ["__tests__/**/*.jsdom-spec.ts"], environment: "jsdom", globalSetup: "__tests__/global-setup.ts", - setupFiles: ["__tests__/__harness__/setup-jquery.ts"], }, plugins, }, @@ -46,7 +44,6 @@ export const projects: UserWorkspaceConfig[] = [ environment: "jsdom", globalSetup: "__tests__/global-setup.ts", setupFiles: [ - "__tests__/__harness__/setup-jquery.ts", "__tests__/__harness__/setup-jsx.ts", "__tests__/__harness__/mock-dom.ts", ], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 61dd6e740737..4cafa9d41fa5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -333,9 +333,6 @@ importers: idb: specifier: 8.0.3 version: 8.0.3 - jquery: - specifier: 3.7.1 - version: 3.7.1 konami: specifier: 1.7.0 version: 1.7.0 @@ -406,9 +403,6 @@ importers: '@types/howler': specifier: 2.2.7 version: 2.2.7 - '@types/jquery': - specifier: 3.5.14 - version: 3.5.14 '@types/node': specifier: 24.9.1 version: 24.9.1 @@ -3551,9 +3545,6 @@ packages: '@types/ioredis@4.28.10': resolution: {integrity: sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ==} - '@types/jquery@3.5.14': - resolution: {integrity: sha512-X1gtMRMbziVQkErhTQmSe2jFwwENA/Zr+PprCkF63vFq+Yt5PZ4AlKqgmeNlwgn7dhsXEK888eIW2520EpC+xg==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3626,9 +3617,6 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - '@types/sizzle@2.3.8': - resolution: {integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==} - '@types/ssh2-streams@0.1.12': resolution: {integrity: sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==} @@ -6527,9 +6515,6 @@ packages: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - jquery@3.7.1: - resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} - js-beautify@1.15.1: resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==} engines: {node: '>=14'} @@ -13274,10 +13259,6 @@ snapshots: dependencies: '@types/node': 24.9.1 - '@types/jquery@3.5.14': - dependencies: - '@types/sizzle': 2.3.8 - '@types/json-schema@7.0.15': {} '@types/jsonwebtoken@9.0.6': @@ -13350,8 +13331,6 @@ snapshots: '@types/node': 24.9.1 '@types/send': 0.17.4 - '@types/sizzle@2.3.8': {} - '@types/ssh2-streams@0.1.12': dependencies: '@types/node': 24.9.1 @@ -16909,8 +16888,6 @@ snapshots: joycon@3.1.1: {} - jquery@3.7.1: {} - js-beautify@1.15.1: dependencies: config-chain: 1.1.13 From ba8275699ccdb48d18288363128ce4fde4bd436e Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 21 Jan 2026 21:42:59 +0100 Subject: [PATCH 4/7] refactor: add config store (@fehmer) (#7382) add config store as copy of `Config.config`. Keeps in sync using events and writes are passed to the `Config.setConfig` --------- Co-authored-by: Miodec --- .../src/ts/components/pages/AboutPage.tsx | 6 ++--- frontend/src/ts/config.ts | 1 + frontend/src/ts/signals/config.ts | 23 +++++++++++++------ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/frontend/src/ts/components/pages/AboutPage.tsx b/frontend/src/ts/components/pages/AboutPage.tsx index 30b54653abf3..16e54aa72c13 100644 --- a/frontend/src/ts/components/pages/AboutPage.tsx +++ b/frontend/src/ts/components/pages/AboutPage.tsx @@ -2,7 +2,7 @@ import { intervalToDuration } from "date-fns"; import { createResource, For, JSXElement, Show } from "solid-js"; import Ape from "../../ape"; -import { getAds } from "../../signals/config"; +import { getConfig } from "../../signals/config"; import { getActivePage } from "../../signals/core"; import { showModal } from "../../stores/modals"; import { getContributorsList, getSupportersList } from "../../utils/json-data"; @@ -225,7 +225,7 @@ export function AboutPage(): JSXElement {
- +