Skip to content

Commit 87dab9f

Browse files
committed
impr(tape mode): support RTL languages (@NadAlaba)
1 parent 23cf24f commit 87dab9f

File tree

5 files changed

+79
-57
lines changed

5 files changed

+79
-57
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
@@ -111,11 +111,17 @@ function getTargetPositionLeft(
111111

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

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

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

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

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

Lines changed: 61 additions & 38 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

@@ -353,16 +353,18 @@ function getWordHTML(word: string): string {
353353
return retval;
354354
}
355355

356-
function updateWordsMargin(): void {
356+
async function updateWordsMargin(): Promise<void> {
357357
if (Config.tapeMode !== "off") {
358-
scrollTape(true);
358+
await scrollTape(true);
359359
} else {
360-
$("#words").css("margin-left", "unset");
361-
$(".afterNewline").css("margin-left", "unset");
360+
setTimeout(() => {
361+
$("#words").css("margin-left", "unset");
362+
$(".afterNewline").css("margin-left", "unset");
363+
}, 125);
362364
}
363365
}
364366

365-
function updateWordWrapperClasses(): void {
367+
async function updateWordWrapperClasses(): Promise<void> {
366368
if (Config.tapeMode !== "off") {
367369
$("#words").addClass("tape");
368370
$("#wordsWrapper").addClass("tape");
@@ -408,13 +410,13 @@ function updateWordWrapperClasses(): void {
408410

409411
updateWordsWidth();
410412
updateWordsWrapperHeight(true);
411-
updateWordsMargin();
413+
await updateWordsMargin();
412414
setTimeout(() => {
413415
void updateWordsInputPosition(true);
414416
}, 250);
415417
}
416418

417-
export function showWords(): void {
419+
export async function showWords(): Promise<void> {
418420
$("#words").empty();
419421

420422
let wordsHTML = "";
@@ -428,12 +430,12 @@ export function showWords(): void {
428430

429431
$("#words").html(wordsHTML);
430432

431-
updateActiveElement(undefined, true);
433+
await updateActiveElement(undefined, true);
432434
setTimeout(() => {
433435
void Caret.updatePosition();
434436
}, 125);
435437

436-
updateWordWrapperClasses();
438+
await updateWordWrapperClasses();
437439
}
438440

439441
const posUpdateLangList = ["japanese", "chinese", "korean"];
@@ -917,17 +919,20 @@ export async function updateActiveWordLetters(
917919
"<div class='beforeNewline'></div><div class='newline'></div><div class='afterNewline'></div>"
918920
);
919921
if (Config.tapeMode !== "off") {
920-
scrollTape();
922+
await scrollTape();
921923
}
922924
}
923925

924926
let allowWordRemoval = true;
925-
export function scrollTape(noRemove = false): void {
927+
export async function scrollTape(noRemove = false): Promise<void> {
926928
if (ActivePage.get() !== "test" || resultVisible) return;
927929

928930
const waitForLineJumpAnimation = lineTransition && !allowWordRemoval;
929931
if (waitForLineJumpAnimation) noRemove = true;
930932

933+
const currentLang = await JSONData.getCurrentLanguage(Config.language);
934+
const isLanguageRTL = currentLang.rightToLeft;
935+
931936
const wordsWrapperWidth = (
932937
document.querySelector("#wordsWrapper") as HTMLElement
933938
).offsetWidth;
@@ -997,7 +1002,11 @@ export function scrollTape(noRemove = false): void {
9971002
const wordOuterWidth = $(child).outerWidth(true) ?? 0;
9981003
const forWordLeft = Math.floor(child.offsetLeft);
9991004
const forWordWidth = Math.floor(child.offsetWidth);
1000-
if (!noRemove && forWordLeft < 0 - forWordWidth) {
1005+
if (
1006+
!noRemove &&
1007+
((!isLanguageRTL && forWordLeft < 0 - forWordWidth) ||
1008+
(isLanguageRTL && forWordLeft > wordsWrapperWidth))
1009+
) {
10011010
toRemove.push(child);
10021011
widthToRemove += wordOuterWidth;
10031012
wordsToRemoveCount++;
@@ -1025,30 +1034,44 @@ export function scrollTape(noRemove = false): void {
10251034
const currentChildMargin = parseInt(element.style.marginLeft) || 0;
10261035
element.style.marginLeft = `${currentChildMargin - widthToRemove}px`;
10271036
}
1037+
if (isLanguageRTL) widthToRemove *= -1;
10281038
const currentWordsMargin = parseInt(wordsEl.style.marginLeft) || 0;
10291039
wordsEl.style.marginLeft = `${currentWordsMargin + widthToRemove}px`;
10301040
}
10311041

1042+
const inputLength = TestInput.input.current.length;
10321043
let currentWordWidth = 0;
1033-
if (Config.tapeMode === "letter") {
1034-
if (TestInput.input.current.length > 0) {
1035-
const letters = activeWordEl.querySelectorAll("letter");
1036-
for (let i = 0; i < TestInput.input.current.length; i++) {
1037-
const letter = letters[i] as HTMLElement;
1038-
if (
1039-
(Config.blindMode || Config.hideExtraLetters) &&
1040-
letter.classList.contains("extra")
1041-
) {
1042-
continue;
1043-
}
1044-
currentWordWidth += $(letter).outerWidth(true) ?? 0;
1044+
if (Config.tapeMode === "letter" && inputLength > 0) {
1045+
const letters = activeWordEl.querySelectorAll("letter");
1046+
let lastPositiveLetterWidth = 0;
1047+
for (let i = 0; i < inputLength; i++) {
1048+
const letter = letters[i] as HTMLElement;
1049+
if (
1050+
(Config.blindMode || Config.hideExtraLetters) &&
1051+
letter.classList.contains("extra")
1052+
) {
1053+
continue;
10451054
}
1055+
const letterOuterWidth = $(letter).outerWidth(true) ?? 0;
1056+
currentWordWidth += letterOuterWidth;
1057+
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
10461058
}
1047-
}
1048-
let newMargin =
1049-
wordsWrapperWidth / 2 - (wordsWidthBeforeActive + currentWordWidth);
1059+
// if current letter has zero width move the tape to previous positive width letter
1060+
if ($(letters[inputLength] as Element).outerWidth(true) === 0)
1061+
currentWordWidth -= lastPositiveLetterWidth;
1062+
}
1063+
1064+
let newMargin = wordsWrapperWidth / 2;
1065+
if (isLanguageRTL)
1066+
newMargin +=
1067+
wordsWidthBeforeActive +
1068+
currentWordWidth -
1069+
wordsEl.offsetWidth +
1070+
wordRightMargin;
1071+
else newMargin -= wordsWidthBeforeActive + currentWordWidth;
10501072
if (waitForLineJumpAnimation)
10511073
newMargin = parseInt(wordsEl.style.marginLeft) || 0;
1074+
10521075
const jqWords = $(wordsEl);
10531076
if (Config.smoothLineScroll) {
10541077
jqWords.stop("leftMargin", true, false).animate(
@@ -1058,10 +1081,10 @@ export function scrollTape(noRemove = false): void {
10581081
{
10591082
duration: SlowTimer.get() ? 0 : 125,
10601083
queue: "leftMargin",
1061-
complete: () => {
1084+
complete: async () => {
10621085
if (noRemove) {
1063-
if (waitForLineJumpAnimation) scrollTape(true);
1064-
else scrollTape();
1086+
if (waitForLineJumpAnimation) await scrollTape(true);
1087+
else await scrollTape();
10651088
}
10661089
},
10671090
}
@@ -1082,7 +1105,7 @@ export function scrollTape(noRemove = false): void {
10821105
linesWidths.forEach((width, index) => {
10831106
(afterNewLineEls[index] as HTMLElement).style.marginLeft = `${width}px`;
10841107
});
1085-
if (noRemove) scrollTape();
1108+
if (noRemove) await scrollTape();
10861109
}
10871110
}
10881111

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)