Skip to content

Commit 32558b3

Browse files
committed
impr(tape mode): support RTL languages (@NadAlaba)
1 parent 1e9487b commit 32558b3

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"];
@@ -569,9 +569,9 @@ function updateWordsWrapperHeight(force = false): void {
569569
}
570570
}
571571

572-
function updateWordsMargin(): void {
572+
async function updateWordsMargin(): Promise<void> {
573573
if (Config.tapeMode !== "off") {
574-
scrollTape(true);
574+
await scrollTape(true);
575575
} else {
576576
setTimeout(() => {
577577
$("#words").css("margin-left", "unset");
@@ -925,17 +925,20 @@ export async function updateActiveWordLetters(
925925
"<div class='beforeNewline'></div><div class='newline'></div><div class='afterNewline'></div>"
926926
);
927927
if (Config.tapeMode !== "off") {
928-
scrollTape();
928+
await scrollTape();
929929
}
930930
}
931931

932932
let allowWordRemoval = true;
933-
export function scrollTape(noRemove = false): void {
933+
export async function scrollTape(noRemove = false): Promise<void> {
934934
if (ActivePage.get() !== "test" || resultVisible) return;
935935

936936
const waitForLineJumpAnimation = lineTransition && !allowWordRemoval;
937937
if (waitForLineJumpAnimation) noRemove = true;
938938

939+
const currentLang = await JSONData.getCurrentLanguage(Config.language);
940+
const isLanguageRTL = currentLang.rightToLeft;
941+
939942
const wordsWrapperWidth = (
940943
document.querySelector("#wordsWrapper") as HTMLElement
941944
).offsetWidth;
@@ -1005,7 +1008,11 @@ export function scrollTape(noRemove = false): void {
10051008
const wordOuterWidth = $(child).outerWidth(true) ?? 0;
10061009
const forWordLeft = Math.floor(child.offsetLeft);
10071010
const forWordWidth = Math.floor(child.offsetWidth);
1008-
if (!noRemove && forWordLeft < 0 - forWordWidth) {
1011+
if (
1012+
!noRemove &&
1013+
((!isLanguageRTL && forWordLeft < 0 - forWordWidth) ||
1014+
(isLanguageRTL && forWordLeft > wordsWrapperWidth))
1015+
) {
10091016
toRemove.push(child);
10101017
widthToRemove += wordOuterWidth;
10111018
wordsToRemoveCount++;
@@ -1033,30 +1040,44 @@ export function scrollTape(noRemove = false): void {
10331040
const currentChildMargin = parseInt(element.style.marginLeft) || 0;
10341041
element.style.marginLeft = `${currentChildMargin - widthToRemove}px`;
10351042
}
1043+
if (isLanguageRTL) widthToRemove *= -1;
10361044
const currentWordsMargin = parseInt(wordsEl.style.marginLeft) || 0;
10371045
wordsEl.style.marginLeft = `${currentWordsMargin + widthToRemove}px`;
10381046
}
10391047

1048+
const inputLength = TestInput.input.current.length;
10401049
let currentWordWidth = 0;
1041-
if (Config.tapeMode === "letter") {
1042-
if (TestInput.input.current.length > 0) {
1043-
const letters = activeWordEl.querySelectorAll("letter");
1044-
for (let i = 0; i < TestInput.input.current.length; i++) {
1045-
const letter = letters[i] as HTMLElement;
1046-
if (
1047-
(Config.blindMode || Config.hideExtraLetters) &&
1048-
letter.classList.contains("extra")
1049-
) {
1050-
continue;
1051-
}
1052-
currentWordWidth += $(letter).outerWidth(true) ?? 0;
1050+
if (Config.tapeMode === "letter" && inputLength > 0) {
1051+
const letters = activeWordEl.querySelectorAll("letter");
1052+
let lastPositiveLetterWidth = 0;
1053+
for (let i = 0; i < inputLength; i++) {
1054+
const letter = letters[i] as HTMLElement;
1055+
if (
1056+
(Config.blindMode || Config.hideExtraLetters) &&
1057+
letter.classList.contains("extra")
1058+
) {
1059+
continue;
10531060
}
1061+
const letterOuterWidth = $(letter).outerWidth(true) ?? 0;
1062+
currentWordWidth += letterOuterWidth;
1063+
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
10541064
}
1055-
}
1056-
let newMargin =
1057-
wordsWrapperWidth / 2 - (wordsWidthBeforeActive + currentWordWidth);
1065+
// if current letter has zero width move the tape to previous positive width letter
1066+
if ($(letters[inputLength] as Element).outerWidth(true) === 0)
1067+
currentWordWidth -= lastPositiveLetterWidth;
1068+
}
1069+
1070+
let newMargin = wordsWrapperWidth / 2;
1071+
if (isLanguageRTL)
1072+
newMargin +=
1073+
wordsWidthBeforeActive +
1074+
currentWordWidth -
1075+
wordsEl.offsetWidth +
1076+
wordRightMargin;
1077+
else newMargin -= wordsWidthBeforeActive + currentWordWidth;
10581078
if (waitForLineJumpAnimation)
10591079
newMargin = parseInt(wordsEl.style.marginLeft) || 0;
1080+
10601081
const jqWords = $(wordsEl);
10611082
if (Config.smoothLineScroll) {
10621083
jqWords.stop("leftMargin", true, false).animate(
@@ -1066,10 +1087,10 @@ export function scrollTape(noRemove = false): void {
10661087
{
10671088
duration: SlowTimer.get() ? 0 : 125,
10681089
queue: "leftMargin",
1069-
complete: () => {
1090+
complete: async () => {
10701091
if (noRemove) {
1071-
if (waitForLineJumpAnimation) scrollTape(true);
1072-
else scrollTape();
1092+
if (waitForLineJumpAnimation) await scrollTape(true);
1093+
else await scrollTape();
10731094
}
10741095
},
10751096
}
@@ -1090,7 +1111,7 @@ export function scrollTape(noRemove = false): void {
10901111
linesWidths.forEach((width, index) => {
10911112
(afterNewLineEls[index] as HTMLElement).style.marginLeft = `${width}px`;
10921113
});
1093-
if (noRemove) scrollTape();
1114+
if (noRemove) await scrollTape();
10941115
}
10951116
}
10961117

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)