mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2026-02-04 14:39:02 +08:00
fix(funbox): caret/tape/ui issues in backwards funbox (@NadAlaba) (#6956)
### Description 1. refactor: - store (language direction / state of direction reversing funbox) in test-state.ts, and set them on `TestLogic.init()` which is called on each restart which happens on each change of Config.language or funbox. - use these new direction variables in (caret.ts / test-ui.ts / pace-caret.ts /result-word-highlight.ts) instead of calling `await JSONData.getCurrentLanguage(Config.language)`. 2. css changes: - add `unicode-bidi: bidi-override;` to .words with ligatures in backwards to fix the direction of LTR words on LTR languages in custom tests (which now have `.withLigatures` class regardless of language). - remove `direction: rtl;` from right to left .word and keep it on right to left #words. This was done because after adding the above `bidi-override`, (.word)s directioin was being forced to rtl on tests with RTL language and RTL words (custom and none custom tests), which is wrong (should be ltr on those tests because of the backwards funbox). - P.S., removing this from .word does not affect normal tests, because .word direction is inherited from #words directtion on non .withLigatures tests (e.g, non custom tests in non withLigatures languages), and it is calculated using internal browser algorithm based on characters used in .withLigatures tests (tests in languages with ligatures and all custom tests). 3. add the property "reverseDirection" to backwards funbox, which signifies that the direction of the test should be the reverse of the direction of Config.language, and the direction of a word should be the reverse of `Strings.isWordRightToLeft()`. 4. allow backwards funbox to work on languages with ligatures. 5. move `void Caret.updatePosition()` call to after the call of `TestUI.lineJump()` in `input-controller.ts:handleSpace()`. 6. change name of `Strings.getWordDirection()` to `Strings.isWordRightToLeft()` which explains what does the returned boolean mean, and add a parameter `reverseDirection` that flips the final result if true. --------- Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
5026f41954
commit
4318601799
14 changed files with 114 additions and 98 deletions
|
|
@ -270,7 +270,7 @@ describe("string utils", () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe("getWordDirection", () => {
|
||||
describe("isWordRightToLeft", () => {
|
||||
beforeEach(() => {
|
||||
Strings.clearWordDirectionCache();
|
||||
});
|
||||
|
|
@ -321,13 +321,27 @@ describe("string utils", () => {
|
|||
languageRTL: boolean,
|
||||
_description: string
|
||||
) => {
|
||||
expect(Strings.getWordDirection(word, languageRTL)).toBe(expected);
|
||||
expect(Strings.isWordRightToLeft(word, languageRTL)).toBe(expected);
|
||||
}
|
||||
);
|
||||
|
||||
it("should return languageRTL for undefined word", () => {
|
||||
expect(Strings.getWordDirection(undefined, false)).toBe(false);
|
||||
expect(Strings.getWordDirection(undefined, true)).toBe(true);
|
||||
expect(Strings.isWordRightToLeft(undefined, false)).toBe(false);
|
||||
expect(Strings.isWordRightToLeft(undefined, true)).toBe(true);
|
||||
});
|
||||
|
||||
// testing reverseDirection
|
||||
it("should return true for LTR word with reversed direction", () => {
|
||||
expect(Strings.isWordRightToLeft("hello", false, true)).toBe(true);
|
||||
expect(Strings.isWordRightToLeft("hello", true, true)).toBe(true);
|
||||
});
|
||||
it("should return false for RTL word with reversed direction", () => {
|
||||
expect(Strings.isWordRightToLeft("مرحبا", true, true)).toBe(false);
|
||||
expect(Strings.isWordRightToLeft("مرحبا", false, true)).toBe(false);
|
||||
});
|
||||
it("should return reverse of languageRTL for undefined word with reversed direction", () => {
|
||||
expect(Strings.isWordRightToLeft(undefined, false, true)).toBe(true);
|
||||
expect(Strings.isWordRightToLeft(undefined, true, true)).toBe(false);
|
||||
});
|
||||
|
||||
describe("caching", () => {
|
||||
|
|
@ -349,7 +363,7 @@ describe("string utils", () => {
|
|||
|
||||
it("should use cache for repeated calls", () => {
|
||||
// First call should cache the result (cache miss)
|
||||
const result1 = Strings.getWordDirection("hello", false);
|
||||
const result1 = Strings.isWordRightToLeft("hello", false);
|
||||
expect(result1).toBe(false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("hello", false);
|
||||
|
||||
|
|
@ -358,7 +372,7 @@ describe("string utils", () => {
|
|||
mapSetSpy.mockClear();
|
||||
|
||||
// Second call should use cache (cache hit)
|
||||
const result2 = Strings.getWordDirection("hello", false);
|
||||
const result2 = Strings.isWordRightToLeft("hello", false);
|
||||
expect(result2).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("hello");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled(); // Should not set again
|
||||
|
|
@ -367,7 +381,7 @@ describe("string utils", () => {
|
|||
mapGetSpy.mockClear();
|
||||
mapSetSpy.mockClear();
|
||||
|
||||
const result3 = Strings.getWordDirection("hello", true);
|
||||
const result3 = Strings.isWordRightToLeft("hello", true);
|
||||
expect(result3).toBe(false); // Still false because "hello" is LTR regardless of language
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("hello");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled(); // Should not set again
|
||||
|
|
@ -375,7 +389,7 @@ describe("string utils", () => {
|
|||
|
||||
it("should cache based on core word without punctuation", () => {
|
||||
// First call should cache the result for core "hello"
|
||||
const result1 = Strings.getWordDirection("hello", false);
|
||||
const result1 = Strings.isWordRightToLeft("hello", false);
|
||||
expect(result1).toBe(false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("hello", false);
|
||||
|
||||
|
|
@ -383,7 +397,7 @@ describe("string utils", () => {
|
|||
mapSetSpy.mockClear();
|
||||
|
||||
// These should all use the same cache entry since they have the same core
|
||||
const result2 = Strings.getWordDirection("hello!", false);
|
||||
const result2 = Strings.isWordRightToLeft("hello!", false);
|
||||
expect(result2).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("hello");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled();
|
||||
|
|
@ -391,7 +405,7 @@ describe("string utils", () => {
|
|||
mapGetSpy.mockClear();
|
||||
mapSetSpy.mockClear();
|
||||
|
||||
const result3 = Strings.getWordDirection("!hello", false);
|
||||
const result3 = Strings.isWordRightToLeft("!hello", false);
|
||||
expect(result3).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("hello");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled();
|
||||
|
|
@ -399,7 +413,7 @@ describe("string utils", () => {
|
|||
mapGetSpy.mockClear();
|
||||
mapSetSpy.mockClear();
|
||||
|
||||
const result4 = Strings.getWordDirection("!hello!", false);
|
||||
const result4 = Strings.isWordRightToLeft("!hello!", false);
|
||||
expect(result4).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("hello");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled();
|
||||
|
|
@ -407,7 +421,7 @@ describe("string utils", () => {
|
|||
|
||||
it("should handle cache clearing", () => {
|
||||
// Cache a result
|
||||
Strings.getWordDirection("test", false);
|
||||
Strings.isWordRightToLeft("test", false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("test", false);
|
||||
|
||||
// Clear cache
|
||||
|
|
@ -419,14 +433,14 @@ describe("string utils", () => {
|
|||
mapClearSpy.mockClear();
|
||||
|
||||
// Should work normally after cache clear (cache miss again)
|
||||
const result = Strings.getWordDirection("test", false);
|
||||
const result = Strings.isWordRightToLeft("test", false);
|
||||
expect(result).toBe(false);
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("test", false);
|
||||
});
|
||||
|
||||
it("should demonstrate cache miss vs cache hit behavior", () => {
|
||||
// Test cache miss - first time seeing this word
|
||||
const result1 = Strings.getWordDirection("unique", false);
|
||||
const result1 = Strings.isWordRightToLeft("unique", false);
|
||||
expect(result1).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("unique");
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("unique", false);
|
||||
|
|
@ -435,7 +449,7 @@ describe("string utils", () => {
|
|||
mapSetSpy.mockClear();
|
||||
|
||||
// Test cache hit - same word again
|
||||
const result2 = Strings.getWordDirection("unique", false);
|
||||
const result2 = Strings.isWordRightToLeft("unique", false);
|
||||
expect(result2).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("unique");
|
||||
expect(mapSetSpy).not.toHaveBeenCalled(); // No cache set on hit
|
||||
|
|
@ -444,7 +458,7 @@ describe("string utils", () => {
|
|||
mapSetSpy.mockClear();
|
||||
|
||||
// Test cache miss - different word
|
||||
const result3 = Strings.getWordDirection("different", false);
|
||||
const result3 = Strings.isWordRightToLeft("different", false);
|
||||
expect(result3).toBe(false);
|
||||
expect(mapGetSpy).toHaveBeenCalledWith("different");
|
||||
expect(mapSetSpy).toHaveBeenCalledWith("different", false);
|
||||
|
|
|
|||
|
|
@ -307,10 +307,6 @@
|
|||
&.rightToLeftTest {
|
||||
//flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
|
||||
direction: rtl;
|
||||
.word {
|
||||
//flex-direction: row-reverse;
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
&.withLigatures {
|
||||
.word {
|
||||
|
|
@ -749,10 +745,6 @@
|
|||
&.rightToLeftTest {
|
||||
//flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
|
||||
direction: rtl;
|
||||
.word {
|
||||
//flex-direction: row-reverse;
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
&.withLigatures {
|
||||
.word {
|
||||
|
|
|
|||
|
|
@ -326,7 +326,6 @@ async function handleSpace(): Promise<void> {
|
|||
void TestLogic.addWord();
|
||||
}
|
||||
TestUI.updateActiveElement();
|
||||
void Caret.updatePosition();
|
||||
|
||||
const shouldLimitToThreeLines =
|
||||
Config.mode === "time" ||
|
||||
|
|
@ -344,8 +343,10 @@ async function handleSpace(): Promise<void> {
|
|||
|
||||
if ((nextTop ?? 0) > currentTop) {
|
||||
void TestUI.lineJump(currentTop);
|
||||
} //end of line wrap
|
||||
}
|
||||
}
|
||||
} //end of line wrap
|
||||
|
||||
void Caret.updatePosition();
|
||||
|
||||
// enable if i decide that auto tab should also work after a space
|
||||
// if (
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@
|
|||
// Constants for padding around the highlights
|
||||
|
||||
import * as Misc from "../utils/misc";
|
||||
import * as JSONData from "../utils/json-data";
|
||||
import Config from "../config";
|
||||
import * as TestState from "../test/test-state";
|
||||
|
||||
const PADDING_X = 16;
|
||||
const PADDING_Y = 12;
|
||||
|
|
@ -56,7 +55,6 @@ let isInitialized = false;
|
|||
let isHoveringChart = false;
|
||||
let isFirstHighlightSinceInit = true;
|
||||
let isFirstHighlightSinceClear = true;
|
||||
let isLanguageRightToLeft = false;
|
||||
let isInitInProgress = false;
|
||||
|
||||
// Highlights .word elements in range [firstWordIndex, lastWordIndex]
|
||||
|
|
@ -104,7 +102,7 @@ export async function highlightWordsInRange(
|
|||
const newHighlightElementPositions = getHighlightElementPositions(
|
||||
firstWordIndex,
|
||||
lastWordIndex,
|
||||
isLanguageRightToLeft
|
||||
TestState.isLanguageRightToLeft
|
||||
);
|
||||
|
||||
// For each line...
|
||||
|
|
@ -198,10 +196,6 @@ async function init(): Promise<boolean> {
|
|||
);
|
||||
}
|
||||
|
||||
// Set isLanguageRTL
|
||||
const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
|
||||
isLanguageRightToLeft = currentLanguage.rightToLeft ?? false;
|
||||
|
||||
RWH_el = $("#resultWordsHistory")[0] as HTMLElement;
|
||||
RWH_rect = RWH_el.getBoundingClientRect();
|
||||
wordEls = $(RWH_el).find(".words .word[input]");
|
||||
|
|
@ -309,7 +303,7 @@ async function init(): Promise<boolean> {
|
|||
|
||||
// For RTL languages, account for difference between highlightContainer left and RWH_el left
|
||||
let RTL_offset;
|
||||
if (isLanguageRightToLeft) {
|
||||
if (TestState.isLanguageRightToLeft) {
|
||||
RTL_offset = line.rect.left - RWH_rect.left + PADDING_X;
|
||||
} else {
|
||||
RTL_offset = 0;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import * as JSONData from "../utils/json-data";
|
||||
import Config from "../config";
|
||||
import * as TestInput from "./test-input";
|
||||
import * as SlowTimer from "../states/slow-timer";
|
||||
import * as TestState from "../test/test-state";
|
||||
import * as TestWords from "./test-words";
|
||||
import { convertRemToPixels } from "../utils/numbers";
|
||||
import { splitIntoCharacters, getWordDirection } from "../utils/strings";
|
||||
import { splitIntoCharacters, isWordRightToLeft } from "../utils/strings";
|
||||
import { safeNumber } from "@monkeytype/util/numbers";
|
||||
import { subscribe } from "../observables/config-event";
|
||||
|
||||
|
|
@ -53,7 +52,6 @@ function getSpaceWidth(wordElement?: HTMLElement): number {
|
|||
|
||||
function getTargetPositionLeft(
|
||||
fullWidthCaret: boolean,
|
||||
isLanguageRightToLeft: boolean,
|
||||
activeWordElement: HTMLElement,
|
||||
currentWordNodeList: NodeListOf<HTMLElement>,
|
||||
fullWidthCaretWidth: number,
|
||||
|
|
@ -65,9 +63,10 @@ function getTargetPositionLeft(
|
|||
let result = 0;
|
||||
|
||||
// use word-specific direction if available and different from language direction
|
||||
const isWordRightToLeft = getWordDirection(
|
||||
const isWordRTL = isWordRightToLeft(
|
||||
currentWord,
|
||||
isLanguageRightToLeft
|
||||
TestState.isLanguageRightToLeft,
|
||||
TestState.isDirectionReversed
|
||||
);
|
||||
|
||||
if (Config.tapeMode === "off") {
|
||||
|
|
@ -77,7 +76,7 @@ function getTargetPositionLeft(
|
|||
const lastWordLetter = currentWordNodeList[wordLen - 1];
|
||||
const lastInputLetter = currentWordNodeList[inputLen - 1];
|
||||
|
||||
if (isWordRightToLeft) {
|
||||
if (isWordRTL) {
|
||||
if (inputLen <= wordLen && currentLetter) {
|
||||
// at word beginning in zen mode both lengths are 0, but currentLetter is defined "_"
|
||||
positionOffsetToWord =
|
||||
|
|
@ -110,13 +109,10 @@ function getTargetPositionLeft(
|
|||
$(document.querySelector("#wordsWrapper") as HTMLElement).width() ?? 0;
|
||||
const tapeMargin =
|
||||
wordsWrapperWidth *
|
||||
(isWordRightToLeft
|
||||
? 1 - Config.tapeMargin / 100
|
||||
: Config.tapeMargin / 100);
|
||||
(isWordRTL ? 1 - Config.tapeMargin / 100 : Config.tapeMargin / 100);
|
||||
|
||||
result =
|
||||
tapeMargin -
|
||||
(fullWidthCaret && isWordRightToLeft ? fullWidthCaretWidth : 0);
|
||||
tapeMargin - (fullWidthCaret && isWordRTL ? fullWidthCaretWidth : 0);
|
||||
|
||||
if (Config.tapeMode === "word" && inputLen > 0) {
|
||||
let currentWordWidth = 0;
|
||||
|
|
@ -131,7 +127,7 @@ function getTargetPositionLeft(
|
|||
// if current letter has zero width move the caret to previous positive width letter
|
||||
if ($(currentWordNodeList[inputLen] as Element).outerWidth(true) === 0)
|
||||
currentWordWidth -= lastPositiveLetterWidth;
|
||||
if (isWordRightToLeft) currentWordWidth *= -1;
|
||||
if (isWordRTL) currentWordWidth *= -1;
|
||||
result += currentWordWidth;
|
||||
}
|
||||
}
|
||||
|
|
@ -185,9 +181,6 @@ export async function updatePosition(noAnim = false): Promise<void> {
|
|||
const lastInputLetter = currentWordNodeList[inputLen - 1];
|
||||
const lastWordLetter = currentWordNodeList[wordLen - 1];
|
||||
|
||||
const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
|
||||
const isLanguageRightToLeft = currentLanguage.rightToLeft ?? false;
|
||||
|
||||
// in blind mode, and hide extra letters, extra letters have zero offsets
|
||||
// offsetHeight is the same for all visible letters
|
||||
// so is offsetTop (for same line letters)
|
||||
|
|
@ -224,7 +217,6 @@ export async function updatePosition(noAnim = false): Promise<void> {
|
|||
|
||||
const letterPosLeft = getTargetPositionLeft(
|
||||
fullWidthCaret,
|
||||
isLanguageRightToLeft,
|
||||
activeWordEl,
|
||||
currentWordNodeList,
|
||||
letterWidth,
|
||||
|
|
|
|||
|
|
@ -4,12 +4,11 @@ import Config from "../config";
|
|||
import * as DB from "../db";
|
||||
import * as SlowTimer from "../states/slow-timer";
|
||||
import * as Misc from "../utils/misc";
|
||||
import * as JSONData from "../utils/json-data";
|
||||
import * as TestState from "./test-state";
|
||||
import * as ConfigEvent from "../observables/config-event";
|
||||
import { convertRemToPixels } from "../utils/numbers";
|
||||
import { getActiveFunboxes } from "./funbox/list";
|
||||
import { getWordDirection } from "../utils/strings";
|
||||
import { isWordRightToLeft } from "../utils/strings";
|
||||
|
||||
type Settings = {
|
||||
wpm: number;
|
||||
|
|
@ -51,22 +50,18 @@ async function resetCaretPosition(): Promise<void> {
|
|||
|
||||
if (firstLetter === undefined || firstLetterHeight === undefined) return;
|
||||
|
||||
const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
|
||||
const isLanguageRightToLeft = currentLanguage.rightToLeft;
|
||||
|
||||
const currentWord = TestWords.words.get(settings?.currentWordIndex ?? 0);
|
||||
|
||||
const isWordRightToLeft = getWordDirection(
|
||||
const isWordRTL = isWordRightToLeft(
|
||||
currentWord,
|
||||
isLanguageRightToLeft ?? false
|
||||
TestState.isLanguageRightToLeft,
|
||||
TestState.isDirectionReversed
|
||||
);
|
||||
|
||||
caret.stop(true, true).animate(
|
||||
{
|
||||
top: firstLetter.offsetTop - firstLetterHeight / 4,
|
||||
left:
|
||||
firstLetter.offsetLeft +
|
||||
(isWordRightToLeft ? firstLetter.offsetWidth : 0),
|
||||
left: firstLetter.offsetLeft + (isWordRTL ? firstLetter.offsetWidth : 0),
|
||||
},
|
||||
0,
|
||||
"linear"
|
||||
|
|
@ -238,17 +233,14 @@ export async function update(expectedStepEnd: number): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
const currentLanguage = await JSONData.getCurrentLanguage(
|
||||
Config.language
|
||||
);
|
||||
const isLanguageRightToLeft = currentLanguage.rightToLeft;
|
||||
|
||||
const currentWord = TestWords.words.get(settings.currentWordIndex);
|
||||
|
||||
const isWordRightToLeft = getWordDirection(
|
||||
const isWordRTL = isWordRightToLeft(
|
||||
currentWord,
|
||||
isLanguageRightToLeft ?? false
|
||||
TestState.isLanguageRightToLeft,
|
||||
TestState.isDirectionReversed
|
||||
);
|
||||
|
||||
newTop =
|
||||
word.offsetTop +
|
||||
currentLetter.offsetTop -
|
||||
|
|
@ -258,13 +250,13 @@ export async function update(expectedStepEnd: number): Promise<void> {
|
|||
word.offsetLeft +
|
||||
currentLetter.offsetLeft -
|
||||
caretWidth / 2 +
|
||||
(isWordRightToLeft ? currentLetterWidth : 0);
|
||||
(isWordRTL ? currentLetterWidth : 0);
|
||||
} else {
|
||||
newLeft =
|
||||
word.offsetLeft +
|
||||
currentLetter.offsetLeft -
|
||||
caretWidth / 2 +
|
||||
(isWordRightToLeft ? 0 : currentLetterWidth);
|
||||
(isWordRTL ? 0 : currentLetterWidth);
|
||||
}
|
||||
caret.removeClass("hidden");
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ import {
|
|||
getActiveFunboxes,
|
||||
getActiveFunboxesWithFunction,
|
||||
isFunboxActive,
|
||||
isFunboxActiveWithProperty,
|
||||
} from "./funbox/list";
|
||||
import { getFunbox } from "@monkeytype/funbox";
|
||||
import * as CompositionState from "../states/composition";
|
||||
|
|
@ -408,7 +409,7 @@ let lastInitError: Error | null = null;
|
|||
let rememberLazyMode: boolean;
|
||||
let testReinitCount = 0;
|
||||
|
||||
export async function init(): Promise<boolean> {
|
||||
async function init(): Promise<boolean> {
|
||||
console.debug("Initializing test");
|
||||
testReinitCount++;
|
||||
if (testReinitCount > 3) {
|
||||
|
|
@ -571,6 +572,13 @@ export async function init(): Promise<boolean> {
|
|||
Funbox.toggleScript(TestWords.words.getCurrent());
|
||||
TestUI.setRightToLeft(language.rightToLeft ?? false);
|
||||
TestUI.setLigatures(language.ligatures ?? false);
|
||||
|
||||
const isLanguageRTL = language.rightToLeft ?? false;
|
||||
TestState.setIsLanguageRightToLeft(isLanguageRTL);
|
||||
TestState.setIsDirectionReversed(
|
||||
isFunboxActiveWithProperty("reverseDirection")
|
||||
);
|
||||
|
||||
TestUI.showWords();
|
||||
console.debug("Test initialized with words", generatedWords);
|
||||
console.debug(
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ export let selectedQuoteId = 1;
|
|||
export let activeWordIndex = 0;
|
||||
export let testInitSuccess = true;
|
||||
export let lineScrollDistance = 0;
|
||||
export let isLanguageRightToLeft = false;
|
||||
export let isDirectionReversed = false;
|
||||
|
||||
export function setRepeated(tf: boolean): void {
|
||||
isRepeated = tf;
|
||||
|
|
@ -58,3 +60,11 @@ export function setTestInitSuccess(tf: boolean): void {
|
|||
export function setLineScrollDistance(val: number): void {
|
||||
lineScrollDistance = val;
|
||||
}
|
||||
|
||||
export function setIsLanguageRightToLeft(rtl: boolean): void {
|
||||
isLanguageRightToLeft = rtl;
|
||||
}
|
||||
|
||||
export function setIsDirectionReversed(val: boolean): void {
|
||||
isDirectionReversed = val;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ export function updateActiveElement(
|
|||
|
||||
activeWordTop = newActiveWord.offsetTop;
|
||||
|
||||
void updateWordsInputPosition();
|
||||
updateWordsInputPosition();
|
||||
|
||||
if (!initial && Config.tapeMode !== "off") {
|
||||
void scrollTape();
|
||||
|
|
@ -272,8 +272,11 @@ async function joinOverlappingHints(
|
|||
activeWordLetters: NodeListOf<Element>,
|
||||
hintElements: HTMLCollection
|
||||
): Promise<void> {
|
||||
const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
|
||||
const isLanguageRTL = currentLanguage.rightToLeft;
|
||||
const isWordRightToLeft = Strings.isWordRightToLeft(
|
||||
TestWords.words.getCurrent(),
|
||||
TestState.isLanguageRightToLeft,
|
||||
TestState.isDirectionReversed
|
||||
);
|
||||
|
||||
let previousBlocksAdjacent = false;
|
||||
let currentHintBlock = 0;
|
||||
|
|
@ -305,8 +308,8 @@ async function joinOverlappingHints(
|
|||
|
||||
const sameTop = block1Letter1.offsetTop === block2Letter1.offsetTop;
|
||||
|
||||
const leftBlock = isLanguageRTL ? hintBlock2 : hintBlock1;
|
||||
const rightBlock = isLanguageRTL ? hintBlock1 : hintBlock2;
|
||||
const leftBlock = isWordRightToLeft ? hintBlock2 : hintBlock1;
|
||||
const rightBlock = isWordRightToLeft ? hintBlock1 : hintBlock2;
|
||||
|
||||
// block edge is offset half its width because of transform: translate(-50%)
|
||||
const leftBlockEnds = leftBlock.offsetLeft + leftBlock.offsetWidth / 2;
|
||||
|
|
@ -321,7 +324,7 @@ async function joinOverlappingHints(
|
|||
|
||||
const block1Letter1Pos =
|
||||
block1Letter1.offsetLeft +
|
||||
(isLanguageRTL ? block1Letter1.offsetWidth : 0);
|
||||
(isWordRightToLeft ? block1Letter1.offsetWidth : 0);
|
||||
const bothBlocksLettersWidthHalved =
|
||||
hintBlock2.offsetLeft - hintBlock1.offsetLeft;
|
||||
hintBlock1.style.left =
|
||||
|
|
@ -510,15 +513,16 @@ export function appendEmptyWordElement(
|
|||
);
|
||||
}
|
||||
let updateWordsInputPositionAnimationFrameId: null | number = null;
|
||||
export async function updateWordsInputPosition(): Promise<void> {
|
||||
export function updateWordsInputPosition(): void {
|
||||
if (updateWordsInputPositionAnimationFrameId !== null) {
|
||||
cancelAnimationFrame(updateWordsInputPositionAnimationFrameId);
|
||||
}
|
||||
updateWordsInputPositionAnimationFrameId = requestAnimationFrame(async () => {
|
||||
updateWordsInputPositionAnimationFrameId = requestAnimationFrame(() => {
|
||||
updateWordsInputPositionAnimationFrameId = null;
|
||||
if (ActivePage.get() !== "test") return;
|
||||
const currentLanguage = await JSONData.getCurrentLanguage(Config.language);
|
||||
const isLanguageRTL = currentLanguage.rightToLeft;
|
||||
const isTestRightToLeft = TestState.isDirectionReversed
|
||||
? !TestState.isLanguageRightToLeft
|
||||
: TestState.isLanguageRightToLeft;
|
||||
|
||||
const el = document.querySelector<HTMLElement>("#wordsInput");
|
||||
|
||||
|
|
@ -549,7 +553,7 @@ export async function updateWordsInputPosition(): Promise<void> {
|
|||
|
||||
el.style.top = targetTop + "px";
|
||||
|
||||
if (activeWord.offsetWidth < letterHeight && isLanguageRTL) {
|
||||
if (activeWord.offsetWidth < letterHeight && isTestRightToLeft) {
|
||||
el.style.left = activeWord.offsetLeft - letterHeight + "px";
|
||||
} else {
|
||||
el.style.left = Math.max(0, activeWord.offsetLeft) + "px";
|
||||
|
|
@ -913,8 +917,9 @@ export async function scrollTape(
|
|||
|
||||
await centeringActiveLine;
|
||||
|
||||
const currentLang = await JSONData.getCurrentLanguage(Config.language);
|
||||
const isLanguageRTL = currentLang.rightToLeft;
|
||||
const isTestRightToLeft = TestState.isDirectionReversed
|
||||
? !TestState.isLanguageRightToLeft
|
||||
: TestState.isLanguageRightToLeft;
|
||||
|
||||
const wordsWrapperWidth = (
|
||||
document.querySelector("#wordsWrapper") as HTMLElement
|
||||
|
|
@ -988,8 +993,8 @@ export async function scrollTape(
|
|||
const forWordLeft = Math.floor(child.offsetLeft);
|
||||
const forWordWidth = Math.floor(child.offsetWidth);
|
||||
if (
|
||||
(!isLanguageRTL && forWordLeft < 0 - forWordWidth) ||
|
||||
(isLanguageRTL && forWordLeft > wordsWrapperWidth)
|
||||
(!isTestRightToLeft && forWordLeft < 0 - forWordWidth) ||
|
||||
(isTestRightToLeft && forWordLeft > wordsWrapperWidth)
|
||||
) {
|
||||
toRemove.push(child);
|
||||
widthRemoved += wordOuterWidth;
|
||||
|
|
@ -1035,7 +1040,7 @@ export async function scrollTape(
|
|||
currentLineIndent - (widthRemovedFromLine[i] ?? 0)
|
||||
}px`;
|
||||
}
|
||||
if (isLanguageRTL) widthRemoved *= -1;
|
||||
if (isTestRightToLeft) widthRemoved *= -1;
|
||||
const currentWordsMargin = parseFloat(wordsEl.style.marginLeft) || 0;
|
||||
wordsEl.style.marginLeft = `${currentWordsMargin + widthRemoved}px`;
|
||||
}
|
||||
|
|
@ -1068,7 +1073,7 @@ export async function scrollTape(
|
|||
wordsWrapperWidth * (Config.tapeMargin / 100) -
|
||||
wordsWidthBeforeActive -
|
||||
currentWordWidth;
|
||||
if (isLanguageRTL) newMargin = wordRightMargin - newMargin;
|
||||
if (isTestRightToLeft) newMargin = wordRightMargin - newMargin;
|
||||
|
||||
const jqWords = $(wordsEl);
|
||||
if (Config.smoothLineScroll) {
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ const debouncedEvent = debounce(250, () => {
|
|||
void TestUI.updateHintsPositionDebounced();
|
||||
}
|
||||
setTimeout(() => {
|
||||
void TestUI.updateWordsInputPosition();
|
||||
TestUI.updateWordsInputPosition();
|
||||
TestUI.focusWords();
|
||||
}, 250);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,26 +236,29 @@ export function clearWordDirectionCache(): void {
|
|||
wordDirectionCache.clear();
|
||||
}
|
||||
|
||||
export function getWordDirection(
|
||||
export function isWordRightToLeft(
|
||||
word: string | undefined,
|
||||
languageRTL: boolean
|
||||
languageRTL: boolean,
|
||||
reverseDirection?: boolean
|
||||
): boolean {
|
||||
if (word === undefined || word.length === 0) return languageRTL;
|
||||
if (word === undefined || word.length === 0) {
|
||||
return reverseDirection ? !languageRTL : languageRTL;
|
||||
}
|
||||
|
||||
// Strip leading/trailing punctuation and whitespace so attached opposite-direction
|
||||
// punctuation like "word؟" or "،word" doesn't flip the direction detection
|
||||
// and if only punctuation/symbols/whitespace, use main language direction
|
||||
const core = word.replace(/^[\p{P}\p{S}\s]+|[\p{P}\p{S}\s]+$/gu, "");
|
||||
if (core.length === 0) return languageRTL;
|
||||
if (core.length === 0) return reverseDirection ? !languageRTL : languageRTL;
|
||||
|
||||
// cache by core to handle variants like "word" vs "word؟"
|
||||
const cached = wordDirectionCache.get(core);
|
||||
if (cached !== undefined) return cached;
|
||||
if (cached !== undefined) return reverseDirection ? !cached : cached;
|
||||
|
||||
const result = hasRTLCharacters(core);
|
||||
wordDirectionCache.set(core, result);
|
||||
|
||||
return result;
|
||||
return reverseDirection ? !result : result;
|
||||
}
|
||||
|
||||
// Export testing utilities for unit tests
|
||||
|
|
|
|||
|
|
@ -3,5 +3,9 @@
|
|||
}
|
||||
|
||||
#words.rightToLeftTest {
|
||||
direction: rtl;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
#words.withLigatures .word {
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -404,9 +404,9 @@ const list: Record<FunboxName, FunboxMetadata> = {
|
|||
name: "backwards",
|
||||
properties: [
|
||||
"hasCssFile",
|
||||
"noLigatures",
|
||||
"conflictsWithSymmetricChars",
|
||||
"wordOrder:reverse",
|
||||
"reverseDirection",
|
||||
],
|
||||
canGetPb: true,
|
||||
frontendFunctions: ["alterText"],
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export type FunboxProperty =
|
|||
| "noLigatures"
|
||||
| `toPush:${number}`
|
||||
| "wordOrder:reverse"
|
||||
| "reverseDirection"
|
||||
| "ignoreReducedMotion";
|
||||
|
||||
type FunboxCSSModification = "typingTest" | "words" | "body" | "main";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue