Skip to content

Commit 127c3c9

Browse files
committed
impr(tape mode): support RTL languages (@nadalaba)
1 parent 7be7304 commit 127c3c9

5 files changed

Lines changed: 74 additions & 38 deletions

File tree

frontend/src/styles/test.scss

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -294,8 +294,17 @@
294294
}
295295
}
296296
&.withLigatures {
297-
letter {
298-
display: inline;
297+
.word {
298+
overflow-wrap: anywhere;
299+
padding-bottom: 0.05em; // compensate for letter border
300+
301+
.hints {
302+
overflow-wrap: initial;
303+
}
304+
305+
letter {
306+
display: inline;
307+
}
299308
}
300309
}
301310
&.blurred {
@@ -723,8 +732,17 @@
723732
}
724733
}
725734
&.withLigatures {
726-
letter {
727-
display: inline;
735+
.word {
736+
overflow-wrap: anywhere;
737+
padding-bottom: 2px; // compensate for letter border
738+
739+
.hints {
740+
overflow-wrap: initial;
741+
}
742+
743+
letter {
744+
display: inline;
745+
}
728746
}
729747
}
730748
}

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

@@ -959,15 +958,18 @@ function getNlCharWidth(
959958
}
960959

961960
let allowWordRemoval = true;
962-
export function scrollTape(noRemove = false): void {
961+
export async function scrollTape(noRemove = false): Promise<void> {
963962
if (ActivePage.get() !== "test" || resultVisible) return;
964963

965964
const waitForLineJumpAnimation = lineTransition && !allowWordRemoval;
966965
if (waitForLineJumpAnimation) {
967-
setTimeout(() => scrollTape(true), 50);
966+
setTimeout(() => void scrollTape(true), 50);
968967
return;
969968
}
970969

970+
const currentLang = await JSONData.getCurrentLanguage(Config.language);
971+
const isLanguageRTL = currentLang.rightToLeft;
972+
971973
// index of the active word in the collection of .word elements
972974
const wordElementIndex = TestState.activeWordIndex - activeWordElementOffset;
973975
const wordsWrapperWidth = (
@@ -1045,7 +1047,11 @@ export function scrollTape(noRemove = false): void {
10451047
const wordOuterWidth = $(child).outerWidth(true) ?? 0;
10461048
const forWordLeft = Math.floor(child.offsetLeft);
10471049
const forWordWidth = Math.floor(child.offsetWidth);
1048-
if (!noRemove && forWordLeft < 0 - forWordWidth) {
1050+
if (
1051+
!noRemove &&
1052+
((!isLanguageRTL && forWordLeft < 0 - forWordWidth) ||
1053+
(isLanguageRTL && forWordLeft > wordsWrapperWidth))
1054+
) {
10491055
toRemove.push(child);
10501056
widthRemoved += wordOuterWidth;
10511057
wordsToRemoveCount++;
@@ -1085,30 +1091,43 @@ export function scrollTape(noRemove = false): void {
10851091
parseFloat(afterNewlineEl.style.marginLeft) || 0;
10861092
afterNewlineEl.style.marginLeft = `${currentLineIndent - width}px`;
10871093
});
1094+
if (isLanguageRTL) widthRemoved *= -1;
10881095
const currentWordsMargin = parseFloat(wordsEl.style.marginLeft) || 0;
10891096
wordsEl.style.marginLeft = `${currentWordsMargin + widthRemoved}px`;
10901097
}
10911098

10921099
/* calculate current word width to add to #words margin */
10931100
let currentWordWidth = 0;
1094-
if (Config.tapeMode === "letter") {
1095-
if (TestInput.input.current.length > 0) {
1096-
const letters = activeWordEl.querySelectorAll("letter");
1097-
for (let i = 0; i < TestInput.input.current.length; i++) {
1098-
const letter = letters[i] as HTMLElement;
1099-
if (
1100-
(Config.blindMode || Config.hideExtraLetters) &&
1101-
letter.classList.contains("extra")
1102-
) {
1103-
continue;
1104-
}
1105-
currentWordWidth += $(letter).outerWidth(true) ?? 0;
1101+
const inputLength = TestInput.input.current.length;
1102+
if (Config.tapeMode === "letter" && inputLength > 0) {
1103+
const letters = activeWordEl.querySelectorAll("letter");
1104+
let lastPositiveLetterWidth = 0;
1105+
for (let i = 0; i < inputLength; i++) {
1106+
const letter = letters[i] as HTMLElement;
1107+
if (
1108+
(Config.blindMode || Config.hideExtraLetters) &&
1109+
letter.classList.contains("extra")
1110+
) {
1111+
continue;
11061112
}
1113+
const letterOuterWidth = $(letter).outerWidth(true) ?? 0;
1114+
currentWordWidth += letterOuterWidth;
1115+
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
11071116
}
1117+
// if current letter has zero width move the tape to previous positive width letter
1118+
if ($(letters[inputLength] as Element).outerWidth(true) === 0)
1119+
currentWordWidth -= lastPositiveLetterWidth;
11081120
}
1121+
11091122
/* change to new #words & .afterNewline margins */
1110-
const tapeMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
1111-
const newMargin = tapeMargin - (wordsWidthBeforeActive + currentWordWidth);
1123+
let newMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
1124+
if (isLanguageRTL)
1125+
newMargin +=
1126+
wordsWidthBeforeActive +
1127+
currentWordWidth -
1128+
wordsEl.offsetWidth +
1129+
wordRightMargin;
1130+
else newMargin -= wordsWidthBeforeActive + currentWordWidth;
11121131

11131132
const jqWords = $(wordsEl);
11141133
if (Config.smoothLineScroll) {
@@ -1120,7 +1139,7 @@ export function scrollTape(noRemove = false): void {
11201139
duration: SlowTimer.get() ? 0 : 125,
11211140
queue: "leftMargin",
11221141
complete: () => {
1123-
if (noRemove) scrollTape();
1142+
if (noRemove) void scrollTape();
11241143
},
11251144
}
11261145
);
@@ -1140,7 +1159,7 @@ export function scrollTape(noRemove = false): void {
11401159
afterNewlinesNewMargins.forEach((margin, index) => {
11411160
(afterNewLineEls[index] as HTMLElement).style.marginLeft = `${margin}px`;
11421161
});
1143-
if (noRemove) scrollTape();
1162+
if (noRemove) void scrollTape();
11441163
}
11451164
}
11461165

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)