diff --git a/frontend/src/ts/elements/keymap.ts b/frontend/src/ts/elements/keymap.ts index ed58159af..2e5cc93a8 100644 --- a/frontend/src/ts/elements/keymap.ts +++ b/frontend/src/ts/elements/keymap.ts @@ -9,6 +9,10 @@ import * as Hangul from "hangul-js"; import * as Notifications from "../elements/notifications"; import * as ActivePage from "../states/active-page"; import * as TestWords from "../test/test-words"; +import { capsState } from "../test/caps-warning"; +import * as ShiftTracker from "../test/shift-tracker"; +import * as AltTracker from "../test/alt-tracker"; +import * as KeyConverter from "../utils/key-converter"; const stenoKeys: JSONData.Layout = { keymapShowTopRow: true, @@ -402,6 +406,112 @@ export async function refresh( } } +const isMacLike = /Mac|iPod|iPhone|iPad/.test(navigator.platform); +const symbolsPattern = /^[^\p{L}\p{N}]{1}$/u; +type KeymapLegendStates = [letters: 0 | 1 | 2 | 3, symbols: 0 | 1 | 2 | 3]; +let keymapLegendStates: KeymapLegendStates = [0, 0]; + +function getLegendStates(): KeymapLegendStates | undefined { + // MacOS has different CapsLock and Shift logic than other operating systems + // Windows and Linux only capitalize letters if either Shift OR CapsLock are + // pressed, but not both at once. + // MacOS instead capitalizes when either or both are pressed, + // so we have to check for that. + const shiftState = ShiftTracker.leftState || ShiftTracker.rightState; + const altState = AltTracker.leftState || AltTracker.rightState; + + const osDependentLettersState = isMacLike + ? shiftState || capsState + : shiftState !== capsState; + + const lettersState = (osDependentLettersState ? 1 : 0) + (altState ? 2 : 0); + const symbolsState = (shiftState ? 1 : 0) + (altState ? 2 : 0); + + const [previousLettersState, previousSymbolsState] = keymapLegendStates; + + if ( + previousLettersState === lettersState && + previousSymbolsState === symbolsState + ) { + return; + } + + keymapLegendStates = [ + lettersState as 0 | 1 | 2 | 3, + symbolsState as 0 | 1 | 2 | 3, + ]; + return keymapLegendStates; +} + +async function updateLegends(): Promise { + const states = getLegendStates(); + if (states === undefined) return; + + const keymapKeys = [...document.getElementsByClassName("keymapKey")].filter( + (el) => { + const isKeymapKey = el.classList.contains("keymapKey"); + const isNotSpace = !el.classList.contains("keySpace"); + + return isKeymapKey && isNotSpace; + } + ) as HTMLElement[]; + + const layoutKeys = keymapKeys.map((el) => el.dataset["key"]); + if (layoutKeys.includes(undefined)) return; + + const keys = keymapKeys.map((el) => el.childNodes[1]); + + const [lettersState, symbolsState] = states; + + const layoutName = + Config.keymapLayout === "overrideSync" + ? Config.layout === "default" + ? "qwerty" + : Config.layout + : Config.keymapLayout; + + const layout = await JSONData.getLayout(layoutName).catch(() => undefined); + if (layout === undefined) { + Notifications.add("Failed to load keymap layout", -1); + + return; + } + + for (let i = 0; i < layoutKeys.length; i++) { + const layoutKey = layoutKeys[i] as string; + const key = keys[i]; + const lowerCaseCharacter = layoutKey[0]; + const upperCaseCharacter = layoutKey[1]; + + if ( + key === undefined || + layoutKey === undefined || + lowerCaseCharacter === undefined || + upperCaseCharacter === undefined + ) + continue; + + const keyIsSymbol = [lowerCaseCharacter, upperCaseCharacter].some( + (character) => symbolsPattern.test(character ?? "") + ); + + const keycode = KeyConverter.layoutKeyToKeycode(lowerCaseCharacter, layout); + if (keycode === undefined) { + return; + } + const oppositeShift = ShiftTracker.isUsingOppositeShift(keycode); + + const state = keyIsSymbol ? symbolsState : lettersState; + const characterIndex = oppositeShift ? state : 0; + + //if the character at the index is undefined, try without alt + const character = + layoutKey[characterIndex] ?? layoutKey[characterIndex - 2]; + + key.textContent = character ?? ""; + } +} + ConfigEvent.subscribe((eventKey, newValue) => { if (eventKey === "layout" && Config.keymapLayout === "overrideSync") { void refresh(Config.keymapLayout); @@ -427,3 +537,27 @@ KeymapEvent.subscribe((mode, key, correct) => { void flashKey(key, correct); } }); + +$(document).on("keydown", (e) => { + if ( + Config.keymapLegendStyle === "dynamic" && + (e.code === "ShiftLeft" || + e.code === "ShiftRight" || + e.code === "AltRight" || + e.code === "AltLeft") + ) { + void updateLegends(); + } +}); + +$(document).on("keyup", (e) => { + if ( + Config.keymapLegendStyle === "dynamic" && + (e.code === "ShiftLeft" || + e.code === "ShiftRight" || + e.code === "AltRight" || + e.code === "AltLeft") + ) { + void updateLegends(); + } +}); diff --git a/frontend/src/ts/test/alt-tracker.ts b/frontend/src/ts/test/alt-tracker.ts new file mode 100644 index 000000000..edf80e459 --- /dev/null +++ b/frontend/src/ts/test/alt-tracker.ts @@ -0,0 +1,23 @@ +export let leftState = false; +export let rightState = false; + +$(document).on("keydown", (e) => { + if (e.code === "AltLeft") { + leftState = true; + } else if (e.code === "AltRight") { + rightState = true; + } +}); + +$(document).on("keyup", (e) => { + if (e.code === "AltLeft") { + leftState = false; + } else if (e.code === "AltRight") { + rightState = false; + } +}); + +export function reset(): void { + leftState = false; + rightState = false; +} diff --git a/frontend/src/ts/test/shift-tracker.ts b/frontend/src/ts/test/shift-tracker.ts index d6347c56b..d60dad90a 100644 --- a/frontend/src/ts/test/shift-tracker.ts +++ b/frontend/src/ts/test/shift-tracker.ts @@ -1,109 +1,9 @@ import Config from "../config"; -import * as JSONData from "../utils/json-data"; -import { capsState } from "./caps-warning"; -import * as Notifications from "../elements/notifications"; import * as KeyConverter from "../utils/key-converter"; export let leftState = false; export let rightState = false; -type KeymapLegendStates = [letters: boolean, symbols: boolean]; - -const symbolsPattern = /^[^\p{L}\p{N}]{1}$/u; - -const isMacLike = /Mac|iPod|iPhone|iPad/.test(navigator.platform); - -let keymapLegendStates: KeymapLegendStates = [false, false]; -function getLegendStates(): KeymapLegendStates | undefined { - const symbolsState = leftState || rightState; - // MacOS has different CapsLock and Shift logic than other operating systems - // Windows and Linux only capitalize letters if either Shift OR CapsLock are - // pressed, but not both at once. - // MacOS instead capitalizes when either or both are pressed, - // so we have to check for that. - const lettersState = isMacLike - ? symbolsState || capsState - : symbolsState !== capsState; - - const [previousLettersState, previousSymbolsState] = keymapLegendStates; - - if ( - previousLettersState === lettersState && - previousSymbolsState === symbolsState - ) { - return; - } - - return (keymapLegendStates = [lettersState, symbolsState]); -} - -async function updateKeymapLegendCasing(): Promise { - const states = getLegendStates(); - if (states === undefined) return; - - const keymapKeys = [...document.getElementsByClassName("keymapKey")].filter( - (el) => { - const isKeymapKey = el.classList.contains("keymapKey"); - const isNotSpace = !el.classList.contains("keySpace"); - - return isKeymapKey && isNotSpace; - } - ) as HTMLElement[]; - - const layoutKeys = keymapKeys.map((el) => el.dataset["key"]); - if (layoutKeys.includes(undefined)) return; - - const keys = keymapKeys.map((el) => el.childNodes[1]); - - const [lettersState, symbolsState] = states; - - const layoutName = - Config.keymapLayout === "overrideSync" - ? Config.layout === "default" - ? "qwerty" - : Config.layout - : Config.keymapLayout; - - const layout = await JSONData.getLayout(layoutName).catch(() => undefined); - if (layout === undefined) { - Notifications.add("Failed to load keymap layout", -1); - - return; - } - - for (let i = 0; i < layoutKeys.length; i++) { - const layoutKey = layoutKeys[i] as string; - const key = keys[i]; - const lowerCaseCharacter = layoutKey[0]; - const upperCaseCharacter = layoutKey[1]; - - if ( - key === undefined || - layoutKey === undefined || - lowerCaseCharacter === undefined || - upperCaseCharacter === undefined - ) - continue; - - const keyIsSymbol = [lowerCaseCharacter, upperCaseCharacter].some( - (character) => symbolsPattern.test(character ?? "") - ); - - const keycode = KeyConverter.layoutKeyToKeycode(lowerCaseCharacter, layout); - if (keycode === undefined) { - return; - } - const oppositeShift = isUsingOppositeShift(keycode); - - const state = keyIsSymbol ? symbolsState : lettersState; - const capitalize = oppositeShift && state; - const keyIndex = Number(capitalize); - const character = layoutKey[keyIndex]; - - key.textContent = character ?? ""; - } -} - $(document).on("keydown", (e) => { if (e.code === "ShiftLeft") { leftState = true; @@ -112,10 +12,6 @@ $(document).on("keydown", (e) => { leftState = false; rightState = true; } - - if (Config.keymapLegendStyle === "dynamic") { - void updateKeymapLegendCasing(); - } }); $(document).on("keyup", (e) => { @@ -123,10 +19,6 @@ $(document).on("keyup", (e) => { leftState = false; rightState = false; } - - if (Config.keymapLegendStyle === "dynamic") { - void updateKeymapLegendCasing(); - } }); export function reset(): void { diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 6338ed2b9..ae24c63cd 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -13,6 +13,7 @@ import * as CustomTextState from "../states/custom-text-name"; import * as TestStats from "./test-stats"; import * as PractiseWords from "./practise-words"; import * as ShiftTracker from "./shift-tracker"; +import * as AltTracker from "./alt-tracker"; import * as Focus from "./focus"; import * as Funbox from "./funbox/funbox"; import * as Keymap from "../elements/keymap"; @@ -255,6 +256,7 @@ export function restart(options = {} as RestartOptions): void { TestInput.restart(); TestInput.corrected.reset(); ShiftTracker.reset(); + AltTracker.reset(); Caret.hide(); TestState.setActive(false); Replay.stopReplayRecording();