Skip to content

Commit 5bcd1e5

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

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
@@ -292,8 +292,17 @@
292292
}
293293
}
294294
&.withLigatures {
295-
letter {
296-
display: inline;
295+
.word {
296+
overflow-wrap: anywhere;
297+
padding-bottom: 0.05em; // compensate for letter border
298+
299+
.hints {
300+
overflow-wrap: initial;
301+
}
302+
303+
letter {
304+
display: inline;
305+
}
297306
}
298307
}
299308
&.blurred {
@@ -721,8 +730,17 @@
721730
}
722731
}
723732
&.withLigatures {
724-
letter {
725-
display: inline;
733+
.word {
734+
overflow-wrap: anywhere;
735+
padding-bottom: 2px; // compensate for letter border
736+
737+
.hints {
738+
overflow-wrap: initial;
739+
}
740+
741+
letter {
742+
display: inline;
743+
}
726744
}
727745
}
728746
}

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 {
@@ -577,7 +576,7 @@ export function updateWordsWrapperHeight(force = false): void {
577576

578577
function updateWordsMargin(): void {
579578
if (Config.tapeMode !== "off") {
580-
scrollTape(true);
579+
void scrollTape(true);
581580
} else {
582581
setTimeout(() => {
583582
$("#words").css("margin-left", "unset");
@@ -934,7 +933,7 @@ export async function updateActiveWordLetters(
934933
"<div class='beforeNewline'></div><div class='newline'></div><div class='afterNewline'></div>"
935934
);
936935
if (Config.tapeMode !== "off") {
937-
scrollTape();
936+
await scrollTape();
938937
}
939938
}
940939

@@ -965,15 +964,18 @@ function getNlCharWidthFromPreviousWord(
965964
}
966965

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

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

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

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

11191138
const jqWords = $(wordsEl);
11201139
if (Config.smoothLineScroll) {
@@ -1126,7 +1145,7 @@ export function scrollTape(noRemove = false): void {
11261145
duration: SlowTimer.get() ? 0 : 125,
11271146
queue: "leftMargin",
11281147
complete: () => {
1129-
if (noRemove) scrollTape();
1148+
if (noRemove) void scrollTape();
11301149
},
11311150
}
11321151
);
@@ -1146,7 +1165,7 @@ export function scrollTape(noRemove = false): void {
11461165
afterNewlinesNewMargins.forEach((margin, index) => {
11471166
(afterNewLineEls[index] as HTMLElement).style.marginLeft = `${margin}px`;
11481167
});
1149-
if (noRemove) scrollTape();
1168+
if (noRemove) void scrollTape();
11501169
}
11511170
}
11521171

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)