Skip to content

Commit 35bbcbf

Browse files
committed
impr(tape mode): support RTL languages (@NadAlaba)
1 parent 77008c8 commit 35bbcbf

File tree

5 files changed

+75
-55
lines changed

5 files changed

+75
-55
lines changed

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ function updateUI(): void {
110110
}
111111
}
112112

113-
function backspaceToPrevious(): void {
113+
async function backspaceToPrevious(): Promise<void> {
114114
if (!TestState.isActive) return;
115115

116116
if (
@@ -152,7 +152,7 @@ function backspaceToPrevious(): void {
152152
}
153153
TestWords.words.decreaseCurrentIndex();
154154
TestUI.setActiveWordElementIndex(TestUI.activeWordElementIndex - 1);
155-
TestUI.updateActiveElement(true);
155+
await TestUI.updateActiveElement(true);
156156
Funbox.toggleScript(TestWords.words.getCurrent());
157157
void TestUI.updateActiveWordLetters();
158158

@@ -337,7 +337,7 @@ async function handleSpace(): Promise<void> {
337337
}
338338
}
339339
TestUI.setActiveWordElementIndex(TestUI.activeWordElementIndex + 1);
340-
TestUI.updateActiveElement();
340+
await TestUI.updateActiveElement();
341341
void Caret.updatePosition();
342342

343343
if (
@@ -1050,7 +1050,7 @@ $(document).on("keydown", async (event) => {
10501050
Monkey.type();
10511051

10521052
if (event.key === "Backspace" && TestInput.input.current.length === 0) {
1053-
backspaceToPrevious();
1053+
await backspaceToPrevious();
10541054
if (TestInput.input.current) {
10551055
setWordsInput(" " + TestInput.input.current + " ");
10561056
}
@@ -1247,7 +1247,7 @@ $("#wordsInput").on("beforeinput", (event) => {
12471247
}
12481248
});
12491249

1250-
$("#wordsInput").on("input", (event) => {
1250+
$("#wordsInput").on("input", async (event) => {
12511251
if (!event.originalEvent?.isTrusted || TestUI.testRestarting) {
12521252
(event.target as HTMLInputElement).value = " ";
12531253
return;
@@ -1316,7 +1316,7 @@ $("#wordsInput").on("input", (event) => {
13161316

13171317
if (realInputValue.length === 0 && currTestInput.length === 0) {
13181318
// fallback for when no Backspace keydown event (mobile)
1319-
backspaceToPrevious();
1319+
await backspaceToPrevious();
13201320
} else if (inputValue.length < currTestInput.length) {
13211321
if (containsChinese) {
13221322
if (

frontend/src/ts/test/caret.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,17 @@ function getTargetPositionLeft(
112112

113113
if (Config.tapeMode === "word" && inputLen > 0) {
114114
let currentWordWidth = 0;
115+
let lastPositiveLetterWidth = 0;
115116
for (let i = 0; i < inputLen; i++) {
116117
if (invisibleExtraLetters && i >= wordLen) break;
117-
currentWordWidth +=
118-
$(currentWordNodeList[i] as HTMLElement).outerWidth(true) ?? 0;
118+
const letterOuterWidth =
119+
$(currentWordNodeList[i] as Element).outerWidth(true) ?? 0;
120+
currentWordWidth += letterOuterWidth;
121+
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
119122
}
123+
// if current letter has zero width move the caret to previous positive width letter
124+
if ($(currentWordNodeList[inputLen] as Element).outerWidth(true) === 0)
125+
currentWordWidth -= lastPositiveLetterWidth;
120126
if (isLanguageRightToLeft) currentWordWidth *= -1;
121127
result += currentWordWidth;
122128
}

frontend/src/ts/test/test-logic.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -420,13 +420,6 @@ export async function init(): Promise<void> {
420420
}
421421
}
422422

423-
if (Config.tapeMode !== "off" && language.rightToLeft) {
424-
Notifications.add("This language does not support tape mode.", 0, {
425-
important: true,
426-
});
427-
UpdateConfig.setTapeMode("off");
428-
}
429-
430423
const allowLazyMode = !language.noLazyMode || Config.mode === "custom";
431424
if (Config.lazyMode && !allowLazyMode) {
432425
rememberLazyMode = true;
@@ -509,7 +502,7 @@ export async function init(): Promise<void> {
509502
Funbox.toggleScript(TestWords.words.getCurrent());
510503
TestUI.setRightToLeft(language.rightToLeft);
511504
TestUI.setLigatures(language.ligatures ?? false);
512-
TestUI.showWords();
505+
await TestUI.showWords();
513506
console.debug("Test initialized with words", generatedWords);
514507
console.debug(
515508
"Test initialized with section indexes",

frontend/src/ts/test/test-ui.ts

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ const debouncedZipfCheck = debounce(250, async () => {
139139
}
140140
});
141141

142-
ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
142+
ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => {
143143
if (
144144
(eventKey === "language" || eventKey === "funbox") &&
145145
Config.funbox.split("#").includes("zipf")
@@ -149,7 +149,7 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
149149
if (eventKey === "fontSize" && !nosave) {
150150
OutOfFocus.hide();
151151
updateWordsWrapperHeight(true);
152-
updateWordsMargin();
152+
await updateWordsMargin();
153153
void updateWordsInputPosition(true);
154154
}
155155
if (
@@ -166,7 +166,7 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
166166

167167
if (eventValue === undefined) return;
168168
if (eventKey === "highlightMode") {
169-
if (ActivePage.get() === "test") updateActiveElement();
169+
if (ActivePage.get() === "test") await updateActiveElement();
170170
}
171171

172172
if (
@@ -178,7 +178,7 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
178178
"hideExtraLetters",
179179
].includes(eventKey)
180180
) {
181-
updateWordWrapperClasses();
181+
await updateWordWrapperClasses();
182182
}
183183

184184
if (eventKey === "showAllLines") {
@@ -246,10 +246,10 @@ export function blurWords(): void {
246246
$("#wordsInput").trigger("blur");
247247
}
248248

249-
export function updateActiveElement(
249+
export async function updateActiveElement(
250250
backspace?: boolean,
251251
initial = false
252-
): void {
252+
): Promise<void> {
253253
const active = document.querySelector("#words .active");
254254
if (!backspace) {
255255
active?.classList.add("typed");
@@ -277,7 +277,7 @@ export function updateActiveElement(
277277
void updateWordsInputPosition();
278278
}
279279
if (!initial && Config.tapeMode !== "off") {
280-
scrollTape();
280+
await scrollTape();
281281
}
282282
}
283283

@@ -354,7 +354,7 @@ function getWordHTML(word: string): string {
354354
return retval;
355355
}
356356

357-
function updateWordWrapperClasses(): void {
357+
async function updateWordWrapperClasses(): Promise<void> {
358358
if (Config.tapeMode !== "off") {
359359
$("#words").addClass("tape");
360360
$("#wordsWrapper").addClass("tape");
@@ -400,13 +400,13 @@ function updateWordWrapperClasses(): void {
400400

401401
updateWordsWidth();
402402
updateWordsWrapperHeight(true);
403-
updateWordsMargin();
403+
await updateWordsMargin();
404404
setTimeout(() => {
405405
void updateWordsInputPosition(true);
406406
}, 250);
407407
}
408408

409-
export function showWords(): void {
409+
export async function showWords(): Promise<void> {
410410
$("#words").empty();
411411

412412
let wordsHTML = "";
@@ -420,12 +420,12 @@ export function showWords(): void {
420420

421421
$("#words").html(wordsHTML);
422422

423-
updateActiveElement(undefined, true);
423+
await updateActiveElement(undefined, true);
424424
setTimeout(() => {
425425
void Caret.updatePosition();
426426
}, 125);
427427

428-
updateWordWrapperClasses();
428+
await updateWordWrapperClasses();
429429
}
430430

431431
const posUpdateLangList = ["japanese", "chinese", "korean"];
@@ -573,9 +573,9 @@ function updateWordsWrapperHeight(force = false): void {
573573
}
574574
}
575575

576-
function updateWordsMargin(): void {
576+
async function updateWordsMargin(): Promise<void> {
577577
if (Config.tapeMode !== "off") {
578-
scrollTape();
578+
await scrollTape();
579579
} else {
580580
setTimeout(() => {
581581
$("#words").css("margin-left", "unset");
@@ -929,17 +929,20 @@ export async function updateActiveWordLetters(
929929
"<div class='beforeNewline'></div><div class='newline'></div><div class='afterNewline'></div>"
930930
);
931931
if (Config.tapeMode !== "off") {
932-
scrollTape();
932+
await scrollTape();
933933
}
934934
}
935935

936936
let allowWordRemoval = true;
937-
export function scrollTape(noRemove = false): void {
937+
export async function scrollTape(noRemove = false): Promise<void> {
938938
if (ActivePage.get() !== "test" || resultVisible) return;
939939

940940
const waitForLineJumpAnimation = lineTransition && !allowWordRemoval;
941941
if (waitForLineJumpAnimation) noRemove = true;
942942

943+
const currentLang = await JSONData.getCurrentLanguage(Config.language);
944+
const isLanguageRTL = currentLang.rightToLeft;
945+
943946
const wordsWrapperWidth = (
944947
document.querySelector("#wordsWrapper") as HTMLElement
945948
).offsetWidth;
@@ -1009,7 +1012,11 @@ export function scrollTape(noRemove = false): void {
10091012
const wordOuterWidth = $(child).outerWidth(true) ?? 0;
10101013
const forWordLeft = Math.floor(child.offsetLeft);
10111014
const forWordWidth = Math.floor(child.offsetWidth);
1012-
if (!noRemove && forWordLeft < 0 - forWordWidth) {
1015+
if (
1016+
!noRemove &&
1017+
((!isLanguageRTL && forWordLeft < 0 - forWordWidth) ||
1018+
(isLanguageRTL && forWordLeft > wordsWrapperWidth))
1019+
) {
10131020
toRemove.push(child);
10141021
widthToRemove += wordOuterWidth;
10151022
wordsToRemoveCount++;
@@ -1037,30 +1044,44 @@ export function scrollTape(noRemove = false): void {
10371044
const currentChildMargin = parseInt(element.style.marginLeft) || 0;
10381045
element.style.marginLeft = `${currentChildMargin - widthToRemove}px`;
10391046
}
1047+
if (isLanguageRTL) widthToRemove *= -1;
10401048
const currentWordsMargin = parseInt(wordsEl.style.marginLeft) || 0;
10411049
wordsEl.style.marginLeft = `${currentWordsMargin + widthToRemove}px`;
10421050
}
10431051

1052+
const inputLength = TestInput.input.current.length;
10441053
let currentWordWidth = 0;
1045-
if (Config.tapeMode === "letter") {
1046-
if (TestInput.input.current.length > 0) {
1047-
const letters = activeWordEl.querySelectorAll("letter");
1048-
for (let i = 0; i < TestInput.input.current.length; i++) {
1049-
const letter = letters[i] as HTMLElement;
1050-
if (
1051-
(Config.blindMode || Config.hideExtraLetters) &&
1052-
letter.classList.contains("extra")
1053-
) {
1054-
continue;
1055-
}
1056-
currentWordWidth += $(letter).outerWidth(true) ?? 0;
1054+
if (Config.tapeMode === "letter" && inputLength > 0) {
1055+
const letters = activeWordEl.querySelectorAll("letter");
1056+
let lastPositiveLetterWidth = 0;
1057+
for (let i = 0; i < inputLength; i++) {
1058+
const letter = letters[i] as HTMLElement;
1059+
if (
1060+
(Config.blindMode || Config.hideExtraLetters) &&
1061+
letter.classList.contains("extra")
1062+
) {
1063+
continue;
10571064
}
1065+
const letterOuterWidth = $(letter).outerWidth(true) ?? 0;
1066+
currentWordWidth += letterOuterWidth;
1067+
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
10581068
}
1059-
}
1060-
let newMargin =
1061-
wordsWrapperWidth / 2 - (wordsWidthBeforeActive + currentWordWidth);
1069+
// if current letter has zero width move the tape to previous positive width letter
1070+
if ($(letters[inputLength] as Element).outerWidth(true) === 0)
1071+
currentWordWidth -= lastPositiveLetterWidth;
1072+
}
1073+
1074+
let newMargin = wordsWrapperWidth / 2;
1075+
if (isLanguageRTL)
1076+
newMargin +=
1077+
wordsWidthBeforeActive +
1078+
currentWordWidth -
1079+
wordsEl.offsetWidth +
1080+
wordRightMargin;
1081+
else newMargin -= wordsWidthBeforeActive + currentWordWidth;
10621082
if (waitForLineJumpAnimation)
10631083
newMargin = parseInt(wordsEl.style.marginLeft) || 0;
1084+
10641085
const jqWords = $(wordsEl);
10651086
if (Config.smoothLineScroll) {
10661087
jqWords.stop("leftMargin", true, false).animate(
@@ -1070,10 +1091,10 @@ export function scrollTape(noRemove = false): void {
10701091
{
10711092
duration: SlowTimer.get() ? 0 : 125,
10721093
queue: "leftMargin",
1073-
complete: () => {
1094+
complete: async () => {
10741095
if (noRemove) {
1075-
if (waitForLineJumpAnimation) scrollTape(true);
1076-
else scrollTape();
1096+
if (waitForLineJumpAnimation) await scrollTape(true);
1097+
else await scrollTape();
10771098
}
10781099
},
10791100
}
@@ -1094,7 +1115,7 @@ export function scrollTape(noRemove = false): void {
10941115
linesWidths.forEach((width, index) => {
10951116
(afterNewLineEls[index] as HTMLElement).style.marginLeft = `${width}px`;
10961117
});
1097-
if (noRemove) scrollTape();
1118+
if (noRemove) await scrollTape();
10981119
}
10991120
}
11001121

frontend/src/ts/ui.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,11 @@ window.addEventListener("beforeunload", (event) => {
9090
}
9191
});
9292

93-
const debouncedEvent = debounce(250, () => {
93+
const debouncedEvent = debounce(250, async () => {
9494
void Caret.updatePosition();
9595
if (getActivePage() === "test" && !TestUI.resultVisible) {
9696
if (Config.tapeMode !== "off") {
97-
TestUI.scrollTape();
97+
await TestUI.scrollTape();
9898
} else {
9999
TestUI.updateTestLine();
100100
}
@@ -112,7 +112,7 @@ const throttledEvent = throttle(250, () => {
112112

113113
$(window).on("resize", () => {
114114
throttledEvent();
115-
debouncedEvent();
115+
void debouncedEvent();
116116
});
117117

118118
ConfigEvent.subscribe((eventKey) => {

0 commit comments

Comments
 (0)