Skip to content

Commit e3ccac2

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

File tree

4 files changed

+98
-38
lines changed

4 files changed

+98
-38
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: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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 {
@@ -596,10 +595,34 @@ function updateWordsMargin(): void {
596595
if (Config.tapeMode !== "off") {
597596
void scrollTape();
598597
} else {
599-
setTimeout(() => {
600-
$("#words").css("margin-left", "unset");
601-
$("#words .afterNewline").css("margin-left", "unset");
602-
}, 0);
598+
const wordsEl = document.getElementById("words") as HTMLElement;
599+
const afterNewlineEls =
600+
wordsEl.querySelectorAll<HTMLElement>(".afterNewline");
601+
const jqWords = $(wordsEl);
602+
if (Config.smoothLineScroll) {
603+
jqWords.stop("leftMargin", true, false).animate(
604+
{
605+
marginLeft: 0,
606+
},
607+
{
608+
duration: SlowTimer.get() ? 0 : 125,
609+
queue: "leftMargin",
610+
}
611+
);
612+
jqWords.dequeue("leftMargin");
613+
//for (const afterNewline of wordsEl.querySelectorAll(".afterNewline")) {
614+
$(afterNewlineEls)
615+
.stop(true, false)
616+
.animate({ marginLeft: 0 }, SlowTimer.get() ? 0 : 125);
617+
//}
618+
} else {
619+
setTimeout(() => {
620+
wordsEl.style.marginLeft = `0`;
621+
for (const afterNewline of afterNewlineEls) {
622+
afterNewline.style.marginLeft = `0`;
623+
}
624+
}, 0);
625+
}
603626
}
604627
}
605628

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

9861009
await centeringActiveLine;
9871010

1011+
const currentLang = await JSONData.getCurrentLanguage(Config.language);
1012+
const isLanguageRTL = currentLang.rightToLeft;
1013+
9881014
// index of the active word in the collection of .word elements
9891015
const wordElementIndex = TestState.activeWordIndex - activeWordElementOffset;
9901016
const wordsWrapperWidth = (
@@ -1062,7 +1088,10 @@ export async function scrollTape(): Promise<void> {
10621088
const wordOuterWidth = $(child).outerWidth(true) ?? 0;
10631089
const forWordLeft = Math.floor(child.offsetLeft);
10641090
const forWordWidth = Math.floor(child.offsetWidth);
1065-
if (forWordLeft < 0 - forWordWidth) {
1091+
if (
1092+
(!isLanguageRTL && forWordLeft < 0 - forWordWidth) ||
1093+
(isLanguageRTL && forWordLeft > wordsWrapperWidth)
1094+
) {
10661095
toRemove.push(child);
10671096
widthRemoved += wordOuterWidth;
10681097
wordsToRemoveCount++;
@@ -1076,15 +1105,20 @@ export async function scrollTape(): Promise<void> {
10761105
fullLineWidths -= nlCharWidth + wordRightMargin;
10771106
if (i < activeWordIndex) wordsWidthBeforeActive = fullLineWidths;
10781107

1079-
if (fullLineWidths < wordsEl.offsetWidth) {
1108+
/** words that are wider than limit can cause a barely visible bottom line shifting,
1109+
* increase limit if that ever happens, but keep the limit because browsers hate
1110+
* ridiculously wide margins which may cause the words to not be displayed
1111+
*/
1112+
const limit = 3 * wordsEl.offsetWidth;
1113+
if (fullLineWidths < limit) {
10801114
afterNewlinesNewMargins.push(fullLineWidths);
10811115
widthRemovedFromLine.push(widthRemoved);
10821116
} else {
1083-
afterNewlinesNewMargins.push(wordsEl.offsetWidth);
1117+
afterNewlinesNewMargins.push(limit);
10841118
widthRemovedFromLine.push(widthRemoved);
10851119
if (i < lastElementIndex) {
10861120
// for the second .afterNewline after active word
1087-
afterNewlinesNewMargins.push(wordsEl.offsetWidth);
1121+
afterNewlinesNewMargins.push(limit);
10881122
widthRemovedFromLine.push(widthRemoved);
10891123
}
10901124
break;
@@ -1104,30 +1138,40 @@ export async function scrollTape(): Promise<void> {
11041138
currentLineIndent - (widthRemovedFromLine[i] ?? 0)
11051139
}px`;
11061140
}
1141+
if (isLanguageRTL) widthRemoved *= -1;
11071142
const currentWordsMargin = parseFloat(wordsEl.style.marginLeft) || 0;
11081143
wordsEl.style.marginLeft = `${currentWordsMargin + widthRemoved}px`;
11091144
}
11101145

11111146
/* calculate current word width to add to #words margin */
11121147
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;
1148+
const inputLength = TestInput.input.current.length;
1149+
if (Config.tapeMode === "letter" && inputLength > 0) {
1150+
const letters = activeWordEl.querySelectorAll("letter");
1151+
let lastPositiveLetterWidth = 0;
1152+
for (let i = 0; i < inputLength; i++) {
1153+
const letter = letters[i] as HTMLElement;
1154+
if (
1155+
(Config.blindMode || Config.hideExtraLetters) &&
1156+
letter.classList.contains("extra")
1157+
) {
1158+
continue;
11251159
}
1160+
const letterOuterWidth = $(letter).outerWidth(true) ?? 0;
1161+
currentWordWidth += letterOuterWidth;
1162+
if (letterOuterWidth > 0) lastPositiveLetterWidth = letterOuterWidth;
11261163
}
1164+
// if current letter has zero width move the tape to previous positive width letter
1165+
if ($(letters[inputLength] as Element).outerWidth(true) === 0)
1166+
currentWordWidth -= lastPositiveLetterWidth;
11271167
}
1168+
11281169
/* change to new #words & .afterNewline margins */
1129-
const tapeMargin = wordsWrapperWidth * (Config.tapeMargin / 100);
1130-
const newMargin = tapeMargin - (wordsWidthBeforeActive + currentWordWidth);
1170+
let newMargin =
1171+
wordsWrapperWidth * (Config.tapeMargin / 100) -
1172+
wordsWidthBeforeActive -
1173+
currentWordWidth;
1174+
if (isLanguageRTL) newMargin = wordRightMargin - newMargin;
11311175

11321176
const jqWords = $(wordsEl);
11331177
if (Config.smoothLineScroll) {

0 commit comments

Comments
 (0)