Skip to content

Commit 40fda7e

Browse files
committed
impr(tape mode): support RTL languages (@NadAlaba)
1 parent a4babff commit 40fda7e

File tree

5 files changed

+74
-38
lines changed

5 files changed

+74
-38
lines changed

frontend/src/styles/test.scss

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,17 @@
293293
}
294294
}
295295
&.withLigatures {
296-
letter {
297-
display: inline;
296+
.word {
297+
overflow-wrap: anywhere;
298+
padding-bottom: 0.05em; // compensate for letter border
299+
300+
.hints {
301+
overflow-wrap: initial;
302+
}
303+
304+
letter {
305+
display: inline;
306+
}
298307
}
299308
}
300309
&.blurred {
@@ -722,8 +731,17 @@
722731
}
723732
}
724733
&.withLigatures {
725-
letter {
726-
display: inline;
734+
.word {
735+
overflow-wrap: anywhere;
736+
padding-bottom: 2px; // compensate for letter border
737+
738+
.hints {
739+
overflow-wrap: initial;
740+
}
741+
742+
letter {
743+
display: inline;
744+
}
727745
}
728746
}
729747
}

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: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -431,13 +431,6 @@ export async function init(): Promise<void> {
431431
}
432432
}
433433

434-
if (Config.tapeMode !== "off" && language.rightToLeft) {
435-
Notifications.add("This language does not support tape mode.", 0, {
436-
important: true,
437-
});
438-
UpdateConfig.setTapeMode("off");
439-
}
440-
441434
const allowLazyMode = !language.noLazyMode || Config.mode === "custom";
442435
if (Config.lazyMode && !allowLazyMode) {
443436
rememberLazyMode = true;

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

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ export function updateActiveElement(
283283
void updateWordsInputPosition();
284284
}
285285
if (!initial && Config.tapeMode !== "off") {
286-
scrollTape();
286+
void scrollTape();
287287
}
288288
}
289289

@@ -425,11 +425,10 @@ export function showWords(): void {
425425
}
426426

427427
updateActiveElement(undefined, true);
428+
updateWordWrapperClasses();
428429
setTimeout(() => {
429430
void Caret.updatePosition();
430431
}, 125);
431-
432-
updateWordWrapperClasses();
433432
}
434433

435434
export function appendEmptyWordElement(): void {
@@ -575,7 +574,7 @@ export function updateWordsWrapperHeight(force = false): void {
575574

576575
function updateWordsMargin(): void {
577576
if (Config.tapeMode !== "off") {
578-
scrollTape(true);
577+
void scrollTape(true);
579578
} else {
580579
setTimeout(() => {
581580
$("#words").css("margin-left", "unset");
@@ -933,7 +932,7 @@ export async function updateActiveWordLetters(
933932
"<div class='beforeNewline'></div><div class='newline'></div><div class='afterNewline'></div>"
934933
);
935934
if (Config.tapeMode !== "off") {
936-
scrollTape();
935+
await scrollTape();
937936
}
938937
}
939938

@@ -964,15 +963,18 @@ function getNlCharWidthFromPreviousWord(
964963
}
965964

966965
let allowWordRemoval = true;
967-
export function scrollTape(noRemove = false): void {
966+
export async function scrollTape(noRemove = false): Promise<void> {
968967
if (ActivePage.get() !== "test" || resultVisible) return;
969968

970969
const waitForLineJumpAnimation = lineTransition && !allowWordRemoval;
971970
if (waitForLineJumpAnimation) {
972-
setTimeout(() => scrollTape(true), 50);
971+
setTimeout(() => void scrollTape(true), 50);
973972
return;
974973
}
975974

975+
const currentLang = await JSONData.getCurrentLanguage(Config.language);
976+
const isLanguageRTL = currentLang.rightToLeft;
977+
976978
// index of the active word in the collection of .word elements
977979
const wordElementIndex = TestState.activeWordIndex - activeWordElementOffset;
978980
const wordsWrapperWidth = (
@@ -1050,7 +1052,11 @@ export function scrollTape(noRemove = false): void {
10501052
const wordOuterWidth = $(child).outerWidth(true) ?? 0;
10511053
const forWordLeft = Math.floor(child.offsetLeft);
10521054
const forWordWidth = Math.floor(child.offsetWidth);
1053-
if (!noRemove && forWordLeft < 0 - forWordWidth) {
1055+
if (
1056+
!noRemove &&
1057+
((!isLanguageRTL && forWordLeft < 0 - forWordWidth) ||
1058+
(isLanguageRTL && forWordLeft > wordsWrapperWidth))
1059+
) {
10541060
toRemove.push(child);
10551061
widthRemoved += wordOuterWidth;
10561062
wordsToRemoveCount++;
@@ -1090,30 +1096,43 @@ export function scrollTape(noRemove = false): void {
10901096
parseFloat(afterNewlineEl.style.marginLeft) || 0;
10911097
afterNewlineEl.style.marginLeft = `${currentLineIndent - width}px`;
10921098
});
1099+
if (isLanguageRTL) widthRemoved *= -1;
10931100
const currentWordsMargin = parseFloat(wordsEl.style.marginLeft) || 0;
10941101
wordsEl.style.marginLeft = `${currentWordsMargin + widthRemoved}px`;
10951102
}
10961103

10971104
/* calculate current word width to add to #words margin */
10981105
let currentWordWidth = 0;
1099-
if (Config.tapeMode === "letter") {
1100-
if (TestInput.input.current.length > 0) {
1101-
const letters = activeWordEl.querySelectorAll("letter");
1102-
for (let i = 0; i < TestInput.input.current.length; i++) {
1103-
const letter = letters[i] as HTMLElement;
1104-
if (
1105-
(Config.blindMode || Config.hideExtraLetters) &&
1106-
letter.classList.contains("extra")
1107-
) {
1108-
continue;
1109-
}
1110-
currentWordWidth += $(letter).outerWidth(true) ?? 0;
1106+
const inputLength = TestInput.input.current.length;
1107+
if (Config.tapeMode === "letter" && inputLength > 0) {
1108+
const letters = activeWordEl.querySelectorAll("letter");
1109+
let lastPositiveLetterWidth = 0;
1110+
for (let i = 0; i < inputLength; i++) {
1111+
const letter = letters[i] as HTMLElement;
1112+
if (
1113+
(Config.blindMode || Config.hideExtraLetters) &&
1114+
letter.classList.contains("extra")
1115+
) {
1116+
continue;
11111117
}
1118+
const letterOuterWidth = $(letter).outerWidth(true) ?? 0;
1119+
currentWordWidth += letterOuterWidth;
1120+
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
11121121
}
1122+
// if current letter has zero width move the tape to previous positive width letter
1123+
if ($(letters[inputLength] as Element).outerWidth(true) === 0)
1124+
currentWordWidth -= lastPositiveLetterWidth;
11131125
}
1126+
11141127
/* change to new #words & .afterNewline margins */
1115-
const tapeMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
1116-
const newMargin = tapeMargin - (wordsWidthBeforeActive + currentWordWidth);
1128+
let newMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
1129+
if (isLanguageRTL)
1130+
newMargin +=
1131+
wordsWidthBeforeActive +
1132+
currentWordWidth -
1133+
wordsEl.offsetWidth +
1134+
wordRightMargin;
1135+
else newMargin -= wordsWidthBeforeActive + currentWordWidth;
11171136

11181137
const jqWords = $(wordsEl);
11191138
if (Config.smoothLineScroll) {
@@ -1125,7 +1144,7 @@ export function scrollTape(noRemove = false): void {
11251144
duration: SlowTimer.get() ? 0 : 125,
11261145
queue: "leftMargin",
11271146
complete: () => {
1128-
if (noRemove) scrollTape();
1147+
if (noRemove) void scrollTape();
11291148
},
11301149
}
11311150
);
@@ -1145,7 +1164,7 @@ export function scrollTape(noRemove = false): void {
11451164
afterNewlinesNewMargins.forEach((margin, index) => {
11461165
(afterNewLineEls[index] as HTMLElement).style.marginLeft = `${margin}px`;
11471166
});
1148-
if (noRemove) scrollTape();
1167+
if (noRemove) void scrollTape();
11491168
}
11501169
}
11511170

frontend/src/ts/ui.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const debouncedEvent = debounce(250, () => {
102102
void Caret.updatePosition();
103103
if (getActivePage() === "test" && !TestUI.resultVisible) {
104104
if (Config.tapeMode !== "off") {
105-
TestUI.scrollTape();
105+
void TestUI.scrollTape();
106106
} else {
107107
TestUI.updateTestLine();
108108
}

0 commit comments

Comments
 (0)