Skip to content

Commit 5a01c97

Browse files
committed
impr(tape mode): support RTL languages (@NadAlaba)
1 parent 1a3acde commit 5a01c97

File tree

5 files changed

+75
-56
lines changed

5 files changed

+75
-56
lines changed

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ function updateUI(): void {
115115
}
116116
}
117117

118-
function backspaceToPrevious(): void {
118+
async function backspaceToPrevious(): Promise<void> {
119119
if (!TestState.isActive) return;
120120

121121
if (
@@ -155,7 +155,7 @@ function backspaceToPrevious(): void {
155155
}
156156
TestState.decreaseActiveWordIndex();
157157
TestUI.setActiveWordElementIndex(TestUI.activeWordElementIndex - 1);
158-
TestUI.updateActiveElement(true);
158+
await TestUI.updateActiveElement(true);
159159
Funbox.toggleScript(TestWords.words.getCurrent());
160160
void TestUI.updateActiveWordLetters();
161161

@@ -335,7 +335,7 @@ async function handleSpace(): Promise<void> {
335335
}
336336
}
337337
TestUI.setActiveWordElementIndex(TestUI.activeWordElementIndex + 1);
338-
TestUI.updateActiveElement();
338+
await TestUI.updateActiveElement();
339339
void Caret.updatePosition();
340340

341341
const shouldLimitToThreeLines =
@@ -1070,7 +1070,7 @@ $(document).on("keydown", async (event) => {
10701070
}
10711071

10721072
if (event.key === "Backspace" && TestInput.input.current.length === 0) {
1073-
backspaceToPrevious();
1073+
await backspaceToPrevious();
10741074
if (TestInput.input.current) {
10751075
setWordsInput(" " + TestInput.input.current + " ");
10761076
}
@@ -1269,7 +1269,7 @@ $("#wordsInput").on("beforeinput", (event) => {
12691269
}
12701270
});
12711271

1272-
$("#wordsInput").on("input", (event) => {
1272+
$("#wordsInput").on("input", async (event) => {
12731273
if (!event.originalEvent?.isTrusted || TestUI.testRestarting) {
12741274
(event.target as HTMLInputElement).value = " ";
12751275
return;
@@ -1338,7 +1338,7 @@ $("#wordsInput").on("input", (event) => {
13381338

13391339
if (realInputValue.length === 0 && currTestInput.length === 0) {
13401340
// fallback for when no Backspace keydown event (mobile)
1341-
backspaceToPrevious();
1341+
await backspaceToPrevious();
13421342
} else if (inputValue.length < currTestInput.length) {
13431343
if (containsChinese) {
13441344
if (

frontend/src/ts/test/caret.ts

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

115115
if (Config.tapeMode === "word" && inputLen > 0) {
116116
let currentWordWidth = 0;
117+
let lastPositiveLetterWidth = 0;
117118
for (let i = 0; i < inputLen; i++) {
118119
if (invisibleExtraLetters && i >= wordLen) break;
119-
currentWordWidth +=
120-
$(currentWordNodeList[i] as HTMLElement).outerWidth(true) ?? 0;
120+
const letterOuterWidth =
121+
$(currentWordNodeList[i] as Element).outerWidth(true) ?? 0;
122+
currentWordWidth += letterOuterWidth;
123+
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
121124
}
125+
// if current letter has zero width move the caret to previous positive width letter
126+
if ($(currentWordNodeList[inputLen] as Element).outerWidth(true) === 0)
127+
currentWordWidth -= lastPositiveLetterWidth;
122128
if (isLanguageRightToLeft) currentWordWidth *= -1;
123129
result += currentWordWidth;
124130
}

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -429,13 +429,6 @@ export async function init(): Promise<void> {
429429
}
430430
}
431431

432-
if (Config.tapeMode !== "off" && language.rightToLeft) {
433-
Notifications.add("This language does not support tape mode.", 0, {
434-
important: true,
435-
});
436-
UpdateConfig.setTapeMode("off");
437-
}
438-
439432
const allowLazyMode = !language.noLazyMode || Config.mode === "custom";
440433
if (Config.lazyMode && !allowLazyMode) {
441434
rememberLazyMode = true;
@@ -518,7 +511,7 @@ export async function init(): Promise<void> {
518511
Funbox.toggleScript(TestWords.words.getCurrent());
519512
TestUI.setRightToLeft(language.rightToLeft);
520513
TestUI.setLigatures(language.ligatures ?? false);
521-
TestUI.showWords();
514+
await TestUI.showWords();
522515
console.debug("Test initialized with words", generatedWords);
523516
console.debug(
524517
"Test initialized with section indexes",

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

Lines changed: 57 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ const debouncedZipfCheck = debounce(250, async () => {
142142
}
143143
});
144144

145-
ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
145+
ConfigEvent.subscribe(async (eventKey, eventValue, nosave) => {
146146
if (
147147
(eventKey === "language" || eventKey === "funbox") &&
148148
Config.funbox.split("#").includes("zipf")
@@ -151,7 +151,7 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
151151
}
152152
if (eventKey === "fontSize" && !nosave) {
153153
OutOfFocus.hide();
154-
updateWordWrapperClasses();
154+
await updateWordWrapperClasses();
155155
}
156156
if (
157157
["fontSize", "fontFamily", "blindMode", "hideExtraLetters"].includes(
@@ -167,7 +167,7 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
167167

168168
if (eventValue === undefined) return;
169169
if (eventKey === "highlightMode") {
170-
if (ActivePage.get() === "test") updateActiveElement();
170+
if (ActivePage.get() === "test") await updateActiveElement();
171171
}
172172

173173
if (
@@ -179,7 +179,7 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
179179
"hideExtraLetters",
180180
].includes(eventKey)
181181
) {
182-
updateWordWrapperClasses();
182+
await updateWordWrapperClasses();
183183
}
184184

185185
if (["tapeMode", "tapeMargin"].includes(eventKey)) {
@@ -251,10 +251,10 @@ export function blurWords(): void {
251251
$("#wordsInput").trigger("blur");
252252
}
253253

254-
export function updateActiveElement(
254+
export async function updateActiveElement(
255255
backspace?: boolean,
256256
initial = false
257-
): void {
257+
): Promise<void> {
258258
const active = document.querySelector("#words .active");
259259
if (!backspace) {
260260
active?.classList.add("typed");
@@ -282,7 +282,7 @@ export function updateActiveElement(
282282
void updateWordsInputPosition();
283283
}
284284
if (!initial && Config.tapeMode !== "off") {
285-
scrollTape();
285+
await scrollTape();
286286
}
287287
}
288288

@@ -358,7 +358,7 @@ function getWordHTML(word: string): string {
358358
return retval;
359359
}
360360

361-
function updateWordWrapperClasses(): void {
361+
async function updateWordWrapperClasses(): Promise<void> {
362362
if (Config.tapeMode !== "off") {
363363
$("#words").addClass("tape");
364364
$("#wordsWrapper").addClass("tape");
@@ -404,13 +404,13 @@ function updateWordWrapperClasses(): void {
404404

405405
updateWordsWidth();
406406
updateWordsWrapperHeight(true);
407-
updateWordsMargin();
407+
await updateWordsMargin();
408408
setTimeout(() => {
409409
void updateWordsInputPosition(true);
410410
}, 250);
411411
}
412412

413-
export function showWords(): void {
413+
export async function showWords(): Promise<void> {
414414
$("#words").empty();
415415

416416
let wordsHTML = "";
@@ -424,12 +424,12 @@ export function showWords(): void {
424424

425425
$("#words").html(wordsHTML);
426426

427-
updateActiveElement(undefined, true);
427+
await updateActiveElement(undefined, true);
428428
setTimeout(() => {
429429
void Caret.updatePosition();
430430
}, 125);
431431

432-
updateWordWrapperClasses();
432+
await updateWordWrapperClasses();
433433
}
434434

435435
const posUpdateLangList = ["japanese", "chinese", "korean"];
@@ -573,9 +573,9 @@ export 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(true);
578+
await scrollTape(true);
579579
} else {
580580
setTimeout(() => $("#words").css("margin-left", "unset"), 0);
581581
}
@@ -926,17 +926,20 @@ export async function updateActiveWordLetters(
926926
"<div class='beforeNewline'></div><div class='newline'></div><div class='afterNewline'></div>"
927927
);
928928
if (Config.tapeMode !== "off") {
929-
scrollTape();
929+
await scrollTape();
930930
}
931931
}
932932

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

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

940+
const currentLang = await JSONData.getCurrentLanguage(Config.language);
941+
const isLanguageRTL = currentLang.rightToLeft;
942+
940943
const wordsWrapperWidth = (
941944
document.querySelector("#wordsWrapper") as HTMLElement
942945
).offsetWidth;
@@ -1006,7 +1009,11 @@ export function scrollTape(noRemove = false): void {
10061009
const wordOuterWidth = $(child).outerWidth(true) ?? 0;
10071010
const forWordLeft = Math.floor(child.offsetLeft);
10081011
const forWordWidth = Math.floor(child.offsetWidth);
1009-
if (!noRemove && forWordLeft < 0 - forWordWidth) {
1012+
if (
1013+
!noRemove &&
1014+
((!isLanguageRTL && forWordLeft < 0 - forWordWidth) ||
1015+
(isLanguageRTL && forWordLeft > wordsWrapperWidth))
1016+
) {
10101017
toRemove.push(child);
10111018
widthToRemove += wordOuterWidth;
10121019
wordsToRemoveCount++;
@@ -1034,31 +1041,44 @@ export function scrollTape(noRemove = false): void {
10341041
const currentChildMargin = parseInt(element.style.marginLeft) || 0;
10351042
element.style.marginLeft = `${currentChildMargin - widthToRemove}px`;
10361043
}
1044+
if (isLanguageRTL) widthToRemove *= -1;
10371045
const currentWordsMargin = parseInt(wordsEl.style.marginLeft) || 0;
10381046
wordsEl.style.marginLeft = `${currentWordsMargin + widthToRemove}px`;
10391047
}
10401048

1049+
const inputLength = TestInput.input.current.length;
10411050
let currentWordWidth = 0;
1042-
if (Config.tapeMode === "letter") {
1043-
if (TestInput.input.current.length > 0) {
1044-
const letters = activeWordEl.querySelectorAll("letter");
1045-
for (let i = 0; i < TestInput.input.current.length; i++) {
1046-
const letter = letters[i] as HTMLElement;
1047-
if (
1048-
(Config.blindMode || Config.hideExtraLetters) &&
1049-
letter.classList.contains("extra")
1050-
) {
1051-
continue;
1052-
}
1053-
currentWordWidth += $(letter).outerWidth(true) ?? 0;
1051+
if (Config.tapeMode === "letter" && inputLength > 0) {
1052+
const letters = activeWordEl.querySelectorAll("letter");
1053+
let lastPositiveLetterWidth = 0;
1054+
for (let i = 0; i < inputLength; i++) {
1055+
const letter = letters[i] as HTMLElement;
1056+
if (
1057+
(Config.blindMode || Config.hideExtraLetters) &&
1058+
letter.classList.contains("extra")
1059+
) {
1060+
continue;
10541061
}
1062+
const letterOuterWidth = $(letter).outerWidth(true) ?? 0;
1063+
currentWordWidth += letterOuterWidth;
1064+
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
10551065
}
1056-
}
1057-
1058-
const tapeMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
1059-
let newMargin = tapeMargin - (wordsWidthBeforeActive + currentWordWidth);
1066+
// if current letter has zero width move the tape to previous positive width letter
1067+
if ($(letters[inputLength] as Element).outerWidth(true) === 0)
1068+
currentWordWidth -= lastPositiveLetterWidth;
1069+
}
1070+
1071+
let newMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
1072+
if (isLanguageRTL)
1073+
newMargin +=
1074+
wordsWidthBeforeActive +
1075+
currentWordWidth -
1076+
wordsEl.offsetWidth +
1077+
wordRightMargin;
1078+
else newMargin -= wordsWidthBeforeActive + currentWordWidth;
10601079
if (waitForLineJumpAnimation)
10611080
newMargin = parseInt(wordsEl.style.marginLeft) || 0;
1081+
10621082
const jqWords = $(wordsEl);
10631083
if (Config.smoothLineScroll) {
10641084
jqWords.stop("leftMargin", true, false).animate(
@@ -1068,10 +1088,10 @@ export function scrollTape(noRemove = false): void {
10681088
{
10691089
duration: SlowTimer.get() ? 0 : 125,
10701090
queue: "leftMargin",
1071-
complete: () => {
1091+
complete: async () => {
10721092
if (noRemove) {
1073-
if (waitForLineJumpAnimation) scrollTape(true);
1074-
else scrollTape();
1093+
if (waitForLineJumpAnimation) await scrollTape(true);
1094+
else await scrollTape();
10751095
}
10761096
},
10771097
}
@@ -1092,7 +1112,7 @@ export function scrollTape(noRemove = false): void {
10921112
linesWidths.forEach((width, index) => {
10931113
(afterNewLineEls[index] as HTMLElement).style.marginLeft = `${width}px`;
10941114
});
1095-
if (noRemove) scrollTape();
1115+
if (noRemove) await scrollTape();
10961116
}
10971117
}
10981118

frontend/src/ts/ui.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,11 @@ window.addEventListener("beforeunload", (event) => {
9898
}
9999
});
100100

101-
const debouncedEvent = debounce(250, () => {
101+
const debouncedEvent = debounce(250, async () => {
102102
void Caret.updatePosition();
103103
if (getActivePage() === "test" && !TestUI.resultVisible) {
104104
if (Config.tapeMode !== "off") {
105-
TestUI.scrollTape();
105+
await TestUI.scrollTape();
106106
} else {
107107
TestUI.updateTestLine();
108108
}
@@ -121,7 +121,7 @@ const throttledEvent = throttle(250, () => {
121121

122122
$(window).on("resize", () => {
123123
throttledEvent();
124-
debouncedEvent();
124+
void debouncedEvent();
125125
});
126126

127127
ConfigEvent.subscribe((eventKey) => {

0 commit comments

Comments
 (0)