@@ -242,6 +242,7 @@ export function setResultCalculating(val: boolean): void {
242
242
export function reset ( ) : void {
243
243
currentTestLine = 0 ;
244
244
activeWordElementOffset = 0 ;
245
+ updateWordsMargin ( true ) ;
245
246
}
246
247
247
248
export function focusWords ( ) : void {
@@ -425,11 +426,10 @@ export function showWords(): void {
425
426
}
426
427
427
428
updateActiveElement ( undefined , true ) ;
429
+ updateWordWrapperClasses ( ) ;
428
430
setTimeout ( ( ) => {
429
431
void Caret . updatePosition ( ) ;
430
432
} , 125 ) ;
431
-
432
- updateWordWrapperClasses ( ) ;
433
433
}
434
434
435
435
export function appendEmptyWordElement ( ) : void {
@@ -592,14 +592,36 @@ export function updateWordsWrapperHeight(force = false): void {
592
592
outOfFocusEl . style . maxHeight = wordHeight * 3 + "px" ;
593
593
}
594
594
595
- function updateWordsMargin ( ) : void {
596
- if ( Config . tapeMode !== "off" ) {
595
+ function updateWordsMargin ( reset = false ) : void {
596
+ if ( Config . tapeMode !== "off" && ! reset ) {
597
597
void scrollTape ( ) ;
598
598
} 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
+ }
603
625
}
604
626
}
605
627
@@ -985,6 +1007,9 @@ export async function scrollTape(): Promise<void> {
985
1007
986
1008
await centeringActiveLine ;
987
1009
1010
+ const currentLang = await JSONData . getCurrentLanguage ( Config . language ) ;
1011
+ const isLanguageRTL = currentLang . rightToLeft ;
1012
+
988
1013
// index of the active word in the collection of .word elements
989
1014
const wordElementIndex = TestState . activeWordIndex - activeWordElementOffset ;
990
1015
const wordsWrapperWidth = (
@@ -1062,7 +1087,10 @@ export async function scrollTape(): Promise<void> {
1062
1087
const wordOuterWidth = $ ( child ) . outerWidth ( true ) ?? 0 ;
1063
1088
const forWordLeft = Math . floor ( child . offsetLeft ) ;
1064
1089
const forWordWidth = Math . floor ( child . offsetWidth ) ;
1065
- if ( forWordLeft < 0 - forWordWidth ) {
1090
+ if (
1091
+ ( ! isLanguageRTL && forWordLeft < 0 - forWordWidth ) ||
1092
+ ( isLanguageRTL && forWordLeft > wordsWrapperWidth )
1093
+ ) {
1066
1094
toRemove . push ( child ) ;
1067
1095
widthRemoved += wordOuterWidth ;
1068
1096
wordsToRemoveCount ++ ;
@@ -1076,15 +1104,20 @@ export async function scrollTape(): Promise<void> {
1076
1104
fullLineWidths -= nlCharWidth + wordRightMargin ;
1077
1105
if ( i < activeWordIndex ) wordsWidthBeforeActive = fullLineWidths ;
1078
1106
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 ) {
1080
1113
afterNewlinesNewMargins . push ( fullLineWidths ) ;
1081
1114
widthRemovedFromLine . push ( widthRemoved ) ;
1082
1115
} else {
1083
- afterNewlinesNewMargins . push ( wordsEl . offsetWidth ) ;
1116
+ afterNewlinesNewMargins . push ( limit ) ;
1084
1117
widthRemovedFromLine . push ( widthRemoved ) ;
1085
1118
if ( i < lastElementIndex ) {
1086
1119
// for the second .afterNewline after active word
1087
- afterNewlinesNewMargins . push ( wordsEl . offsetWidth ) ;
1120
+ afterNewlinesNewMargins . push ( limit ) ;
1088
1121
widthRemovedFromLine . push ( widthRemoved ) ;
1089
1122
}
1090
1123
break ;
@@ -1104,30 +1137,40 @@ export async function scrollTape(): Promise<void> {
1104
1137
currentLineIndent - ( widthRemovedFromLine [ i ] ?? 0 )
1105
1138
} px`;
1106
1139
}
1140
+ if ( isLanguageRTL ) widthRemoved *= - 1 ;
1107
1141
const currentWordsMargin = parseFloat ( wordsEl . style . marginLeft ) || 0 ;
1108
1142
wordsEl . style . marginLeft = `${ currentWordsMargin + widthRemoved } px` ;
1109
1143
}
1110
1144
1111
1145
/* calculate current word width to add to #words margin */
1112
1146
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 ;
1125
1158
}
1159
+ const letterOuterWidth = $ ( letter ) . outerWidth ( true ) ?? 0 ;
1160
+ currentWordWidth += letterOuterWidth ;
1161
+ if ( letterOuterWidth > 0 ) lastPositiveLetterWidth = letterOuterWidth ;
1126
1162
}
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 ;
1127
1166
}
1167
+
1128
1168
/* 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 ;
1131
1174
1132
1175
const jqWords = $ ( wordsEl ) ;
1133
1176
if ( Config . smoothLineScroll ) {
0 commit comments