Skip to content

Commit ef5263d

Browse files
authored
refactor: refactor iteration over funboxes (@fehmer) (monkeytypegame#6275)
1 parent ba7bf22 commit ef5263d

File tree

10 files changed

+211
-122
lines changed

10 files changed

+211
-122
lines changed

frontend/src/ts/controllers/account-controller.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import { navigate } from "./route-controller";
4848
import { FirebaseError } from "firebase/app";
4949
import * as PSA from "../elements/psa";
5050
import defaultResultFilters from "../constants/default-result-filters";
51-
import { getActiveFunboxes } from "../test/funbox/list";
51+
import { getFunctionsFromActiveFunboxes } from "../test/funbox/list";
5252

5353
export const gmailProvider = new GoogleAuthProvider();
5454
export const githubProvider = new GithubAuthProvider();
@@ -174,8 +174,10 @@ async function getDataAndInit(): Promise<boolean> {
174174
UpdateConfig.saveFullConfigToLocalStorage(true);
175175

176176
//funboxes might be different and they wont activate on the account page
177-
for (const fb of getActiveFunboxes()) {
178-
fb.functions?.applyGlobalCSS?.();
177+
for (const applyGlobalCSS of getFunctionsFromActiveFunboxes(
178+
"applyGlobalCSS"
179+
)) {
180+
applyGlobalCSS();
179181
}
180182
}
181183
AccountButton.loading(false);

frontend/src/ts/controllers/input-controller.ts

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ import { ModifierKeys } from "../constants/modifier-keys";
3535
import { navigate } from "./route-controller";
3636
import * as Loader from "../elements/loader";
3737
import * as KeyConverter from "../utils/key-converter";
38-
import { getActiveFunboxes } from "../test/funbox/list";
38+
import {
39+
findSingleActiveFunboxWithFunction,
40+
getFunctionsFromActiveFunboxes,
41+
isFunboxActiveWithProperty,
42+
} from "../test/funbox/list";
3943

4044
let dontInsertSpace = false;
4145
let correctShiftUsed = true;
@@ -145,7 +149,7 @@ function backspaceToPrevious(): void {
145149

146150
TestInput.input.current = TestInput.input.popHistory();
147151
TestInput.corrected.popHistory();
148-
if (getActiveFunboxes().find((f) => f.properties?.includes("nospace"))) {
152+
if (isFunboxActiveWithProperty("nospace")) {
149153
TestInput.input.current = TestInput.input.current.slice(0, -1);
150154
setWordsInput(" " + TestInput.input.current + " ");
151155
}
@@ -194,8 +198,8 @@ async function handleSpace(): Promise<void> {
194198

195199
const currentWord: string = TestWords.words.getCurrent();
196200

197-
for (const fb of getActiveFunboxes()) {
198-
fb.functions?.handleSpace?.();
201+
for (const handleSpace of getFunctionsFromActiveFunboxes("handleSpace")) {
202+
handleSpace();
199203
}
200204

201205
dontInsertSpace = true;
@@ -204,9 +208,7 @@ async function handleSpace(): Promise<void> {
204208
void LiveBurst.update(Math.round(burst));
205209
TestInput.pushBurstToHistory(burst);
206210

207-
const nospace =
208-
getActiveFunboxes().find((f) => f.properties?.includes("nospace")) !==
209-
undefined;
211+
const nospace = isFunboxActiveWithProperty("nospace");
210212

211213
//correct word or in zen mode
212214
const isWordCorrect: boolean =
@@ -406,8 +408,8 @@ function isCharCorrect(char: string, charIndex: number): boolean {
406408
return true;
407409
}
408410

409-
const funbox = getActiveFunboxes().find((fb) => fb.functions?.isCharCorrect);
410-
if (funbox?.functions?.isCharCorrect) {
411+
const funbox = findSingleActiveFunboxWithFunction("isCharCorrect");
412+
if (funbox) {
411413
return funbox.functions.isCharCorrect(char, originalChar);
412414
}
413415

@@ -492,15 +494,11 @@ function handleChar(
492494

493495
const isCharKorean: boolean = TestInput.input.getKoreanStatus();
494496

495-
for (const fb of getActiveFunboxes()) {
496-
if (fb.functions?.handleChar) {
497-
char = fb.functions.handleChar(char);
498-
}
497+
for (const handleChar of getFunctionsFromActiveFunboxes("handleChar")) {
498+
char = handleChar(char);
499499
}
500500

501-
const nospace =
502-
getActiveFunboxes().find((f) => f.properties?.includes("nospace")) !==
503-
undefined;
501+
const nospace = isFunboxActiveWithProperty("nospace") !== undefined;
504502

505503
if (char !== "\n" && char !== "\t" && /\s/.test(char)) {
506504
if (nospace) return;
@@ -904,10 +902,8 @@ $(document).on("keydown", async (event) => {
904902
return;
905903
}
906904

907-
for (const fb of getActiveFunboxes()) {
908-
if (fb.functions?.handleKeydown) {
909-
void fb.functions.handleKeydown(event);
910-
}
905+
for (const handleKeydown of getFunctionsFromActiveFunboxes("handleKeydown")) {
906+
void handleKeydown(event);
911907
}
912908

913909
//autofocus
@@ -1157,20 +1153,20 @@ $(document).on("keydown", async (event) => {
11571153
}
11581154
}
11591155

1160-
for (const fb of getActiveFunboxes()) {
1161-
if (fb.functions?.preventDefaultEvent) {
1162-
if (
1163-
await fb.functions.preventDefaultEvent(
1164-
//i cant figure this type out, but it works fine
1165-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
1166-
event as JQuery.KeyDownEvent
1167-
)
1168-
) {
1169-
event.preventDefault();
1170-
handleChar(event.key, TestInput.input.current.length);
1171-
updateUI();
1172-
setWordsInput(" " + TestInput.input.current);
1173-
}
1156+
for (const preventDefaultEvent of getFunctionsFromActiveFunboxes(
1157+
"preventDefaultEvent"
1158+
)) {
1159+
if (
1160+
await preventDefaultEvent(
1161+
//i cant figure this type out, but it works fine
1162+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
1163+
event as JQuery.KeyDownEvent
1164+
)
1165+
) {
1166+
event.preventDefault();
1167+
handleChar(event.key, TestInput.input.current.length);
1168+
updateUI();
1169+
setWordsInput(" " + TestInput.input.current);
11741170
}
11751171
}
11761172

frontend/src/ts/ready.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as AccountButton from "./elements/account-button";
77
//@ts-expect-error
88
import Konami from "konami";
99
import * as ServerConfiguration from "./ape/server-configuration";
10-
import { getActiveFunboxes } from "./test/funbox/list";
10+
import { getFunctionsFromActiveFunboxes } from "./test/funbox/list";
1111
import { loadPromise } from "./config";
1212

1313
$(async (): Promise<void> => {
@@ -19,8 +19,10 @@ $(async (): Promise<void> => {
1919
$("body").css("transition", "background .25s, transform .05s");
2020
MerchBanner.showIfNotClosedBefore();
2121

22-
for (const fb of getActiveFunboxes()) {
23-
fb.functions?.applyGlobalCSS?.();
22+
for (const applyGlobalCSS of getFunctionsFromActiveFunboxes(
23+
"applyGlobalCSS"
24+
)) {
25+
applyGlobalCSS();
2426
}
2527

2628
$("#app")

frontend/src/ts/test/funbox/funbox.ts

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,28 @@ import * as FunboxMemory from "./funbox-memory";
99
import { HighlightMode } from "@monkeytype/contracts/schemas/configs";
1010
import { Mode } from "@monkeytype/contracts/schemas/shared";
1111
import { FunboxName, checkCompatibility } from "@monkeytype/funbox";
12-
import { getActiveFunboxes, getActiveFunboxNames, get } from "./list";
12+
import {
13+
getActiveFunboxes,
14+
getActiveFunboxNames,
15+
get,
16+
getFunctionsFromActiveFunboxes,
17+
isFunboxActiveWithProperty,
18+
getActiveFunboxesWithProperty,
19+
} from "./list";
1320
import { checkForcedConfig } from "./funbox-validation";
1421

1522
export function toggleScript(...params: string[]): void {
1623
if (Config.funbox === "none") return;
1724

18-
for (const fb of getActiveFunboxes()) {
19-
fb.functions?.toggleScript?.(params);
25+
for (const toggleScript of getFunctionsFromActiveFunboxes("toggleScript")) {
26+
toggleScript(params);
2027
}
2128
}
2229

2330
export function setFunbox(funbox: string): boolean {
2431
if (funbox === "none") {
25-
for (const fb of getActiveFunboxes()) {
26-
fb.functions?.clearGlobal?.();
32+
for (const clearGlobal of getFunctionsFromActiveFunboxes("clearGlobal")) {
33+
clearGlobal();
2734
}
2835
}
2936
FunboxMemory.load();
@@ -125,9 +132,7 @@ export async function activate(funbox?: string): Promise<boolean | undefined> {
125132
}
126133

127134
if (language.ligatures) {
128-
if (
129-
getActiveFunboxes().find((f) => f.properties?.includes("noLigatures"))
130-
) {
135+
if (isFunboxActiveWithProperty("noLigatures")) {
131136
Notifications.add(
132137
"Current language does not support this funbox mode",
133138
0
@@ -194,16 +199,18 @@ export async function activate(funbox?: string): Promise<boolean | undefined> {
194199
}
195200

196201
ManualRestart.set();
197-
for (const fb of getActiveFunboxes()) {
198-
fb.functions?.applyConfig?.();
202+
for (const applyConfig of getFunctionsFromActiveFunboxes("applyConfig")) {
203+
applyConfig();
199204
}
200205
// ModesNotice.update();
201206
return true;
202207
}
203208

204209
export async function rememberSettings(): Promise<void> {
205-
for (const fb of getActiveFunboxes()) {
206-
fb.functions?.rememberSettings?.();
210+
for (const rememberSettings of getFunctionsFromActiveFunboxes(
211+
"rememberSettings"
212+
)) {
213+
rememberSettings();
207214
}
208215
}
209216

@@ -220,11 +227,7 @@ async function setFunboxBodyClasses(): Promise<boolean> {
220227
?.split(/\s+/)
221228
.filter((it) => !it.startsWith("fb-")) ?? [];
222229

223-
if (
224-
getActiveFunboxes().some((it) =>
225-
it.properties?.includes("ignoreReducedMotion")
226-
)
227-
) {
230+
if (isFunboxActiveWithProperty("ignoreReducedMotion")) {
228231
currentClasses.push("ignore-reduced-motion");
229232
}
230233

@@ -238,14 +241,12 @@ async function setFunboxBodyClasses(): Promise<boolean> {
238241

239242
async function applyFunboxCSS(): Promise<boolean> {
240243
$(".funBoxTheme").remove();
241-
for (const funbox of getActiveFunboxes()) {
242-
if (funbox.properties?.includes("hasCssFile")) {
243-
const css = document.createElement("link");
244-
css.classList.add("funBoxTheme");
245-
css.rel = "stylesheet";
246-
css.href = "funbox/" + funbox.name + ".css";
247-
document.head.appendChild(css);
248-
}
244+
for (const funbox of getActiveFunboxesWithProperty("hasCssFile")) {
245+
const css = document.createElement("link");
246+
css.classList.add("funBoxTheme");
247+
css.rel = "stylesheet";
248+
css.href = "funbox/" + funbox.name + ".css";
249+
document.head.appendChild(css);
249250
}
250251
return true;
251252
}

frontend/src/ts/test/funbox/list.ts

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,102 @@ export function getActiveFunboxNames(): FunboxName[] {
5959
return stringToFunboxNames(Config.funbox);
6060
}
6161

62+
/**
63+
* Get all active funboxes defining the given property
64+
* @param property
65+
* @returns list of matching funboxes, empty list if none matching
66+
*/
6267
export function getActiveFunboxesWithProperty(
6368
property: FunboxProperty
6469
): FunboxMetadataWithFunctions[] {
6570
return getActiveFunboxes().filter((fb) => fb.properties?.includes(property));
6671
}
6772

68-
export function getActiveFunboxesWithFunction(
73+
/**
74+
* Find a single active funbox defining the given property
75+
* @param property
76+
* @returns the active funbox if any, `undefined` otherwise.
77+
* @throws Error if there are multiple funboxes defining the given property
78+
*/
79+
export function findSingleActiveFunboxWithProperty(
80+
property: FunboxProperty
81+
): FunboxMetadataWithFunctions | undefined {
82+
const matching = getActiveFunboxesWithProperty(property);
83+
if (matching.length == 0) return undefined;
84+
if (matching.length == 1) return matching[0];
85+
throw new Error(
86+
`Expecting exactly one funbox with property "${property} but found ${matching.length}`
87+
);
88+
}
89+
90+
/**
91+
* Check if there is an active funbox with the given property name
92+
* @param property property name
93+
* @returns
94+
*/
95+
export function isFunboxActiveWithProperty(property: FunboxProperty): boolean {
96+
return getActiveFunboxesWithProperty(property).length > 0;
97+
}
98+
99+
type MandatoryFunboxFunction<F extends keyof FunboxFunctions> = Exclude<
100+
FunboxFunctions[F],
101+
undefined
102+
>;
103+
type FunboxWithFunction<F extends keyof FunboxFunctions> =
104+
FunboxMetadataWithFunctions & {
105+
functions: Record<F, MandatoryFunboxFunction<F>>;
106+
};
107+
108+
/**
109+
* Get all active funboxes implementing the given function
110+
* @param functionName function name
111+
* @returns list of matching funboxes, empty list if none matching
112+
*/
113+
export function getActiveFunboxesWithFunction<F extends keyof FunboxFunctions>(
114+
functionName: F
115+
): FunboxWithFunction<F>[] {
116+
return getActiveFunboxes().filter(
117+
(fb) => fb.functions?.[functionName]
118+
) as FunboxWithFunction<F>[];
119+
}
120+
121+
/**
122+
* Get requested, implemented functions from all active funboxes
123+
* @param functionName name of the function
124+
* @returns array of each implemented requested function of all active funboxes
125+
*/
126+
export function getFunctionsFromActiveFunboxes<F extends keyof FunboxFunctions>(
127+
functionName: F
128+
): MandatoryFunboxFunction<F>[] {
129+
return getActiveFunboxesWithFunction(functionName).map(
130+
(it) => it.functions[functionName]
131+
);
132+
}
133+
134+
/**
135+
* Check if there is an active funbox implemenging the given function
136+
* @param functionName function name
137+
* @returns
138+
*/
139+
export function isFunboxActiveWithFunction(
69140
functionName: keyof FunboxFunctions
70-
): FunboxMetadataWithFunctions[] {
71-
return getActiveFunboxes().filter((fb) => fb.functions?.[functionName]);
141+
): boolean {
142+
return getActiveFunboxesWithFunction(functionName).length > 0;
143+
}
144+
145+
/**
146+
* Find a single active funbox implementing the given function name
147+
* @param functionName
148+
* @returns the active funbox if any, `undefined` otherwise.
149+
* @throws Error if there are multiple funboxes implementing the function name
150+
*/
151+
export function findSingleActiveFunboxWithFunction<
152+
F extends keyof FunboxFunctions
153+
>(functionName: F): FunboxWithFunction<F> | undefined {
154+
const matching = getActiveFunboxesWithFunction(functionName);
155+
if (matching.length == 0) return undefined;
156+
if (matching.length == 1) return matching[0] as FunboxWithFunction<F>;
157+
throw new Error(
158+
`Expecting exactly one funbox implementing "${functionName} but found ${matching.length}`
159+
);
72160
}

frontend/src/ts/test/result.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ import type {
3737
} from "chartjs-plugin-annotation";
3838
import Ape from "../ape";
3939
import { CompletedEvent } from "@monkeytype/contracts/schemas/results";
40-
import { getActiveFunboxes, getFromString } from "./funbox/list";
40+
import {
41+
getActiveFunboxes,
42+
getFromString,
43+
isFunboxActiveWithProperty,
44+
} from "./funbox/list";
4145
import { getFunboxesFromString } from "@monkeytype/funbox";
4246

4347
let result: CompletedEvent;
@@ -678,10 +682,7 @@ function updateTestType(randomQuote: Quote | null): void {
678682
testType += " " + ["short", "medium", "long", "thicc"][randomQuote.group];
679683
}
680684
}
681-
const ignoresLanguage =
682-
getActiveFunboxes().find((f) =>
683-
f.properties?.includes("ignoresLanguage")
684-
) !== undefined;
685+
const ignoresLanguage = isFunboxActiveWithProperty("ignoresLanguage");
685686
if (Config.mode !== "custom" && !ignoresLanguage) {
686687
testType += "<br>" + Strings.getLanguageDisplayString(result.language);
687688
}

0 commit comments

Comments
 (0)