Skip to content

Commit c65877f

Browse files
committed
impr(tape mode): support RTL languages
1 parent 9a4ac30 commit c65877f

File tree

4 files changed

+99
-40
lines changed

4 files changed

+99
-40
lines changed

frontend/src/styles/test.scss

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,6 @@
278278
&.tape {
279279
display: block;
280280
white-space: nowrap;
281-
width: 200vw;
282281
.word {
283282
margin: 0.25em 0.6em 0.25em 0;
284283
display: inline-block;
@@ -296,8 +295,17 @@
296295
}
297296
}
298297
&.withLigatures {
299-
letter {
300-
display: inline;
298+
.word {
299+
overflow-wrap: anywhere;
300+
padding-bottom: 0.05em; // compensate for letter border
301+
302+
.hints {
303+
overflow-wrap: initial;
304+
}
305+
306+
letter {
307+
display: inline;
308+
}
301309
}
302310
}
303311
&.blurred {
@@ -725,8 +733,17 @@
725733
}
726734
}
727735
&.withLigatures {
728-
letter {
729-
display: inline;
736+
.word {
737+
overflow-wrap: anywhere;
738+
padding-bottom: 2px; // compensate for letter border
739+
740+
.hints {
741+
overflow-wrap: initial;
742+
}
743+
744+
letter {
745+
display: inline;
746+
}
730747
}
731748
}
732749
}

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: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ export function setResultCalculating(val: boolean): void {
242242
export function reset(): void {
243243
currentTestLine = 0;
244244
activeWordElementOffset = 0;
245+
updateWordsMargin(true);
245246
}
246247

247248
export function focusWords(): void {
@@ -425,11 +426,10 @@ export function showWords(): void {
425426
}
426427

427428
updateActiveElement(undefined, true);
429+
updateWordWrapperClasses();
428430
setTimeout(() => {
429431
void Caret.updatePosition();
430432
}, 125);
431-
432-
updateWordWrapperClasses();
433433
}
434434

435435
export function appendEmptyWordElement(): void {
@@ -592,14 +592,36 @@ export function updateWordsWrapperHeight(force = false): void {
592592
outOfFocusEl.style.maxHeight = wordHeight * 3 + "px";
593593
}
594594

595-
function updateWordsMargin(): void {
596-
if (Config.tapeMode !== "off") {
595+
function updateWordsMargin(reset = false): void {
596+
if (Config.tapeMode !== "off" && !reset) {
597597
void scrollTape();
598598
} else {
599-
setTimeout(() => {
600-
$("#words").css("margin-left", "unset");
601-
$("#words .afterNewline").css("margin-left", "unset");
602-
}, 0);
599+
const wordsEl = document.getElementById("words") as HTMLElement;
600+
const afterNewlineEls =
601+
wordsEl.querySelectorAll<HTMLElement>(".afterNewline");
602+
if (Config.smoothLineScroll && !reset) {
603+
const jqWords = $(wordsEl);
604+
jqWords.stop("leftMargin", true, false).animate(
605+
{
606+
marginLeft: 0,
607+
},
608+
{
609+
duration: SlowTimer.get() ? 0 : 125,
610+
queue: "leftMargin",
611+
}
612+
);
613+
jqWords.dequeue("leftMargin");
614+
$(afterNewlineEls)
615+
.stop(true, false)
616+
.animate({ marginLeft: 0 }, SlowTimer.get() ? 0 : 125);
617+
} else {
618+
setTimeout(() => {
619+
wordsEl.style.marginLeft = `0`;
620+
for (const afterNewline of afterNewlineEls) {
621+
afterNewline.style.marginLeft = `0`;
622+
}
623+
}, 0);
624+
}
603625
}
604626
}
605627

@@ -985,6 +1007,9 @@ export async function scrollTape(): Promise<void> {
9851007

9861008
await centeringActiveLine;
9871009

1010+
const currentLang = await JSONData.getCurrentLanguage(Config.language);
1011+
const isLanguageRTL = currentLang.rightToLeft;
1012+
9881013
// index of the active word in the collection of .word elements
9891014
const wordElementIndex = TestState.activeWordIndex - activeWordElementOffset;
9901015
const wordsWrapperWidth = (
@@ -1062,7 +1087,10 @@ export async function scrollTape(): Promise<void> {
10621087
const wordOuterWidth = $(child).outerWidth(true) ?? 0;
10631088
const forWordLeft = Math.floor(child.offsetLeft);
10641089
const forWordWidth = Math.floor(child.offsetWidth);
1065-
if (forWordLeft < 0 - forWordWidth) {
1090+
if (
1091+
(!isLanguageRTL && forWordLeft < 0 - forWordWidth) ||
1092+
(isLanguageRTL && forWordLeft > wordsWrapperWidth)
1093+
) {
10661094
toRemove.push(child);
10671095
widthRemoved += wordOuterWidth;
10681096
wordsToRemoveCount++;
@@ -1076,15 +1104,20 @@ export async function scrollTape(): Promise<void> {
10761104
fullLineWidths -= nlCharWidth + wordRightMargin;
10771105
if (i < activeWordIndex) wordsWidthBeforeActive = fullLineWidths;
10781106

1079-
if (fullLineWidths < wordsEl.offsetWidth) {
1107+
/** words that are wider than limit can cause a barely visible bottom line shifting,
1108+
* increase limit if that ever happens, but keep the limit because browsers hate
1109+
* ridiculously wide margins which may cause the words to not be displayed
1110+
*/
1111+
const limit = 3 * wordsEl.offsetWidth;
1112+
if (fullLineWidths < limit) {
10801113
afterNewlinesNewMargins.push(fullLineWidths);
10811114
widthRemovedFromLine.push(widthRemoved);
10821115
} else {
1083-
afterNewlinesNewMargins.push(wordsEl.offsetWidth);
1116+
afterNewlinesNewMargins.push(limit);
10841117
widthRemovedFromLine.push(widthRemoved);
10851118
if (i < lastElementIndex) {
10861119
// for the second .afterNewline after active word
1087-
afterNewlinesNewMargins.push(wordsEl.offsetWidth);
1120+
afterNewlinesNewMargins.push(limit);
10881121
widthRemovedFromLine.push(widthRemoved);
10891122
}
10901123
break;
@@ -1104,30 +1137,40 @@ export async function scrollTape(): Promise<void> {
11041137
currentLineIndent - (widthRemovedFromLine[i] ?? 0)
11051138
}px`;
11061139
}
1140+
if (isLanguageRTL) widthRemoved *= -1;
11071141
const currentWordsMargin = parseFloat(wordsEl.style.marginLeft) || 0;
11081142
wordsEl.style.marginLeft = `${currentWordsMargin + widthRemoved}px`;
11091143
}
11101144

11111145
/* calculate current word width to add to #words margin */
11121146
let currentWordWidth = 0;
1113-
if (Config.tapeMode === "letter") {
1114-
if (TestInput.input.current.length > 0) {
1115-
const letters = activeWordEl.querySelectorAll("letter");
1116-
for (let i = 0; i < TestInput.input.current.length; i++) {
1117-
const letter = letters[i] as HTMLElement;
1118-
if (
1119-
(Config.blindMode || Config.hideExtraLetters) &&
1120-
letter.classList.contains("extra")
1121-
) {
1122-
continue;
1123-
}
1124-
currentWordWidth += $(letter).outerWidth(true) ?? 0;
1147+
const inputLength = TestInput.input.current.length;
1148+
if (Config.tapeMode === "letter" && inputLength > 0) {
1149+
const letters = activeWordEl.querySelectorAll("letter");
1150+
let lastPositiveLetterWidth = 0;
1151+
for (let i = 0; i < inputLength; i++) {
1152+
const letter = letters[i] as HTMLElement;
1153+
if (
1154+
(Config.blindMode || Config.hideExtraLetters) &&
1155+
letter.classList.contains("extra")
1156+
) {
1157+
continue;
11251158
}
1159+
const letterOuterWidth = $(letter).outerWidth(true) ?? 0;
1160+
currentWordWidth += letterOuterWidth;
1161+
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
11261162
}
1163+
// if current letter has zero width move the tape to previous positive width letter
1164+
if ($(letters[inputLength] as Element).outerWidth(true) === 0)
1165+
currentWordWidth -= lastPositiveLetterWidth;
11271166
}
1167+
11281168
/* change to new #words & .afterNewline margins */
1129-
const tapeMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
1130-
const newMargin = tapeMargin - (wordsWidthBeforeActive + currentWordWidth);
1169+
let newMargin =
1170+
wordsWrapperWidth * (Config.tapeMargin / 100) -
1171+
wordsWidthBeforeActive -
1172+
currentWordWidth;
1173+
if (isLanguageRTL) newMargin = wordRightMargin - newMargin;
11311174

11321175
const jqWords = $(wordsEl);
11331176
if (Config.smoothLineScroll) {

0 commit comments

Comments
 (0)