From 9a9b6d72ae4852d1b5160f4295bbb427c198d528 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 12 Dec 2025 13:28:49 +0100 Subject: [PATCH 1/4] refactor: mock ElementWithUtils (@fehmer) (#7224) --- frontend/__tests__/setup-tests.ts | 68 ++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/frontend/__tests__/setup-tests.ts b/frontend/__tests__/setup-tests.ts index 43b2255ae136..6828d27727c0 100644 --- a/frontend/__tests__/setup-tests.ts +++ b/frontend/__tests__/setup-tests.ts @@ -1,5 +1,7 @@ import { vi } from "vitest"; import $ from "jquery"; +import { ElementWithUtils } from "../src/ts/utils/dom"; + //@ts-expect-error add to globl global["$"] = $; //@ts-expect-error add to globl @@ -19,33 +21,51 @@ vi.mock("../src/ts/firebase", () => ({ })); vi.mock("../src/ts/utils/dom", () => { - const createMockElement = (): any => { - const mock = { - qsr: vi.fn(), + const createMockElement = (): ElementWithUtils => { + return { + disable: vi.fn().mockReturnThis(), + enable: vi.fn().mockReturnThis(), + isDisabled: vi.fn().mockReturnValue(false), + getAttribute: vi.fn(), + hasAttribute: vi.fn().mockReturnValue(false), + setAttribute: vi.fn().mockReturnThis(), + removeAttribute: vi.fn().mockReturnThis(), + isChecked: vi.fn().mockReturnValue(false), + hide: vi.fn().mockReturnThis(), + show: vi.fn().mockReturnThis(), + addClass: vi.fn().mockReturnThis(), + removeClass: vi.fn().mockReturnThis(), + hasClass: vi.fn().mockReturnValue(false), + toggleClass: vi.fn().mockReturnThis(), + on: vi.fn().mockReturnThis(), + onChild: vi.fn().mockReturnThis(), + setHtml: vi.fn().mockReturnThis(), + setText: vi.fn().mockReturnThis(), + remove: vi.fn(), + setStyle: vi.fn().mockReturnThis(), + getStyle: vi.fn().mockReturnValue({}), + isFocused: vi.fn().mockReturnValue(false), qs: vi.fn().mockReturnValue(null), - find: vi.fn(), - addClass: vi.fn(), - removeClass: vi.fn(), - hide: vi.fn(), - show: vi.fn(), - setText: vi.fn(), - prependHtml: vi.fn(), - empty: vi.fn(), - appendHtml: vi.fn(), + qsr: vi.fn().mockImplementation(() => createMockElement()), + qsa: vi.fn().mockReturnValue([]), + empty: vi.fn().mockReturnThis(), + appendHtml: vi.fn().mockReturnThis(), + append: vi.fn().mockReturnThis(), + prependHtml: vi.fn().mockReturnThis(), + dispatch: vi.fn().mockReturnThis(), + offset: vi.fn().mockReturnValue({ top: 0, left: 0 }), + wrapWith: vi.fn().mockImplementation(() => createMockElement()), + setValue: vi.fn().mockReturnThis(), + getValue: vi.fn().mockReturnValue(""), + getParent: vi.fn().mockImplementation(() => createMockElement()), + replaceWith: vi.fn().mockReturnThis(), + getOffsetWidth: vi.fn().mockReturnValue(0), + getOffsetHeight: vi.fn().mockReturnValue(0), + getOffsetTop: vi.fn().mockReturnValue(0), + getOffsetLeft: vi.fn().mockReturnValue(0), + animate: vi.fn().mockResolvedValue(null), native: document.createElement("div"), }; - - // Make chainable methods return the mock itself - mock.qsr.mockImplementation(() => createMockElement()); - mock.addClass.mockReturnValue(mock); - mock.removeClass.mockReturnValue(mock); - mock.hide.mockReturnValue(mock); - mock.show.mockReturnValue(mock); - mock.setText.mockReturnValue(mock); - mock.prependHtml.mockReturnValue(mock); - mock.empty.mockReturnValue(mock); - - return mock; }; return { From 735740da98fc239c8d8548d92d17a28084a67668 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 12 Dec 2025 14:44:11 +0100 Subject: [PATCH 2/4] refactor: change config event params to an options object (@miodec) (#7227) Also makes the types stronger on the event. Also cleans up unused events. --------- Co-authored-by: Christian Fehmer --- frontend/__tests__/root/config.spec.ts | 69 +++++++------------ frontend/src/ts/commandline/lists/themes.ts | 4 +- frontend/src/ts/config.ts | 37 +++++----- frontend/src/ts/controllers/ad-controller.ts | 10 +-- .../ts/controllers/challenge-controller.ts | 4 +- .../src/ts/controllers/chart-controller.ts | 6 +- .../src/ts/controllers/quotes-controller.ts | 2 +- .../src/ts/controllers/sound-controller.ts | 8 +-- .../src/ts/controllers/theme-controller.ts | 34 ++++----- .../ts/elements/custom-background-filter.ts | 6 +- frontend/src/ts/elements/keymap.ts | 22 +++--- frontend/src/ts/elements/modes-notice.ts | 4 +- .../src/ts/elements/settings/theme-picker.ts | 6 +- frontend/src/ts/observables/config-event.ts | 34 ++++----- frontend/src/ts/pages/account.ts | 4 +- frontend/src/ts/pages/leaderboards.ts | 4 +- frontend/src/ts/pages/settings.ts | 22 +++--- frontend/src/ts/test/caret.ts | 6 +- frontend/src/ts/test/live-acc.ts | 4 +- frontend/src/ts/test/live-burst.ts | 4 +- frontend/src/ts/test/live-speed.ts | 4 +- frontend/src/ts/test/monkey.ts | 4 +- frontend/src/ts/test/pace-caret.ts | 6 +- frontend/src/ts/test/practise-words.ts | 4 +- frontend/src/ts/test/result.ts | 4 +- frontend/src/ts/test/test-config.ts | 16 ++--- frontend/src/ts/test/test-logic.ts | 28 +++----- frontend/src/ts/test/test-ui.ts | 44 ++++++------ frontend/src/ts/test/timer-progress.ts | 4 +- frontend/src/ts/test/tts.ts | 12 ++-- frontend/src/ts/ui.ts | 8 +-- 31 files changed, 195 insertions(+), 229 deletions(-) diff --git a/frontend/__tests__/root/config.spec.ts b/frontend/__tests__/root/config.spec.ts index d209eadbebed..1f20478d3178 100644 --- a/frontend/__tests__/root/config.spec.ts +++ b/frontend/__tests__/root/config.spec.ts @@ -145,26 +145,26 @@ describe("Config", () => { Config.setConfig("confidenceMode", "max"); //THEN - expect(dispatchConfigEventMock).not.toHaveBeenCalledWith( - "freedomMode", - false, - true, - true, - ); + expect(dispatchConfigEventMock).not.toHaveBeenCalledWith({ + key: "freedomMode", + newValue: false, + nosave: true, + previousValue: true, + }); - expect(dispatchConfigEventMock).toHaveBeenCalledWith( - "stopOnError", - "off", - false, - "letter", - ); + expect(dispatchConfigEventMock).toHaveBeenCalledWith({ + key: "stopOnError", + newValue: "off", + nosave: false, + previousValue: "letter", + }); - expect(dispatchConfigEventMock).toHaveBeenCalledWith( - "confidenceMode", - "max", - false, - "off", - ); + expect(dispatchConfigEventMock).toHaveBeenCalledWith({ + key: "confidenceMode", + newValue: "max", + nosave: false, + previousValue: "off", + }); }); it("saves to localstorage if nosave=false", async () => { @@ -186,12 +186,6 @@ describe("Config", () => { //hide loading expect(accountButtonLoadingMock).toHaveBeenNthCalledWith(2, false); - - //send event - expect(dispatchConfigEventMock).toHaveBeenCalledWith( - "saveToLocalStorage", - expect.stringContaining("numbers"), - ); }); it("saves configOverride values to localstorage if nosave=false", async () => { @@ -210,16 +204,6 @@ describe("Config", () => { minWpmCustomSpeed: 120, minWpm: "custom", }); - - //send event - expect(dispatchConfigEventMock).toHaveBeenCalledWith( - "saveToLocalStorage", - expect.stringContaining("minWpmCustomSpeed"), - ); - expect(dispatchConfigEventMock).toHaveBeenCalledWith( - "saveToLocalStorage", - expect.stringContaining("minWpm"), - ); }); it("does not save to localstorage if nosave=true", async () => { @@ -236,11 +220,6 @@ describe("Config", () => { expect(accountButtonLoadingMock).not.toHaveBeenCalled(); expect(dbSaveConfigMock).not.toHaveBeenCalled(); - - expect(dispatchConfigEventMock).not.toHaveBeenCalledWith( - "saveToLocalStorage", - expect.any(String), - ); }); it("dispatches event on set", () => { @@ -252,12 +231,12 @@ describe("Config", () => { //THEN - expect(dispatchConfigEventMock).toHaveBeenCalledWith( - "numbers", - true, - true, - false, - ); + expect(dispatchConfigEventMock).toHaveBeenCalledWith({ + key: "numbers", + newValue: true, + nosave: true, + previousValue: false, + }); }); it("triggers resize if property is set", () => { diff --git a/frontend/src/ts/commandline/lists/themes.ts b/frontend/src/ts/commandline/lists/themes.ts index dc2e765f6a83..1300b1356b02 100644 --- a/frontend/src/ts/commandline/lists/themes.ts +++ b/frontend/src/ts/commandline/lists/themes.ts @@ -76,8 +76,8 @@ export function update(themes: Theme[]): void { } // subscribe to theme-related config events to update the theme command list -ConfigEvent.subscribe((eventKey, _eventValue) => { - if (eventKey === "favThemes") { +ConfigEvent.subscribe(({ key }) => { + if (key === "favThemes") { // update themes list when favorites change try { update(ThemesList); diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index 86ab103c5a6c..a14d8a23bf48 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -66,8 +66,6 @@ function saveToLocalStorage( configToSend[key] = config[key]; saveToDatabase(); } - const localToSaveStringified = JSON.stringify(config); - ConfigEvent.dispatch("saveToLocalStorage", localToSaveStringified); } export function saveFullConfigToLocalStorage(noDbCheck = false): void { @@ -78,8 +76,6 @@ export function saveFullConfigToLocalStorage(noDbCheck = false): void { void DB.saveConfig(config); AccountButton.loading(false); } - const stringified = JSON.stringify(config); - ConfigEvent.dispatch("saveToLocalStorage", stringified); } function isConfigChangeBlocked(): boolean { @@ -92,9 +88,9 @@ function isConfigChangeBlocked(): boolean { return false; } -export function setConfig( +export function setConfig( key: T, - value: ConfigSchemas.Config[T], + value: Config[T], nosave: boolean = false, ): boolean { const metadata = configMetadata[key] as ConfigMetadataObject[T]; @@ -183,7 +179,14 @@ export function setConfig( config[key] = value; if (!nosave) saveToLocalStorage(key, nosave); - ConfigEvent.dispatch(key, value, nosave, previousValue); + + // @ts-expect-error i can't figure this out + ConfigEvent.dispatch({ + key: key, + newValue: value, + nosave, + previousValue: previousValue as Config[T], + }); if (metadata.triggerResize && !nosave) { triggerResize(); @@ -201,6 +204,8 @@ export function toggleFunbox(funbox: FunboxName, nosave?: boolean): boolean { return false; } + const previousValue = config.funbox; + let newConfig: FunboxName[] = config.funbox; if (newConfig.includes(funbox)) { @@ -216,7 +221,12 @@ export function toggleFunbox(funbox: FunboxName, nosave?: boolean): boolean { config.funbox = newConfig; saveToLocalStorage("funbox", nosave); - ConfigEvent.dispatch("funbox", config.funbox); + ConfigEvent.dispatch({ + key: "funbox", + newValue: config.funbox, + nosave, + previousValue, + }); return true; } @@ -248,7 +258,7 @@ export async function applyConfig( //migrate old values if needed, remove additional keys and merge with default config const fullConfig: Config = migrateConfig(partialConfig); - ConfigEvent.dispatch("fullConfigChange"); + ConfigEvent.dispatch({ key: "fullConfigChange" }); const defaultConfig = getDefaultConfig(); for (const key of typedKeys(fullConfig)) { @@ -276,14 +286,7 @@ export async function applyConfig( saveToLocalStorage(key); } - ConfigEvent.dispatch( - "configApplied", - undefined, - undefined, - undefined, - config, - ); - ConfigEvent.dispatch("fullConfigChangeFinished"); + ConfigEvent.dispatch({ key: "fullConfigChangeFinished" }); } export async function resetConfig(): Promise { diff --git a/frontend/src/ts/controllers/ad-controller.ts b/frontend/src/ts/controllers/ad-controller.ts index 1029b0e92451..a612a81f0fe5 100644 --- a/frontend/src/ts/controllers/ad-controller.ts +++ b/frontend/src/ts/controllers/ad-controller.ts @@ -300,14 +300,14 @@ window.addEventListener("resize", () => { debouncedBreakpoint2Update(); }); -ConfigEvent.subscribe((event, value) => { - if (event === "ads") { - if (value === "off") { +ConfigEvent.subscribe(({ key, newValue }) => { + if (key === "ads") { + if (newValue === "off") { removeAll(); - } else if (value === "result") { + } else if (newValue === "result") { removeSellout(); removeOn(); - } else if (value === "on") { + } else if (newValue === "on") { removeSellout(); } } diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index 1f42debd143b..0e001099cd60 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -337,7 +337,7 @@ export async function setup(challengeName: string): Promise { } } -ConfigEvent.subscribe((eventKey) => { +ConfigEvent.subscribe(({ key }) => { if ( [ "difficulty", @@ -354,7 +354,7 @@ ConfigEvent.subscribe((eventKey) => { "keymapMode", "keymapLayout", "layout", - ].includes(eventKey) + ].includes(key) ) { clearActive(); } diff --git a/frontend/src/ts/controllers/chart-controller.ts b/frontend/src/ts/controllers/chart-controller.ts index 2a8c1e8ce243..48759b8b6a5c 100644 --- a/frontend/src/ts/controllers/chart-controller.ts +++ b/frontend/src/ts/controllers/chart-controller.ts @@ -1432,12 +1432,12 @@ export function updateAllChartColors(): void { void miniResult.updateColors(); } -ConfigEvent.subscribe((eventKey, eventValue) => { - if (eventKey === "accountChart" && ActivePage.get() === "account") { +ConfigEvent.subscribe(({ key, newValue }) => { + if (key === "accountChart" && ActivePage.get() === "account") { updateResults(); updateAccuracy(); updateAverage10(); updateAverage100(); } - if (eventKey === "fontFamily") setDefaultFontFamily(eventValue as string); + if (key === "fontFamily") setDefaultFontFamily(newValue); }); diff --git a/frontend/src/ts/controllers/quotes-controller.ts b/frontend/src/ts/controllers/quotes-controller.ts index 8c095b3043ec..a15d3002251c 100644 --- a/frontend/src/ts/controllers/quotes-controller.ts +++ b/frontend/src/ts/controllers/quotes-controller.ts @@ -251,7 +251,7 @@ class QuotesController { const quoteController = new QuotesController(); -subscribe((key, newValue) => { +subscribe(({ key, newValue }) => { if (key === "quoteLength") { quoteController.updateQuoteQueue(newValue as number[]); } diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts index c1dd3547dda0..07b9befa6e30 100644 --- a/frontend/src/ts/controllers/sound-controller.ts +++ b/frontend/src/ts/controllers/sound-controller.ts @@ -736,9 +736,9 @@ function setVolume(val: number): void { } } -ConfigEvent.subscribe((eventKey, eventValue) => { - if (eventKey === "playSoundOnClick" && eventValue !== "off") void init(); - if (eventKey === "soundVolume") { - setVolume(parseFloat(eventValue as string)); +ConfigEvent.subscribe(({ key, newValue }) => { + if (key === "playSoundOnClick" && newValue !== "off") void init(); + if (key === "soundVolume") { + setVolume(newValue); } }); diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index 0a6817f15257..dcce1d1b2033 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -477,11 +477,11 @@ window let ignoreConfigEvent = false; -ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => { - if (eventKey === "fullConfigChange") { +ConfigEvent.subscribe(async ({ key, newValue, nosave }) => { + if (key === "fullConfigChange") { ignoreConfigEvent = true; } - if (eventKey === "fullConfigChangeFinished") { + if (key === "fullConfigChangeFinished") { ignoreConfigEvent = false; await clearRandom(); @@ -506,26 +506,26 @@ ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => { // once the full config is loaded, we can apply everything once if (ignoreConfigEvent) return; - if (eventKey === "randomTheme") { + if (key === "randomTheme") { void changeThemeList(); } - if (eventKey === "customTheme") { - (eventValue as boolean) ? await set("custom") : await set(Config.theme); + if (key === "customTheme") { + newValue ? await set("custom") : await set(Config.theme); } - if (eventKey === "customThemeColors") { + if (key === "customThemeColors") { nosave ? preview("custom") : await set("custom"); } - if (eventKey === "theme") { + if (key === "theme") { await clearRandom(); await clearPreview(false); - await set(eventValue as string); + await set(newValue as string); } - if (eventKey === "randomTheme" && eventValue === "off") await clearRandom(); - if (eventKey === "customBackground") await applyCustomBackground(); + if (key === "randomTheme" && newValue === "off") await clearRandom(); + if (key === "customBackground") await applyCustomBackground(); - if (eventKey === "customBackgroundSize") applyCustomBackgroundSize(); - if (eventKey === "autoSwitchTheme") { - if (eventValue as boolean) { + if (key === "customBackgroundSize") applyCustomBackgroundSize(); + if (key === "autoSwitchTheme") { + if (newValue) { if (prefersColorSchemeDark()) { await set(Config.themeDark, true); } else { @@ -536,7 +536,7 @@ ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => { } } if ( - eventKey === "themeLight" && + key === "themeLight" && Config.autoSwitchTheme && !prefersColorSchemeDark() && !nosave @@ -544,7 +544,7 @@ ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => { await set(Config.themeLight, true); } if ( - eventKey === "themeDark" && + key === "themeDark" && Config.autoSwitchTheme && window.matchMedia !== undefined && window.matchMedia("(prefers-color-scheme: dark)").matches && @@ -559,7 +559,7 @@ ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => { "customThemeColors", "randomTheme", "favThemes", - ].includes(eventKey) + ].includes(key) ) { updateFooterIndicator(); } diff --git a/frontend/src/ts/elements/custom-background-filter.ts b/frontend/src/ts/elements/custom-background-filter.ts index b8c8ff0eb11d..23e98fa8e560 100644 --- a/frontend/src/ts/elements/custom-background-filter.ts +++ b/frontend/src/ts/elements/custom-background-filter.ts @@ -133,9 +133,9 @@ const debouncedSave = debounce(2000, async () => { setConfig("customBackgroundFilter", arr, false); }); -ConfigEvent.subscribe((eventKey, eventValue) => { - if (eventKey === "customBackgroundFilter" && (eventValue as boolean)) { - loadConfig(eventValue as CustomBackgroundFilter); +ConfigEvent.subscribe(({ key, newValue }) => { + if (key === "customBackgroundFilter") { + loadConfig(newValue); apply(); } }); diff --git a/frontend/src/ts/elements/keymap.ts b/frontend/src/ts/elements/keymap.ts index decdebe6fbec..3c1ac3d16109 100644 --- a/frontend/src/ts/elements/keymap.ts +++ b/frontend/src/ts/elements/keymap.ts @@ -595,7 +595,7 @@ async function updateLegends(): Promise { } let ignoreConfigEvent = false; -ConfigEvent.subscribe((eventKey) => { +ConfigEvent.subscribe(({ key }) => { const handleMode = (): void => { keymap.qsa(".activeKey").removeClass("activeKey"); keymap.qsa(".keymapKey").setAttribute("style", ""); @@ -634,10 +634,10 @@ ConfigEvent.subscribe((eventKey) => { keymap.qsa(".keymapLegendStyle").addClass(style); }; - if (eventKey === "fullConfigChange") { + if (key === "fullConfigChange") { ignoreConfigEvent = true; } - if (eventKey === "fullConfigChangeFinished") { + if (key === "fullConfigChangeFinished") { ignoreConfigEvent = false; void refresh(); handleMode(); @@ -647,21 +647,21 @@ ConfigEvent.subscribe((eventKey) => { if (ignoreConfigEvent) return; if ( - (eventKey === "layout" && Config.keymapLayout === "overrideSync") || - eventKey === "keymapLayout" || - eventKey === "keymapStyle" || - eventKey === "keymapShowTopRow" || - eventKey === "keymapMode" + (key === "layout" && Config.keymapLayout === "overrideSync") || + key === "keymapLayout" || + key === "keymapStyle" || + key === "keymapShowTopRow" || + key === "keymapMode" ) { void refresh(); } - if (eventKey === "keymapMode") { + if (key === "keymapMode") { handleMode(); } - if (eventKey === "keymapSize") { + if (key === "keymapSize") { handleSize(); } - if (eventKey === "keymapLegendStyle") { + if (key === "keymapLegendStyle") { handleLegendStyle(); } }); diff --git a/frontend/src/ts/elements/modes-notice.ts b/frontend/src/ts/elements/modes-notice.ts index 0d22d57aeb42..e2091b9c47b3 100644 --- a/frontend/src/ts/elements/modes-notice.ts +++ b/frontend/src/ts/elements/modes-notice.ts @@ -13,7 +13,7 @@ import { getActiveFunboxes, getActiveFunboxNames } from "../test/funbox/list"; import { escapeHTML, getMode2 } from "../utils/misc"; import { qsr } from "../utils/dom"; -ConfigEvent.subscribe((eventKey) => { +ConfigEvent.subscribe(({ key }) => { const configKeys: ConfigEvent.ConfigEventKey[] = [ "difficulty", "blindMode", @@ -33,7 +33,7 @@ ConfigEvent.subscribe((eventKey) => { "customPolyglot", "alwaysShowDecimalPlaces", ]; - if (configKeys.includes(eventKey)) { + if (configKeys.includes(key)) { void update(); } }); diff --git a/frontend/src/ts/elements/settings/theme-picker.ts b/frontend/src/ts/elements/settings/theme-picker.ts index fe166c772732..7b667bd9364d 100644 --- a/frontend/src/ts/elements/settings/theme-picker.ts +++ b/frontend/src/ts/elements/settings/theme-picker.ts @@ -493,11 +493,11 @@ $(".pageSettings #saveCustomThemeButton").on("click", async () => { void fillCustomButtons(); }); -ConfigEvent.subscribe((eventKey) => { - if (eventKey === "theme" && ActivePage.get() === "settings") { +ConfigEvent.subscribe(({ key }) => { + if (key === "theme" && ActivePage.get() === "settings") { updateActiveButton(); } - if (eventKey === "favThemes" && ActivePage.get() === "settings") { + if (key === "favThemes" && ActivePage.get() === "settings") { void fillPresetButtons(); } }); diff --git a/frontend/src/ts/observables/config-event.ts b/frontend/src/ts/observables/config-event.ts index 3856d711f3d8..4794c6e25b35 100644 --- a/frontend/src/ts/observables/config-event.ts +++ b/frontend/src/ts/observables/config-event.ts @@ -1,20 +1,20 @@ -import { Config, ConfigKey, ConfigValue } from "@monkeytype/schemas/configs"; +import { Config } from "@monkeytype/schemas/configs"; export type ConfigEventKey = - | ConfigKey - | "saveToLocalStorage" - | "setThemes" - | "configApplied" + | keyof Config | "fullConfigChange" | "fullConfigChangeFinished"; -type SubscribeFunction = ( - key: ConfigEventKey, - newValue?: ConfigValue, - nosave?: boolean, - previousValue?: ConfigValue, - fullConfig?: Config, -) => void; +type SubscribeParams = { + nosave?: boolean; + fullConfig?: Config; +} & { + [K in ConfigEventKey]?: K extends keyof Config + ? { key: K; newValue: Config[K]; previousValue: Config[K] } + : { key: K; newValue?: undefined; previousValue?: undefined }; +}[ConfigEventKey]; + +type SubscribeFunction = (options: SubscribeParams) => void; const subscribers: SubscribeFunction[] = []; @@ -22,16 +22,10 @@ export function subscribe(fn: SubscribeFunction): void { subscribers.push(fn); } -export function dispatch( - key: ConfigEventKey, - newValue?: ConfigValue, - nosave?: boolean, - previousValue?: ConfigValue, - fullConfig?: Config, -): void { +export function dispatch(options: SubscribeParams): void { subscribers.forEach((fn) => { try { - fn(key, newValue, nosave, previousValue, fullConfig); + fn(options); } catch (e) { console.error("Config event subscriber threw an error"); console.error(e); diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 07f3fb9e7d6b..5af14a45112f 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -1187,8 +1187,8 @@ $(".pageAccount button.loadMoreResults").on("click", async () => { Loader.hide(); }); -ConfigEvent.subscribe((eventKey) => { - if (ActivePage.get() === "account" && eventKey === "typingSpeedUnit") { +ConfigEvent.subscribe(({ key }) => { + if (ActivePage.get() === "account" && key === "typingSpeedUnit") { void update(); } }); diff --git a/frontend/src/ts/pages/leaderboards.ts b/frontend/src/ts/pages/leaderboards.ts index 602b78633a85..33dfcfeba8cf 100644 --- a/frontend/src/ts/pages/leaderboards.ts +++ b/frontend/src/ts/pages/leaderboards.ts @@ -1520,8 +1520,8 @@ $(async () => { Skeleton.save("pageLeaderboards"); }); -ConfigEvent.subscribe((eventKey) => { - if (ActivePage.get() === "leaderboards" && eventKey === "typingSpeedUnit") { +ConfigEvent.subscribe(({ key }) => { + if (ActivePage.get() === "leaderboards" && key === "typingSpeedUnit") { updateContent(); fillUser(); } diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index cd71f5bda552..239323893efe 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -978,24 +978,24 @@ $(".pageSettings .section .groupTitle button").on("click", (e) => { }); }); -ConfigEvent.subscribe((eventKey, eventValue) => { - if (eventKey === "fullConfigChange") setEventDisabled(true); - if (eventKey === "fullConfigChangeFinished") setEventDisabled(false); - if (eventKey === "themeLight") { +ConfigEvent.subscribe(({ key, newValue }) => { + if (key === "fullConfigChange") setEventDisabled(true); + if (key === "fullConfigChangeFinished") setEventDisabled(false); + if (key === "themeLight") { $( - `.pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.light option[value="${eventValue}"]`, + `.pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.light option[value="${newValue}"]`, ).attr("selected", "true"); - } else if (eventKey === "themeDark") { + } else if (key === "themeDark") { $( - `.pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.dark option[value="${eventValue}"]`, + `.pageSettings .section[data-config-name='autoSwitchThemeInputs'] select.dark option[value="${newValue}"]`, ).attr("selected", "true"); } //make sure the page doesnt update a billion times when applying a preset/config at once - if (configEventDisabled || eventKey === "saveToLocalStorage") return; - if (ActivePage.get() === "settings" && eventKey !== "theme") { - void (eventKey === "customBackground" + if (configEventDisabled) return; + if (ActivePage.get() === "settings" && key !== "theme") { + void (key === "customBackground" ? updateFilterSectionVisibility() - : update({ eventKey })); + : update({ eventKey: key })); } }); diff --git a/frontend/src/ts/test/caret.ts b/frontend/src/ts/test/caret.ts index 2c3728a8204f..d2cb2d259d97 100644 --- a/frontend/src/ts/test/caret.ts +++ b/frontend/src/ts/test/caret.ts @@ -43,12 +43,12 @@ export function updatePosition(noAnim = false): void { export const caret = new Caret(qsr("#caret"), Config.caretStyle); -subscribe((eventKey) => { - if (eventKey === "caretStyle") { +subscribe(({ key }) => { + if (key === "caretStyle") { caret.setStyle(Config.caretStyle); updatePosition(true); } - if (eventKey === "smoothCaret") { + if (key === "smoothCaret") { caret.updateBlinkingAnimation(); } }); diff --git a/frontend/src/ts/test/live-acc.ts b/frontend/src/ts/test/live-acc.ts index 35c65626f153..a2b3925aa349 100644 --- a/frontend/src/ts/test/live-acc.ts +++ b/frontend/src/ts/test/live-acc.ts @@ -73,6 +73,6 @@ export function hide(): void { }); } -ConfigEvent.subscribe((eventKey, eventValue) => { - if (eventKey === "liveAccStyle") eventValue === "off" ? hide() : show(); +ConfigEvent.subscribe(({ key, newValue }) => { + if (key === "liveAccStyle") newValue === "off" ? hide() : show(); }); diff --git a/frontend/src/ts/test/live-burst.ts b/frontend/src/ts/test/live-burst.ts index 8a683f399c89..93e37f5c9096 100644 --- a/frontend/src/ts/test/live-burst.ts +++ b/frontend/src/ts/test/live-burst.ts @@ -71,6 +71,6 @@ export function hide(): void { }); } -ConfigEvent.subscribe((eventKey, eventValue) => { - if (eventKey === "liveBurstStyle") eventValue === "off" ? hide() : show(); +ConfigEvent.subscribe(({ key, newValue }) => { + if (key === "liveBurstStyle") newValue === "off" ? hide() : show(); }); diff --git a/frontend/src/ts/test/live-speed.ts b/frontend/src/ts/test/live-speed.ts index 10c5addf8d2b..319b8daf288e 100644 --- a/frontend/src/ts/test/live-speed.ts +++ b/frontend/src/ts/test/live-speed.ts @@ -75,6 +75,6 @@ export function hide(): void { }); } -ConfigEvent.subscribe((eventKey, eventValue) => { - if (eventKey === "liveSpeedStyle") eventValue === "off" ? hide() : show(); +ConfigEvent.subscribe(({ key, newValue }) => { + if (key === "liveSpeedStyle") newValue === "off" ? hide() : show(); }); diff --git a/frontend/src/ts/test/monkey.ts b/frontend/src/ts/test/monkey.ts index 82614317300f..49edd43007f1 100644 --- a/frontend/src/ts/test/monkey.ts +++ b/frontend/src/ts/test/monkey.ts @@ -8,8 +8,8 @@ import { animate } from "animejs"; const monkeyEl = document.querySelector("#monkey") as HTMLElement; const monkeyFastEl = document.querySelector("#monkey .fast") as HTMLElement; -ConfigEvent.subscribe((eventKey) => { - if (eventKey === "monkey" && TestState.isActive) { +ConfigEvent.subscribe(({ key }) => { + if (key === "monkey" && TestState.isActive) { if (Config.monkey) { monkeyEl.classList.remove("hidden"); } else { diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts index e9d6b6b83bf3..4a3b861d79f4 100644 --- a/frontend/src/ts/test/pace-caret.ts +++ b/frontend/src/ts/test/pace-caret.ts @@ -258,9 +258,9 @@ export function start(): void { void update((settings?.spc ?? 0) * 1000); } -ConfigEvent.subscribe((eventKey) => { - if (eventKey === "paceCaret") void init(); - if (eventKey === "paceCaretStyle") { +ConfigEvent.subscribe(({ key }) => { + if (key === "paceCaret") void init(); + if (key === "paceCaretStyle") { caret.setStyle(Config.paceCaretStyle); } }); diff --git a/frontend/src/ts/test/practise-words.ts b/frontend/src/ts/test/practise-words.ts index 0d3a9018f1cb..ba1f678a7f96 100644 --- a/frontend/src/ts/test/practise-words.ts +++ b/frontend/src/ts/test/practise-words.ts @@ -177,6 +177,6 @@ export function resetBefore(): void { before.customText = null; } -ConfigEvent.subscribe((eventKey) => { - if (eventKey === "mode") resetBefore(); +ConfigEvent.subscribe(({ key }) => { + if (key === "mode") resetBefore(); }); diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index c1b57ba9b7c1..5d352af7b7d2 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -1303,9 +1303,9 @@ $(".pageTest #favoriteQuoteButton").on("click", async () => { } }); -ConfigEvent.subscribe(async (eventKey) => { +ConfigEvent.subscribe(async ({ key }) => { if ( - ["typingSpeedUnit", "startGraphsAtZero"].includes(eventKey) && + ["typingSpeedUnit", "startGraphsAtZero"].includes(key) && TestState.resultVisible ) { resultAnnotation = []; diff --git a/frontend/src/ts/test/test-config.ts b/frontend/src/ts/test/test-config.ts index adde4037d3c7..5405c152678d 100644 --- a/frontend/src/ts/test/test-config.ts +++ b/frontend/src/ts/test/test-config.ts @@ -320,11 +320,11 @@ export function hideFavoriteQuoteLength(): void { let ignoreConfigEvent = false; -ConfigEvent.subscribe((eventKey, eventValue, _nosave, eventPreviousValue) => { - if (eventKey === "fullConfigChange") { +ConfigEvent.subscribe(({ key, newValue, previousValue }) => { + if (key === "fullConfigChange") { ignoreConfigEvent = true; } - if (eventKey === "fullConfigChangeFinished") { + if (key === "fullConfigChangeFinished") { ignoreConfigEvent = false; void instantUpdate(); @@ -335,15 +335,15 @@ ConfigEvent.subscribe((eventKey, eventValue, _nosave, eventPreviousValue) => { if (ignoreConfigEvent) return; if (ActivePage.get() !== "test") return; - if (eventKey === "mode") { - void update(eventPreviousValue as Mode, eventValue as Mode); + if (key === "mode") { + void update(previousValue, newValue); } else if ( ["time", "quoteLength", "words", "numbers", "punctuation"].includes( - eventKey, + key ?? "", ) ) { - if (eventValue !== undefined) { - updateActiveExtraButtons(eventKey, eventValue); + if (newValue !== undefined) { + updateActiveExtraButtons(key, newValue); } } }); diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index c123ba45b8f4..754dae2d8838 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -1590,31 +1590,21 @@ $("header").on("click", "nav #startTestButton, #logo", () => { // =============================== -ConfigEvent.subscribe((eventKey, eventValue, nosave) => { +ConfigEvent.subscribe(({ key, newValue, nosave }) => { if (ActivePage.get() === "test") { - if (eventKey === "language") { + if (key === "language") { //automatically enable lazy mode for arabic - if ( - (eventValue as string)?.startsWith("arabic") && - ArabicLazyMode.get() - ) { + if ((newValue as string)?.startsWith("arabic") && ArabicLazyMode.get()) { setConfig("lazyMode", true, true); } restart(); } - if (eventKey === "difficulty" && !nosave) restart(); - if ( - eventKey === "customLayoutfluid" && - Config.funbox.includes("layoutfluid") - ) { + if (key === "difficulty" && !nosave) restart(); + if (key === "customLayoutfluid" && Config.funbox.includes("layoutfluid")) { restart(); } - if ( - eventKey === "keymapMode" && - eventValue === "next" && - Config.mode !== "zen" - ) { + if (key === "keymapMode" && newValue === "next" && Config.mode !== "zen") { setTimeout(() => { void KeymapEvent.highlight( Arrays.nthElementFromArray( @@ -1627,11 +1617,11 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => { }, 0); } } - if (eventKey === "lazyMode" && !nosave) { + if (key === "lazyMode" && !nosave) { if (Config.language.startsWith("arabic")) { - ArabicLazyMode.set(eventValue as boolean); + ArabicLazyMode.set(newValue); } - if (eventValue === false) { + if (newValue) { if (!showedLazyModeNotification) { rememberLazyMode = false; } diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index f9bc373dc110..4ec86b2f9cdf 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -82,17 +82,17 @@ export const updateHintsPositionDebounced = Misc.debounceUntilResolved( { rejectSkippedCalls: false }, ); -ConfigEvent.subscribe((eventKey, eventValue, nosave) => { +ConfigEvent.subscribe(({ key, newValue, nosave }) => { if ( - (eventKey === "language" || eventKey === "funbox") && + (key === "language" || key === "funbox") && Config.funbox.includes("zipf") ) { debouncedZipfCheck(); } - if (eventKey === "fontSize") { + if (key === "fontSize") { $( "#caret, #paceCaret, #liveStatsMini, #typingTest, #wordsInput, #compositionDisplay", - ).css("fontSize", (eventValue as number) + "rem"); + ).css("fontSize", newValue + "rem"); if (!nosave) { OutOfFocus.hide(); updateWordWrapperClasses(); @@ -100,16 +100,16 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => { } if ( ["fontSize", "fontFamily", "blindMode", "hideExtraLetters"].includes( - eventKey, + key ?? "", ) ) { void updateHintsPositionDebounced(); } - if (eventKey === "theme") void applyBurstHeatmap(); + if (key === "theme") void applyBurstHeatmap(); - if (eventValue === undefined) return; - if (eventKey === "highlightMode") { + if (newValue === undefined) return; + if (key === "highlightMode") { if (ActivePage.get() === "test") { void updateWordLetters({ input: TestInput.input.current, @@ -126,26 +126,26 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => { "indicateTypos", "tapeMode", "hideExtraLetters", - ].includes(eventKey) + ].includes(key) ) { updateWordWrapperClasses(); } - if (["tapeMode", "tapeMargin"].includes(eventKey)) { + if (["tapeMode", "tapeMargin"].includes(key)) { updateLiveStatsMargin(); } - if (eventKey === "showAllLines") { + if (key === "showAllLines") { updateWordsWrapperHeight(true); - if (eventValue === false) { + if (!newValue) { void centerActiveLine(); } } - if (typeof eventValue !== "boolean") return; - if (eventKey === "flipTestColors") flipColors(eventValue); - if (eventKey === "colorfulMode") colorful(eventValue); - if (eventKey === "burstHeatmap") void applyBurstHeatmap(); + if (typeof newValue !== "boolean") return; + if (key === "flipTestColors") flipColors(newValue); + if (key === "colorfulMode") colorful(newValue); + if (key === "burstHeatmap") void applyBurstHeatmap(); }); const wordsEl = document.querySelector(".pageTest #words") as HTMLElement; @@ -2032,9 +2032,9 @@ $("#wordsWrapper").on("click", () => { focusWords(); }); -ConfigEvent.subscribe((key, value) => { +ConfigEvent.subscribe(({ key, newValue }) => { if (key === "quickRestart") { - if (value === "off") { + if (newValue === "off") { $(".pageTest #restartTestButton").removeClass("hidden"); } else { $(".pageTest #restartTestButton").addClass("hidden"); @@ -2044,16 +2044,16 @@ ConfigEvent.subscribe((key, value) => { updateWordsWidth(); } if (key === "timerOpacity") { - updateLiveStatsOpacity(value as TimerOpacity); + updateLiveStatsOpacity(newValue); } if (key === "timerColor") { - updateLiveStatsColor(value as TimerColor); + updateLiveStatsColor(newValue); } - if (key === "showOutOfFocusWarning" && value === false) { + if (key === "showOutOfFocusWarning" && !newValue) { OutOfFocus.hide(); } if (key === "compositionDisplay") { - if (value === "below") { + if (newValue === "below") { CompositionDisplay.update(" "); CompositionDisplay.show(); } else { diff --git a/frontend/src/ts/test/timer-progress.ts b/frontend/src/ts/test/timer-progress.ts index 30d87eb3f452..ddc931e67cc1 100644 --- a/frontend/src/ts/test/timer-progress.ts +++ b/frontend/src/ts/test/timer-progress.ts @@ -254,6 +254,6 @@ export function updateStyle(): void { }, 125); } -ConfigEvent.subscribe((eventKey, eventValue) => { - if (eventKey === "timerStyle") updateStyle(); +ConfigEvent.subscribe(({ key }) => { + if (key === "timerStyle") updateStyle(); }); diff --git a/frontend/src/ts/test/tts.ts b/frontend/src/ts/test/tts.ts index a70e58d36c0d..ab62b05954ea 100644 --- a/frontend/src/ts/test/tts.ts +++ b/frontend/src/ts/test/tts.ts @@ -31,15 +31,15 @@ export async function speak(text: string): Promise { } } -ConfigEvent.subscribe((eventKey, eventValue) => { - if (eventKey === "funbox") { - if (eventValue === "none") { - clear(); - } else if (eventValue === "tts") { +ConfigEvent.subscribe(({ key, newValue }) => { + if (key === "funbox") { + if (newValue.includes("tts")) { void init(); + } else { + clear(); } } - if (eventKey === "language" && Config.funbox.includes("tts")) { + if (key === "language" && Config.funbox.includes("tts")) { void setLanguage(); } }); diff --git a/frontend/src/ts/ui.ts b/frontend/src/ts/ui.ts index d0e7b59d271f..ebd3496e5947 100644 --- a/frontend/src/ts/ui.ts +++ b/frontend/src/ts/ui.ts @@ -115,9 +115,9 @@ window.addEventListener("resize", () => { debouncedEvent(); }); -ConfigEvent.subscribe(async (eventKey) => { - if (eventKey === "quickRestart") updateKeytips(); - if (eventKey === "showKeyTips") { +ConfigEvent.subscribe(async ({ key }) => { + if (key === "quickRestart") updateKeytips(); + if (key === "showKeyTips") { const keyTipsElement = qs("footer .keyTips"); if (Config.showKeyTips) { keyTipsElement?.removeClass("hidden"); @@ -125,7 +125,7 @@ ConfigEvent.subscribe(async (eventKey) => { keyTipsElement?.addClass("hidden"); } } - if (eventKey === "fontFamily") { + if (key === "fontFamily") { await applyFontFamily(); } }); From 2b380bb931fb749c9eacf18f2eff5849990ebf89 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 12 Dec 2025 15:16:11 +0100 Subject: [PATCH 3/4] refactor: use ElementWithUtils in page class (@fehmer) (#7223) --- frontend/__tests__/setup-tests.ts | 1 + frontend/src/ts/controllers/page-controller.ts | 18 +++++++++--------- frontend/src/ts/pages/404.ts | 3 ++- frontend/src/ts/pages/about.ts | 3 ++- frontend/src/ts/pages/account-settings.ts | 3 ++- frontend/src/ts/pages/account.ts | 3 ++- frontend/src/ts/pages/friends.ts | 5 ++--- frontend/src/ts/pages/leaderboards.ts | 3 ++- frontend/src/ts/pages/loading.ts | 3 ++- frontend/src/ts/pages/login.ts | 2 +- frontend/src/ts/pages/page.ts | 5 +++-- frontend/src/ts/pages/profile-search.ts | 2 +- frontend/src/ts/pages/profile.ts | 3 ++- frontend/src/ts/pages/settings.ts | 2 +- frontend/src/ts/pages/test.ts | 3 ++- frontend/src/ts/utils/dom.ts | 16 ++++++++++++++++ 16 files changed, 50 insertions(+), 25 deletions(-) diff --git a/frontend/__tests__/setup-tests.ts b/frontend/__tests__/setup-tests.ts index 6828d27727c0..0ac3acf1e385 100644 --- a/frontend/__tests__/setup-tests.ts +++ b/frontend/__tests__/setup-tests.ts @@ -64,6 +64,7 @@ vi.mock("../src/ts/utils/dom", () => { getOffsetTop: vi.fn().mockReturnValue(0), getOffsetLeft: vi.fn().mockReturnValue(0), animate: vi.fn().mockResolvedValue(null), + promiseAnimate: vi.fn().mockResolvedValue(null), native: document.createElement("div"), }; }; diff --git a/frontend/src/ts/controllers/page-controller.ts b/frontend/src/ts/controllers/page-controller.ts index fbf98be33d61..e60461598866 100644 --- a/frontend/src/ts/controllers/page-controller.ts +++ b/frontend/src/ts/controllers/page-controller.ts @@ -59,14 +59,14 @@ async function showSyncLoading({ loadingOptions: LoadingOptions[]; totalDuration: number; }): Promise { - PageLoading.page.element.removeClass("hidden").css("opacity", 0); + PageLoading.page.element.show().setStyle({ opacity: "0" }); await PageLoading.page.beforeShow({}); const fillDivider = loadingOptions.length; const fillOffset = 100 / fillDivider; //void here to run the loading promise as soon as possible - void Misc.promiseAnimate(PageLoading.page.element[0] as HTMLElement, { + void PageLoading.page.element.promiseAnimate({ opacity: "1", duration: totalDuration / 2, }); @@ -97,13 +97,13 @@ async function showSyncLoading({ } } - await Misc.promiseAnimate(PageLoading.page.element[0] as HTMLElement, { + await PageLoading.page.element.promiseAnimate({ opacity: "0", duration: totalDuration / 2, }); await PageLoading.page.afterHide(); - PageLoading.page.element.addClass("hidden"); + PageLoading.page.element.hide(); } // Global abort controller for keyframe promises @@ -206,12 +206,12 @@ export async function change( //previous page await previousPage?.beforeHide?.(); - previousPage.element.removeClass("hidden").css("opacity", 1); - await Misc.promiseAnimate(previousPage.element[0] as HTMLElement, { + previousPage.element.show().setStyle({ opacity: "1" }); + await previousPage.element.promiseAnimate({ opacity: "0", duration: totalDuration / 2, }); - previousPage.element.addClass("hidden"); + previousPage.element.hide(); await previousPage?.afterHide(); // we need to evaluate and store next page loading mode in case options.loadingOptions.loadingMode is sync @@ -281,8 +281,8 @@ export async function change( }); } - nextPage.element.removeClass("hidden").css("opacity", 0); - await Misc.promiseAnimate(nextPage.element[0] as HTMLElement, { + nextPage.element.show().setStyle({ opacity: "0" }); + await nextPage.element.promiseAnimate({ opacity: "1", duration: totalDuration / 2, }); diff --git a/frontend/src/ts/pages/404.ts b/frontend/src/ts/pages/404.ts index bb67ce5b73b5..c081af356114 100644 --- a/frontend/src/ts/pages/404.ts +++ b/frontend/src/ts/pages/404.ts @@ -1,9 +1,10 @@ import Page from "./page"; import * as Skeleton from "../utils/skeleton"; +import { qsr } from "../utils/dom"; export const page = new Page({ id: "404", - element: $(".page.page404"), + element: qsr(".page.page404"), path: "/404", afterHide: async (): Promise => { Skeleton.remove("page404"); diff --git a/frontend/src/ts/pages/about.ts b/frontend/src/ts/pages/about.ts index 6761be0b7a5a..a82da0c534a7 100644 --- a/frontend/src/ts/pages/about.ts +++ b/frontend/src/ts/pages/about.ts @@ -10,6 +10,7 @@ import * as Skeleton from "../utils/skeleton"; import { TypingStats, SpeedHistogram } from "@monkeytype/schemas/public"; import { getNumberWithMagnitude, numberWithSpaces } from "../utils/numbers"; import { tryCatch } from "@monkeytype/util/trycatch"; +import { qsr } from "../utils/dom"; function reset(): void { $(".pageAbout .contributors").empty(); @@ -199,7 +200,7 @@ function getHistogramDataBucketed(data: Record): { export const page = new Page({ id: "about", - element: $(".page.pageAbout"), + element: qsr(".page.pageAbout"), path: "/about", afterHide: async (): Promise => { reset(); diff --git a/frontend/src/ts/pages/account-settings.ts b/frontend/src/ts/pages/account-settings.ts index 534ed19bdfa1..4d9ab4502d14 100644 --- a/frontend/src/ts/pages/account-settings.ts +++ b/frontend/src/ts/pages/account-settings.ts @@ -12,6 +12,7 @@ import * as BlockedUserTable from "../elements/account-settings/blocked-user-tab import * as Notifications from "../elements/notifications"; import { z } from "zod"; import * as AuthEvent from "../observables/auth-event"; +import { qsr } from "../utils/dom"; const pageElement = $(".page.pageAccountSettings"); @@ -229,7 +230,7 @@ AuthEvent.subscribe((event) => { export const page = new PageWithUrlParams({ id: "accountSettings", display: "Account Settings", - element: pageElement, + element: qsr(".page.pageAccountSettings"), path: "/account-settings", urlParamsSchema: UrlParameterSchema, afterHide: async (): Promise => { diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 5af14a45112f..537725739ac9 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -35,6 +35,7 @@ import { SnapshotResult } from "../constants/default-snapshot"; import Ape from "../ape"; import { AccountChart } from "@monkeytype/schemas/configs"; import { SortedTableWithLimit } from "../utils/sorted-table"; +import { qsr } from "../utils/dom"; let filterDebug = false; //toggle filterdebug @@ -1195,7 +1196,7 @@ ConfigEvent.subscribe(({ key }) => { export const page = new Page({ id: "account", - element: $(".page.pageAccount"), + element: qsr(".page.pageAccount"), path: "/account", loadingOptions: { loadingMode: () => { diff --git a/frontend/src/ts/pages/friends.ts b/frontend/src/ts/pages/friends.ts index 31d72ad9fff0..6b5c9a554d39 100644 --- a/frontend/src/ts/pages/friends.ts +++ b/frontend/src/ts/pages/friends.ts @@ -30,8 +30,7 @@ import { Friend, UserNameSchema } from "@monkeytype/schemas/users"; import * as Loader from "../elements/loader"; import { LocalStorageWithSchema } from "../utils/local-storage-with-schema"; import { remoteValidation } from "../utils/remote-validation"; - -const pageElement = $(".page.pageFriends"); +import { qsr } from "../utils/dom"; let friendsTable: SortedTable | undefined = undefined; @@ -499,7 +498,7 @@ function update(): void { export const page = new Page({ id: "friends", display: "Friends", - element: pageElement, + element: qsr(".page.pageFriends"), path: "/friends", loadingOptions: { loadingMode: () => { diff --git a/frontend/src/ts/pages/leaderboards.ts b/frontend/src/ts/pages/leaderboards.ts index 33dfcfeba8cf..7336c819722a 100644 --- a/frontend/src/ts/pages/leaderboards.ts +++ b/frontend/src/ts/pages/leaderboards.ts @@ -44,6 +44,7 @@ import { isSafeNumber } from "@monkeytype/util/numbers"; import { Mode, Mode2, ModeSchema } from "@monkeytype/schemas/shared"; import * as ServerConfiguration from "../ape/server-configuration"; import { getAvatarElement } from "../utils/discord-avatar"; +import { qsr } from "../utils/dom"; const LeaderboardTypeSchema = z.enum(["allTime", "weekly", "daily"]); type LeaderboardType = z.infer; @@ -1489,7 +1490,7 @@ $(".page.pageLeaderboards .buttonGroup.friendsOnlyButtons").on( export const page = new PageWithUrlParams({ id: "leaderboards", - element: $(".page.pageLeaderboards"), + element: qsr(".page.pageLeaderboards"), path: "/leaderboards", urlParamsSchema: UrlParameterSchema, diff --git a/frontend/src/ts/pages/loading.ts b/frontend/src/ts/pages/loading.ts index 98580073a7ff..4e7437fc55ba 100644 --- a/frontend/src/ts/pages/loading.ts +++ b/frontend/src/ts/pages/loading.ts @@ -1,6 +1,7 @@ import Page from "./page"; import * as Skeleton from "../utils/skeleton"; import { promiseAnimate } from "../utils/misc"; +import { qsr } from "../utils/dom"; const pageEl = $(".page.pageLoading"); const barEl = pageEl.find(".bar"); @@ -45,7 +46,7 @@ export async function showBar(): Promise { export const page = new Page({ id: "loading", - element: pageEl, + element: qsr(".page.pageLoading"), path: "/", afterHide: async (): Promise => { Skeleton.remove("pageLoading"); diff --git a/frontend/src/ts/pages/login.ts b/frontend/src/ts/pages/login.ts index 73c63da7850a..b7cd522945f6 100644 --- a/frontend/src/ts/pages/login.ts +++ b/frontend/src/ts/pages/login.ts @@ -208,7 +208,7 @@ new ValidatedHtmlInputElement(passwordVerifyInputEl, { export const page = new Page({ id: "login", - element: $(".page.pageLogin"), + element: qsr(".page.pageLogin"), path: "/login", afterHide: async (): Promise => { hidePreloader(); diff --git a/frontend/src/ts/pages/page.ts b/frontend/src/ts/pages/page.ts index e177c663c0d3..70375c86fcb5 100644 --- a/frontend/src/ts/pages/page.ts +++ b/frontend/src/ts/pages/page.ts @@ -3,6 +3,7 @@ import { safeParse as parseUrlSearchParams, serialize as serializeUrlSearchParams, } from "zod-urlsearchparams"; +import { ElementWithUtils } from "../utils/dom"; export type PageName = | "loading" @@ -69,7 +70,7 @@ export type LoadingOptions = { type PageProperties = { id: PageName; display?: string; - element: JQuery; + element: ElementWithUtils; path: string; loadingOptions?: LoadingOptions; beforeHide?: () => Promise; @@ -84,7 +85,7 @@ async function empty(): Promise { export default class Page { public id: PageName; public display: string | undefined; - public element: JQuery; + public element: ElementWithUtils; public pathname: string; public loadingOptions: LoadingOptions | undefined; diff --git a/frontend/src/ts/pages/profile-search.ts b/frontend/src/ts/pages/profile-search.ts index 684bf7a8bea8..1bf6dbd9096e 100644 --- a/frontend/src/ts/pages/profile-search.ts +++ b/frontend/src/ts/pages/profile-search.ts @@ -20,7 +20,7 @@ function disableButton(): void { export const page = new Page({ id: "profileSearch", - element: $(".page.pageProfileSearch"), + element: qsr(".page.pageProfileSearch"), path: "/profile", afterHide: async (): Promise => { Skeleton.remove("pageProfileSearch"); diff --git a/frontend/src/ts/pages/profile.ts b/frontend/src/ts/pages/profile.ts index 2ee81a6467e7..098a5a5af04d 100644 --- a/frontend/src/ts/pages/profile.ts +++ b/frontend/src/ts/pages/profile.ts @@ -12,6 +12,7 @@ import * as TestActivity from "../elements/test-activity"; import { TestActivityCalendar } from "../elements/test-activity-calendar"; import { getFirstDayOfTheWeek } from "../utils/date-and-time"; import { addFriend } from "./friends"; +import { qsr } from "../utils/dom"; const firstDayOfTheWeek = getFirstDayOfTheWeek(); @@ -260,7 +261,7 @@ $(".page.pageProfile").on("click", ".profile .addFriendButton", async () => { export const page = new Page({ id: "profile", - element: $(".page.pageProfile"), + element: qsr(".page.pageProfile"), path: "/profile", afterHide: async (): Promise => { Skeleton.remove("pageProfile"); diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 239323893efe..c6707aaf0373 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -1011,7 +1011,7 @@ AuthEvent.subscribe((event) => { export const page = new PageWithUrlParams({ id: "settings", - element: $(".page.pageSettings"), + element: qsr(".page.pageSettings"), path: "/settings", urlParamsSchema: StateSchema, afterHide: async (): Promise => { diff --git a/frontend/src/ts/pages/test.ts b/frontend/src/ts/pages/test.ts index 37b37a3acf2d..0791fdd53758 100644 --- a/frontend/src/ts/pages/test.ts +++ b/frontend/src/ts/pages/test.ts @@ -9,10 +9,11 @@ import * as Keymap from "../elements/keymap"; import * as TestConfig from "../test/test-config"; import * as ScrollToTop from "../elements/scroll-to-top"; import { blurInputElement } from "../input/input-element"; +import { qsr } from "../utils/dom"; export const page = new Page({ id: "test", - element: $(".page.pageTest"), + element: qsr(".page.pageTest"), path: "/", beforeHide: async (): Promise => { blurInputElement(); diff --git a/frontend/src/ts/utils/dom.ts b/frontend/src/ts/utils/dom.ts index f47fa6e6b4a4..467a7eef2f26 100644 --- a/frontend/src/ts/utils/dom.ts +++ b/frontend/src/ts/utils/dom.ts @@ -514,6 +514,22 @@ export class ElementWithUtils { animate(animationParams: AnimationParams): JSAnimation { return animejsAnimate(this.native, animationParams); } + + /** + * Animate the element using Anime.js + * @param animationParams The Anime.js animation parameters + */ + async promiseAnimate(animationParams: AnimationParams): Promise { + return new Promise((resolve) => { + animejsAnimate(this.native, { + ...animationParams, + onComplete: (self, e) => { + animationParams.onComplete?.(self, e); + resolve(); + }, + }); + }); + } } /** From e0b5c465cd26c0d33b17ef9cc0f966ffba1d2baf Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 12 Dec 2025 16:55:33 +0100 Subject: [PATCH 4/4] refactor: add options object to setConfig (@miodec) (#7228) --- frontend/__tests__/root/config.spec.ts | 14 +- frontend/__tests__/utils/url-handler.spec.ts | 54 ++++++-- frontend/src/ts/config.ts | 24 ++-- .../ts/controllers/challenge-controller.ts | 120 +++++++++++++----- .../src/ts/controllers/theme-controller.ts | 4 +- .../ts/elements/custom-background-filter.ts | 2 +- frontend/src/ts/elements/input-validation.ts | 2 +- .../ts/elements/settings/settings-group.ts | 4 +- frontend/src/ts/modals/mobile-test-config.ts | 2 +- frontend/src/ts/modals/quote-search.ts | 2 +- .../src/ts/test/funbox/funbox-functions.ts | 36 ++++-- frontend/src/ts/test/funbox/funbox-memory.ts | 4 +- frontend/src/ts/test/funbox/funbox.ts | 18 ++- frontend/src/ts/test/practise-words.ts | 4 +- frontend/src/ts/test/test-logic.ts | 18 ++- frontend/src/ts/test/test-timer.ts | 8 +- frontend/src/ts/utils/url-handler.ts | 34 +++-- 17 files changed, 253 insertions(+), 97 deletions(-) diff --git a/frontend/__tests__/root/config.spec.ts b/frontend/__tests__/root/config.spec.ts index 1f20478d3178..fc4293f446c0 100644 --- a/frontend/__tests__/root/config.spec.ts +++ b/frontend/__tests__/root/config.spec.ts @@ -212,7 +212,9 @@ describe("Config", () => { replaceConfig({ numbers: false }); //WHEN - Config.setConfig("numbers", true, true); + Config.setConfig("numbers", true, { + nosave: true, + }); //THEN //wait for debounce @@ -227,7 +229,9 @@ describe("Config", () => { replaceConfig({ numbers: false }); //WHEN - Config.setConfig("numbers", true, true); + Config.setConfig("numbers", true, { + nosave: true, + }); //THEN @@ -241,21 +245,21 @@ describe("Config", () => { it("triggers resize if property is set", () => { ///WHEN - Config.setConfig("maxLineWidth", 50, false); + Config.setConfig("maxLineWidth", 50); expect(miscTriggerResizeMock).toHaveBeenCalled(); }); it("does not triggers resize if property is not set", () => { ///WHEN - Config.setConfig("startGraphsAtZero", true, false); + Config.setConfig("startGraphsAtZero", true); expect(miscTriggerResizeMock).not.toHaveBeenCalled(); }); it("does not triggers resize if property on nosave", () => { ///WHEN - Config.setConfig("maxLineWidth", 50, true); + Config.setConfig("maxLineWidth", 50, { nosave: true }); expect(miscTriggerResizeMock).not.toHaveBeenCalled(); }); diff --git a/frontend/__tests__/utils/url-handler.spec.ts b/frontend/__tests__/utils/url-handler.spec.ts index 81ef431804dd..a89a7ccdd6c4 100644 --- a/frontend/__tests__/utils/url-handler.spec.ts +++ b/frontend/__tests__/utils/url-handler.spec.ts @@ -56,8 +56,12 @@ describe("url-handler", () => { loadTestSettingsFromUrl(""); //THEN - expect(setConfigMock).toHaveBeenCalledWith("mode", "time", true); - expect(setConfigMock).toHaveBeenCalledWith("time", 60, true); + expect(setConfigMock).toHaveBeenCalledWith("mode", "time", { + nosave: true, + }); + expect(setConfigMock).toHaveBeenCalledWith("time", 60, { + nosave: true, + }); expect(restartTestMock).toHaveBeenCalled(); }); it("sets time", () => { @@ -70,8 +74,12 @@ describe("url-handler", () => { loadTestSettingsFromUrl(""); //THEN - expect(setConfigMock).toHaveBeenCalledWith("mode", "time", true); - expect(setConfigMock).toHaveBeenCalledWith("time", 30, true); + expect(setConfigMock).toHaveBeenCalledWith("mode", "time", { + nosave: true, + }); + expect(setConfigMock).toHaveBeenCalledWith("time", 30, { + nosave: true, + }); expect(restartTestMock).toHaveBeenCalled(); }); it("sets word count", () => { @@ -84,8 +92,12 @@ describe("url-handler", () => { loadTestSettingsFromUrl(""); //THEN - expect(setConfigMock).toHaveBeenCalledWith("mode", "words", true); - expect(setConfigMock).toHaveBeenCalledWith("words", 50, true); + expect(setConfigMock).toHaveBeenCalledWith("mode", "words", { + nosave: true, + }); + expect(setConfigMock).toHaveBeenCalledWith("words", 50, { + nosave: true, + }); expect(restartTestMock).toHaveBeenCalled(); }); it("sets quote length", () => { @@ -98,8 +110,10 @@ describe("url-handler", () => { loadTestSettingsFromUrl(""); //THEN - expect(setConfigMock).toHaveBeenCalledWith("mode", "quote", true); - expect(setConfigMock).toHaveBeenCalledWith("quoteLength", [-2], false); + expect(setConfigMock).toHaveBeenCalledWith("mode", "quote", { + nosave: true, + }); + expect(setConfigMock).toHaveBeenCalledWith("quoteLength", [-2]); expect(setSelectedQuoteIdMock).toHaveBeenCalledWith(512); expect(restartTestMock).toHaveBeenCalled(); }); @@ -111,7 +125,9 @@ describe("url-handler", () => { loadTestSettingsFromUrl(""); //THEN - expect(setConfigMock).toHaveBeenCalledWith("punctuation", true, true); + expect(setConfigMock).toHaveBeenCalledWith("punctuation", true, { + nosave: true, + }); expect(restartTestMock).toHaveBeenCalled(); }); it("sets numbers", () => { @@ -122,7 +138,9 @@ describe("url-handler", () => { loadTestSettingsFromUrl(""); //THEN - expect(setConfigMock).toHaveBeenCalledWith("numbers", false, true); + expect(setConfigMock).toHaveBeenCalledWith("numbers", false, { + nosave: true, + }); expect(restartTestMock).toHaveBeenCalled(); }); it("sets language", () => { @@ -133,7 +151,9 @@ describe("url-handler", () => { loadTestSettingsFromUrl(""); //THEN - expect(setConfigMock).toHaveBeenCalledWith("language", "english", true); + expect(setConfigMock).toHaveBeenCalledWith("language", "english", { + nosave: true, + }); expect(restartTestMock).toHaveBeenCalled(); }); it("sets difficulty", () => { @@ -144,7 +164,9 @@ describe("url-handler", () => { loadTestSettingsFromUrl(""); //THEN - expect(setConfigMock).toHaveBeenCalledWith("difficulty", "master", true); + expect(setConfigMock).toHaveBeenCalledWith("difficulty", "master", { + nosave: true, + }); expect(restartTestMock).toHaveBeenCalled(); }); it("sets funbox", () => { @@ -160,7 +182,9 @@ describe("url-handler", () => { expect(setConfigMock).toHaveBeenCalledWith( "funbox", ["crt", "choo_choo"], - true, + { + nosave: true, + }, ); expect(restartTestMock).toHaveBeenCalled(); }); @@ -177,7 +201,9 @@ describe("url-handler", () => { expect(setConfigMock).toHaveBeenCalledWith( "funbox", ["crt", "choo_choo"], - true, + { + nosave: true, + }, ); expect(restartTestMock).toHaveBeenCalled(); }); diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index a14d8a23bf48..271105df54d9 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -91,7 +91,9 @@ function isConfigChangeBlocked(): boolean { export function setConfig( key: T, value: Config[T], - nosave: boolean = false, + options?: { + nosave?: boolean; + }, ): boolean { const metadata = configMetadata[key] as ConfigMetadataObject[T]; if (metadata === undefined) { @@ -168,7 +170,7 @@ export function setConfig( continue; // no need to set if the value is already the same } - const set = setConfig(targetKey, targetValue, nosave); + const set = setConfig(targetKey, targetValue, options); if (!set) { throw new Error( `Failed to set config key "${targetKey}" with value "${targetValue}" for ${metadata.displayString} config override.`, @@ -178,22 +180,24 @@ export function setConfig( } config[key] = value; - if (!nosave) saveToLocalStorage(key, nosave); + if (!options?.nosave) saveToLocalStorage(key, options?.nosave); // @ts-expect-error i can't figure this out ConfigEvent.dispatch({ key: key, newValue: value, - nosave, + nosave: options?.nosave ?? false, previousValue: previousValue as Config[T], }); - if (metadata.triggerResize && !nosave) { + if (metadata.triggerResize && !options?.nosave) { triggerResize(); } - metadata.afterSet?.({ nosave: nosave || false, currentConfig: config }); - + metadata.afterSet?.({ + nosave: options?.nosave ?? false, + currentConfig: config, + }); return true; } @@ -232,7 +236,9 @@ export function toggleFunbox(funbox: FunboxName, nosave?: boolean): boolean { } export function setQuoteLengthAll(nosave?: boolean): boolean { - return setConfig("quoteLength", [0, 1, 2, 3], nosave); + return setConfig("quoteLength", [0, 1, 2, 3], { + nosave, + }); } const lastConfigsToApply: Set = new Set([ @@ -275,7 +281,7 @@ export async function applyConfig( for (const configKey of [...firstKeys, ...lastConfigsToApply]) { const configValue = fullConfig[configKey]; - const set = setConfig(configKey, configValue, true); + const set = setConfig(configKey, configValue, { nosave: true }); if (!set) { configKeysToReset.push(configKey); diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index 0e001099cd60..f56a0f376a61 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -241,26 +241,48 @@ export async function setup(challengeName: string): Promise { return false; } if (challenge.type === "customTime") { - setConfig("time", challenge.parameters[0] as number, true); - setConfig("mode", "time", true); - setConfig("difficulty", "normal", true); + setConfig("time", challenge.parameters[0] as number, { + nosave: true, + }); + setConfig("mode", "time", { + nosave: true, + }); + setConfig("difficulty", "normal", { + nosave: true, + }); if (challenge.name === "englishMaster") { - setConfig("language", "english_10k", true); - setConfig("numbers", true, true); - setConfig("punctuation", true, true); + setConfig("language", "english_10k", { + nosave: true, + }); + setConfig("numbers", true, { + nosave: true, + }); + setConfig("punctuation", true, { + nosave: true, + }); } } else if (challenge.type === "customWords") { - setConfig("words", challenge.parameters[0] as number, true); - setConfig("mode", "words", true); - setConfig("difficulty", "normal", true); + setConfig("words", challenge.parameters[0] as number, { + nosave: true, + }); + setConfig("mode", "words", { + nosave: true, + }); + setConfig("difficulty", "normal", { + nosave: true, + }); } else if (challenge.type === "customText") { CustomText.setText((challenge.parameters[0] as string).split(" ")); CustomText.setMode(challenge.parameters[1] as CustomTextMode); CustomText.setLimitValue(challenge.parameters[2] as number); CustomText.setLimitMode(challenge.parameters[3] as CustomTextLimitMode); CustomText.setPipeDelimiter(challenge.parameters[4] as boolean); - setConfig("mode", "custom", true); - setConfig("difficulty", "normal", true); + setConfig("mode", "custom", { + nosave: true, + }); + setConfig("difficulty", "normal", { + nosave: true, + }); } else if (challenge.type === "script") { Loader.show(); const response = await fetch( @@ -278,8 +300,12 @@ export async function setup(challengeName: string): Promise { CustomText.setMode("repeat"); CustomText.setLimitMode("word"); CustomText.setPipeDelimiter(false); - setConfig("mode", "custom", true); - setConfig("difficulty", "normal", true); + setConfig("mode", "custom", { + nosave: true, + }); + setConfig("difficulty", "normal", { + nosave: true, + }); if (challenge.parameters[1] !== null) { setConfig("theme", challenge.parameters[1] as ThemeName); } @@ -287,32 +313,66 @@ export async function setup(challengeName: string): Promise { void Funbox.activate(challenge.parameters[2] as FunboxName[]); } } else if (challenge.type === "accuracy") { - setConfig("time", 0, true); - setConfig("mode", "time", true); - setConfig("difficulty", "master", true); + setConfig("time", 0, { + nosave: true, + }); + setConfig("mode", "time", { + nosave: true, + }); + setConfig("difficulty", "master", { + nosave: true, + }); } else if (challenge.type === "funbox") { - setConfig("funbox", challenge.parameters[0] as FunboxName[], true); - setConfig("difficulty", "normal", true); + setConfig("funbox", challenge.parameters[0] as FunboxName[], { + nosave: true, + }); + setConfig("difficulty", "normal", { + nosave: true, + }); if (challenge.parameters[1] === "words") { - setConfig("words", challenge.parameters[2] as number, true); + setConfig("words", challenge.parameters[2] as number, { + nosave: true, + }); } else if (challenge.parameters[1] === "time") { - setConfig("time", challenge.parameters[2] as number, true); + setConfig("time", challenge.parameters[2] as number, { + nosave: true, + }); } - setConfig("mode", challenge.parameters[1] as Mode, true); + setConfig("mode", challenge.parameters[1] as Mode, { + nosave: true, + }); if (challenge.parameters[3] !== undefined) { - setConfig("difficulty", challenge.parameters[3] as Difficulty, true); + setConfig("difficulty", challenge.parameters[3] as Difficulty, { + nosave: true, + }); } } else if (challenge.type === "special") { if (challenge.name === "semimak") { // so can you make a link that sets up 120s, 10k, punct, stop on word, and semimak as the layout? - setConfig("mode", "time", true); - setConfig("time", 120, true); - setConfig("language", "english_10k", true); - setConfig("punctuation", true, true); - setConfig("stopOnError", "word", true); - setConfig("layout", "semimak", true); - setConfig("keymapLayout", "overrideSync", true); - setConfig("keymapMode", "static", true); + setConfig("mode", "time", { + nosave: true, + }); + setConfig("time", 120, { + nosave: true, + }); + setConfig("language", "english_10k", { + nosave: true, + }); + setConfig("punctuation", true, { + nosave: true, + }); + setConfig("stopOnError", "word", { + nosave: true, + }); + setConfig("layout", "semimak", { + nosave: true, + }); + setConfig("keymapLayout", "overrideSync", { + nosave: true, + }); + setConfig("keymapMode", "static", { + nosave: true, + }); } } ManualRestart.set(); diff --git a/frontend/src/ts/controllers/theme-controller.ts b/frontend/src/ts/controllers/theme-controller.ts index dcce1d1b2033..827314b11f81 100644 --- a/frontend/src/ts/controllers/theme-controller.ts +++ b/frontend/src/ts/controllers/theme-controller.ts @@ -352,7 +352,9 @@ export async function randomizeTheme(): Promise { randomTheme = "custom"; } - setConfig("customTheme", false, true); + setConfig("customTheme", false, { + nosave: true, + }); await apply(randomTheme, colorsOverride); if (randomThemeIndex >= themesList.length) { diff --git a/frontend/src/ts/elements/custom-background-filter.ts b/frontend/src/ts/elements/custom-background-filter.ts index 23e98fa8e560..9e056e761c4d 100644 --- a/frontend/src/ts/elements/custom-background-filter.ts +++ b/frontend/src/ts/elements/custom-background-filter.ts @@ -130,7 +130,7 @@ const debouncedSave = debounce(2000, async () => { const arr = Object.keys(filters).map( (filterKey) => filters[filterKey as keyof typeof filters].value, ) as CustomBackgroundFilter; - setConfig("customBackgroundFilter", arr, false); + setConfig("customBackgroundFilter", arr); }); ConfigEvent.subscribe(({ key, newValue }) => { diff --git a/frontend/src/ts/elements/input-validation.ts b/frontend/src/ts/elements/input-validation.ts index 69b987787e2f..21316ae46025 100644 --- a/frontend/src/ts/elements/input-validation.ts +++ b/frontend/src/ts/elements/input-validation.ts @@ -294,7 +294,7 @@ export function handleConfigInput({ if (Config[configName] === value) { return; } - const didConfigSave = setConfig(configName, value, false); + const didConfigSave = setConfig(configName, value); if (didConfigSave) { Notifications.add("Saved", 1, { diff --git a/frontend/src/ts/elements/settings/settings-group.ts b/frontend/src/ts/elements/settings/settings-group.ts index 9eecde00ca1a..832c60f0743e 100644 --- a/frontend/src/ts/elements/settings/settings-group.ts +++ b/frontend/src/ts/elements/settings/settings-group.ts @@ -46,7 +46,9 @@ export default class SettingsGroup { this.configName = configName; this.mode = mode; this.configFunction = (param, nosave) => - setConfig(configName, param as ConfigType[K], nosave); + setConfig(configName, param as ConfigType[K], { + nosave: nosave ?? false, + }); this.setCallback = options?.setCallback; this.updateCallback = options?.updateCallback; this.validation = options?.validation; diff --git a/frontend/src/ts/modals/mobile-test-config.ts b/frontend/src/ts/modals/mobile-test-config.ts index 00dab9fc37a6..8cd475107f6a 100644 --- a/frontend/src/ts/modals/mobile-test-config.ts +++ b/frontend/src/ts/modals/mobile-test-config.ts @@ -153,7 +153,7 @@ async function setup(modalEl: HTMLElement): Promise { arr = [len]; } - if (setConfig("quoteLength", arr, false)) { + if (setConfig("quoteLength", arr)) { ManualRestart.set(); TestLogic.restart(); } diff --git a/frontend/src/ts/modals/quote-search.ts b/frontend/src/ts/modals/quote-search.ts index ae7b1eed7b7e..14e7c043a1c4 100644 --- a/frontend/src/ts/modals/quote-search.ts +++ b/frontend/src/ts/modals/quote-search.ts @@ -363,7 +363,7 @@ function apply(val: number): void { ); } if (val !== null && !isNaN(val) && val >= 0) { - setConfig("quoteLength", [-2], false); + setConfig("quoteLength", [-2]); TestState.setSelectedQuoteId(val); ManualRestart.set(); } else { diff --git a/frontend/src/ts/test/funbox/funbox-functions.ts b/frontend/src/ts/test/funbox/funbox-functions.ts index b97c55185fc5..4810b36fbc74 100644 --- a/frontend/src/ts/test/funbox/funbox-functions.ts +++ b/frontend/src/ts/test/funbox/funbox-functions.ts @@ -207,7 +207,9 @@ const list: Partial> = { }, simon_says: { applyConfig(): void { - setConfig("keymapMode", "next", true); + setConfig("keymapMode", "next", { + nosave: true, + }); }, rememberSettings(): void { save("keymapMode", Config.keymapMode); @@ -215,7 +217,9 @@ const list: Partial> = { }, tts: { applyConfig(): void { - setConfig("keymapMode", "off", true); + setConfig("keymapMode", "off", { + nosave: true, + }); }, rememberSettings(): void { save("keymapMode", Config.keymapMode); @@ -368,8 +372,12 @@ const list: Partial> = { if (Config.layout === "default") { layout = "qwerty"; } - setConfig("layout", layout, true); - setConfig("keymapLayout", "overrideSync", true); + setConfig("layout", layout, { + nosave: true, + }); + setConfig("keymapLayout", "overrideSync", { + nosave: true, + }); }, rememberSettings(): void { save("keymapMode", Config.keymapMode); @@ -380,8 +388,12 @@ const list: Partial> = { applyConfig(): void { const layout = Config.customLayoutfluid[0] ?? "qwerty"; - setConfig("layout", layout as Layout, true); - setConfig("keymapLayout", layout as KeymapLayout, true); + setConfig("layout", layout as Layout, { + nosave: true, + }); + setConfig("keymapLayout", layout as KeymapLayout, { + nosave: true, + }); }, rememberSettings(): void { save("keymapMode", Config.keymapMode); @@ -485,9 +497,13 @@ const list: Partial> = { memory: { applyConfig(): void { $("#wordsWrapper").addClass("hidden"); - setConfig("showAllLines", true, true); + setConfig("showAllLines", true, { + nosave: true, + }); if (Config.keymapMode === "next") { - setConfig("keymapMode", "react", true); + setConfig("keymapMode", "react", { + nosave: true, + }); } }, rememberSettings(): void { @@ -684,7 +700,9 @@ const list: Partial> = { if (languages.length === 1) { const lang = languages[0] as LanguageObject; - setConfig("language", lang.name, true); + setConfig("language", lang.name, { + nosave: true, + }); toggleFunbox("polyglot", true); Notifications.add( `Disabled polyglot funbox because only one valid language was found. Check your polyglot languages config (${Config.customPolyglot.join( diff --git a/frontend/src/ts/test/funbox/funbox-memory.ts b/frontend/src/ts/test/funbox/funbox-memory.ts index 886b72b50762..5fc953eeb945 100644 --- a/frontend/src/ts/test/funbox/funbox-memory.ts +++ b/frontend/src/ts/test/funbox/funbox-memory.ts @@ -20,7 +20,9 @@ export function save( settingsMemory[settingName] ??= { value, setFunction: (param, noSave?) => - setConfig(settingName, param as Config[T], noSave), + setConfig(settingName, param as Config[T], { + nosave: noSave ?? false, + }), }; } diff --git a/frontend/src/ts/test/funbox/funbox.ts b/frontend/src/ts/test/funbox/funbox.ts index 58bd506ff322..f3e20af3a687 100644 --- a/frontend/src/ts/test/funbox/funbox.ts +++ b/frontend/src/ts/test/funbox/funbox.ts @@ -38,7 +38,7 @@ export function setFunbox(funbox: FunboxName[]): boolean { } } FunboxMemory.load(); - setConfig("funbox", funbox, false); + setConfig("funbox", funbox); return true; } @@ -104,7 +104,9 @@ export async function activate( ), -1, ); - setConfig("funbox", [], true); + setConfig("funbox", [], { + nosave: true, + }); await clear(); return false; } @@ -123,7 +125,9 @@ export async function activate( Misc.createErrorMessage(error, "Failed to activate funbox"), -1, ); - setConfig("funbox", [], true); + setConfig("funbox", [], { + nosave: true, + }); await clear(); return false; } @@ -134,7 +138,9 @@ export async function activate( "Current language does not support this funbox mode", 0, ); - setConfig("funbox", [], true); + setConfig("funbox", [], { + nosave: true, + }); await clear(); return; } @@ -188,7 +194,9 @@ export async function activate( -1, ); } - setConfig("funbox", [], true); + setConfig("funbox", [], { + nosave: true, + }); await clear(); return; } diff --git a/frontend/src/ts/test/practise-words.ts b/frontend/src/ts/test/practise-words.ts index ba1f678a7f96..f7d0342cd0d9 100644 --- a/frontend/src/ts/test/practise-words.ts +++ b/frontend/src/ts/test/practise-words.ts @@ -148,7 +148,9 @@ export function init( customText = CustomText.getData(); } - setConfig("mode", "custom", true); + setConfig("mode", "custom", { + nosave: true, + }); CustomText.setPipeDelimiter(true); CustomText.setText(newCustomText); CustomText.setLimitMode("section"); diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 754dae2d8838..5a8d449e81be 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -497,9 +497,11 @@ async function init(): Promise { important: true, }, ); - setConfig("lazyMode", false, false); + setConfig("lazyMode", false); } else if (rememberLazyMode && anySupportsLazyMode) { - setConfig("lazyMode", true, true); + setConfig("lazyMode", true, { + nosave: true, + }); } } else { // normal mode @@ -510,9 +512,11 @@ async function init(): Promise { important: true, }); - setConfig("lazyMode", false, false); + setConfig("lazyMode", false); } else if (rememberLazyMode && !language.noLazyMode) { - setConfig("lazyMode", true, true); + setConfig("lazyMode", true, { + nosave: true, + }); } } @@ -1559,7 +1563,7 @@ $(".pageTest").on("click", "#testConfig .quoteLength .textButton", (e) => { arr = [len]; } - if (setConfig("quoteLength", arr, false)) { + if (setConfig("quoteLength", arr)) { ManualRestart.set(); restart(); } @@ -1595,7 +1599,9 @@ ConfigEvent.subscribe(({ key, newValue, nosave }) => { if (key === "language") { //automatically enable lazy mode for arabic if ((newValue as string)?.startsWith("arabic") && ArabicLazyMode.get()) { - setConfig("lazyMode", true, true); + setConfig("lazyMode", true, { + nosave: true, + }); } restart(); } diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts index 55eb5f641c9a..17c2a46dba0b 100644 --- a/frontend/src/ts/test/test-timer.ts +++ b/frontend/src/ts/test/test-timer.ts @@ -127,8 +127,12 @@ function layoutfluid(): void { if (Config.layout !== layout && layout !== undefined) { LayoutfluidFunboxTimer.hide(); - setConfig("layout", layout as Layout, true); - setConfig("keymapLayout", layout as KeymapLayout, true); + setConfig("layout", layout as Layout, { + nosave: true, + }); + setConfig("keymapLayout", layout as KeymapLayout, { + nosave: true, + }); } } if (timerDebug) console.timeEnd("layoutfluid"); diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index 0cbab7c8660b..5a94abd004e5 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -181,18 +181,24 @@ export function loadTestSettingsFromUrl(getOverride?: string): void { const applied: Record = {}; if (de[0] !== null) { - setConfig("mode", de[0], true); + setConfig("mode", de[0], { + nosave: true, + }); applied["mode"] = de[0]; } const mode = de[0] ?? Config.mode; if (de[1] !== null) { if (mode === "time") { - setConfig("time", parseInt(de[1], 10), true); + setConfig("time", parseInt(de[1], 10), { + nosave: true, + }); } else if (mode === "words") { - setConfig("words", parseInt(de[1], 10), true); + setConfig("words", parseInt(de[1], 10), { + nosave: true, + }); } else if (mode === "quote") { - setConfig("quoteLength", [-2], false); + setConfig("quoteLength", [-2]); TestState.setSelectedQuoteId(parseInt(de[1], 10)); ManualRestart.set(); } @@ -236,22 +242,30 @@ export function loadTestSettingsFromUrl(getOverride?: string): void { } if (de[3] !== null) { - setConfig("punctuation", de[3], true); + setConfig("punctuation", de[3], { + nosave: true, + }); applied["punctuation"] = de[3] ? "on" : "off"; } if (de[4] !== null) { - setConfig("numbers", de[4], true); + setConfig("numbers", de[4], { + nosave: true, + }); applied["numbers"] = de[4] ? "on" : "off"; } if (de[5] !== null) { - setConfig("language", de[5] as Language, true); + setConfig("language", de[5] as Language, { + nosave: true, + }); applied["language"] = de[5]; } if (de[6] !== null) { - setConfig("difficulty", de[6], true); + setConfig("difficulty", de[6], { + nosave: true, + }); applied["difficulty"] = de[6]; } @@ -263,7 +277,9 @@ export function loadTestSettingsFromUrl(getOverride?: string): void { } else { val = de[7]; } - setConfig("funbox", val, true); + setConfig("funbox", val, { + nosave: true, + }); applied["funbox"] = val.join(", "); }