@@ -27,83 +27,6 @@ import { convertRemToPixels } from "../utils/numbers";
2727import { findSingleActiveFunboxWithFunction } from "./funbox/list" ;
2828import * as TestState from "./test-state" ;
2929
30- function createHintsHtml (
31- incorrectLtrIndices : number [ ] [ ] ,
32- activeWordLetters : NodeListOf < Element > ,
33- inputWord : string
34- ) : string {
35- const inputChars = Strings . splitIntoCharacters ( inputWord ) ;
36- let hintsHtml = "" ;
37- for ( const adjacentLetters of incorrectLtrIndices ) {
38- for ( const indx of adjacentLetters ) {
39- const blockLeft = ( activeWordLetters [ indx ] as HTMLElement ) . offsetLeft ;
40- const blockWidth = ( activeWordLetters [ indx ] as HTMLElement ) . offsetWidth ;
41- const blockIndices = `[${ indx } ]` ;
42- const blockChars = inputChars [ indx ] ;
43-
44- hintsHtml +=
45- `<hint data-length=1 data-chars-index=${ blockIndices } ` +
46- ` style="left: ${ blockLeft + blockWidth / 2 } px;">${ blockChars } </hint>` ;
47- }
48- }
49- hintsHtml = `<div class="hints">${ hintsHtml } </div>` ;
50- return hintsHtml ;
51- }
52-
53- async function joinOverlappingHints (
54- incorrectLtrIndices : number [ ] [ ] ,
55- activeWordLetters : NodeListOf < Element > ,
56- hintElements : HTMLCollection
57- ) : Promise < void > {
58- const currentLanguage = await JSONData . getCurrentLanguage ( Config . language ) ;
59- const isLanguageRTL = currentLanguage . rightToLeft ;
60-
61- let i = 0 ;
62- for ( const adjacentLetters of incorrectLtrIndices ) {
63- for ( let j = 0 ; j < adjacentLetters . length - 1 ; j ++ ) {
64- const block1El = hintElements [ i ] as HTMLElement ;
65- const block2El = hintElements [ i + 1 ] as HTMLElement ;
66- const leftBlock = isLanguageRTL ? block2El : block1El ;
67- const rightBlock = isLanguageRTL ? block1El : block2El ;
68-
69- /** HintBlock.offsetLeft is at the center line of corresponding letters
70- * then "transform: translate(-50%)" aligns hints with letters */
71- if (
72- leftBlock . offsetLeft + leftBlock . offsetWidth / 2 >
73- rightBlock . offsetLeft - rightBlock . offsetWidth / 2
74- ) {
75- block1El . dataset [ "length" ] = (
76- parseInt ( block1El . dataset [ "length" ] ?? "1" ) +
77- parseInt ( block2El . dataset [ "length" ] ?? "1" )
78- ) . toString ( ) ;
79-
80- const block1Indices = block1El . dataset [ "charsIndex" ] ?? "[]" ;
81- const block2Indices = block2El . dataset [ "charsIndex" ] ?? "[]" ;
82- block1El . dataset [ "charsIndex" ] =
83- block1Indices . slice ( 0 , - 1 ) + "," + block2Indices . slice ( 1 ) ;
84-
85- const letter1Index = adjacentLetters [ j ] ?? 0 ;
86- const newLeft =
87- ( activeWordLetters [ letter1Index ] as HTMLElement ) . offsetLeft +
88- ( isLanguageRTL
89- ? ( activeWordLetters [ letter1Index ] as HTMLElement ) . offsetWidth
90- : 0 ) +
91- ( block2El . offsetLeft - block1El . offsetLeft ) ;
92- block1El . style . left = newLeft . toString ( ) + "px" ;
93-
94- block1El . insertAdjacentHTML ( "beforeend" , block2El . innerHTML ) ;
95-
96- block2El . remove ( ) ;
97- adjacentLetters . splice ( j + 1 , 1 ) ;
98- i -= j === 0 ? 1 : 2 ;
99- j -= j === 0 ? 1 : 2 ;
100- }
101- i ++ ;
102- }
103- i ++ ;
104- }
105- }
106-
10730const debouncedZipfCheck = debounce ( 250 , async ( ) => {
10831 const supports = await JSONData . checkIfLanguageSupportsZipf ( Config . language ) ;
10932 if ( supports === "no" ) {
@@ -130,6 +53,11 @@ const debouncedZipfCheck = debounce(250, async () => {
13053 }
13154} ) ;
13255
56+ export const updateHintsPositionDebounced = Misc . debounceUntilResolved (
57+ updateHintsPosition ,
58+ { rejectSkippedCalls : false }
59+ ) ;
60+
13361ConfigEvent . subscribe ( ( eventKey , eventValue , nosave ) => {
13462 if (
13563 ( eventKey === "language" || eventKey === "funbox" ) &&
@@ -146,9 +74,7 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
14674 eventKey
14775 )
14876 ) {
149- updateHintsPosition ( ) . catch ( ( e : unknown ) => {
150- console . error ( e ) ;
151- } ) ;
77+ void updateHintsPositionDebounced ( ) ;
15278 }
15379
15480 if ( eventKey === "theme" ) void applyBurstHeatmap ( ) ;
@@ -269,51 +195,175 @@ export function updateActiveElement(
269195 }
270196}
271197
272- export async function updateHintsPosition ( ) : Promise < void > {
198+ function createHintsHtml (
199+ incorrectLettersIndices : number [ ] [ ] ,
200+ activeWordLetters : NodeListOf < Element > ,
201+ input : string | string [ ] ,
202+ wrapWithDiv : boolean = true
203+ ) : string {
204+ // if input is an array, it contains only incorrect letters input.
205+ // if input is a string, it contains the whole word input.
206+ const isFullWord = typeof input === "string" ;
207+ const inputChars = isFullWord ? Strings . splitIntoCharacters ( input ) : input ;
208+
209+ let hintsHtml = "" ;
210+ let currentHint = 0 ;
211+
212+ for ( const adjacentLetters of incorrectLettersIndices ) {
213+ for ( const letterIndex of adjacentLetters ) {
214+ const letter = activeWordLetters [ letterIndex ] as HTMLElement ;
215+ const blockIndices = `${ letterIndex } ` ;
216+ const blockChars = isFullWord
217+ ? inputChars [ letterIndex ]
218+ : inputChars [ currentHint ++ ] ;
219+
220+ hintsHtml += `<hint data-chars-index=${ blockIndices } style="left:${
221+ letter . offsetLeft + letter . offsetWidth / 2
222+ } px;">${ blockChars } </hint>`;
223+ }
224+ }
225+ if ( wrapWithDiv ) hintsHtml = `<div class="hints">${ hintsHtml } </div>` ;
226+ return hintsHtml ;
227+ }
228+
229+ async function joinOverlappingHints (
230+ incorrectLettersIndices : number [ ] [ ] ,
231+ activeWordLetters : NodeListOf < Element > ,
232+ hintElements : HTMLCollection
233+ ) : Promise < void > {
234+ const currentLanguage = await JSONData . getCurrentLanguage ( Config . language ) ;
235+ const isLanguageRTL = currentLanguage . rightToLeft ;
236+
237+ let firstHintInSeq = 0 ;
238+ for ( const adjacentLettersSequence of incorrectLettersIndices ) {
239+ const lastHintInSeq = firstHintInSeq + adjacentLettersSequence . length - 1 ;
240+ joinHintsOfAdjacentLetters ( firstHintInSeq , lastHintInSeq ) ;
241+ firstHintInSeq += adjacentLettersSequence . length ;
242+ }
243+
244+ function joinHintsOfAdjacentLetters (
245+ firstHintInSequence : number ,
246+ lastHintInSequence : number
247+ ) : void {
248+ let currentHint = firstHintInSequence ;
249+
250+ while ( currentHint < lastHintInSequence ) {
251+ const block1El = hintElements [ currentHint ] as HTMLElement ;
252+ const block2El = hintElements [ currentHint + 1 ] as HTMLElement ;
253+
254+ const block1Indices = block1El . dataset [ "charsIndex" ] ?. split ( "," ) ?? [ ] ;
255+ const block2Indices = block2El . dataset [ "charsIndex" ] ?. split ( "," ) ?? [ ] ;
256+
257+ const block1Letter1Indx = parseInt ( block1Indices [ 0 ] ?? "0" ) ;
258+ const block2Letter1Indx = parseInt ( block2Indices [ 0 ] ?? "0" ) ;
259+
260+ const block1Letter1 = activeWordLetters [ block1Letter1Indx ] as HTMLElement ;
261+ const block2Letter1 = activeWordLetters [ block2Letter1Indx ] as HTMLElement ;
262+
263+ const leftBlock = isLanguageRTL ? block2El : block1El ;
264+ const rightBlock = isLanguageRTL ? block1El : block2El ;
265+
266+ // block edge is offset half its width because of transform: translate(-50%)
267+ const leftBlockEnds = leftBlock . offsetLeft + leftBlock . offsetWidth / 2 ;
268+ const rightBlockStarts =
269+ rightBlock . offsetLeft - rightBlock . offsetWidth / 2 ;
270+
271+ const sameTop = block1Letter1 . offsetTop === block2Letter1 . offsetTop ;
272+
273+ if ( sameTop && leftBlockEnds > rightBlockStarts ) {
274+ // join hint blocks
275+ block1El . dataset [ "charsIndex" ] = [
276+ ...block1Indices ,
277+ ...block2Indices ,
278+ ] . join ( "," ) ;
279+
280+ const block1Letter1Pos =
281+ block1Letter1 . offsetLeft +
282+ ( isLanguageRTL ? block1Letter1 . offsetWidth : 0 ) ;
283+ const bothBlocksLettersWidthHalved =
284+ block2El . offsetLeft - block1El . offsetLeft ;
285+ block1El . style . left =
286+ block1Letter1Pos + bothBlocksLettersWidthHalved + "px" ;
287+
288+ block1El . insertAdjacentHTML ( "beforeend" , block2El . innerHTML ) ;
289+ block2El . remove ( ) ;
290+
291+ // after joining blocks, the sequence is shorter
292+ lastHintInSequence -- ;
293+ // check if the newly formed block overlaps with the previous one
294+ currentHint -- ;
295+ if ( currentHint < firstHintInSeq ) currentHint = firstHintInSeq ;
296+ } else {
297+ currentHint ++ ;
298+ }
299+ }
300+ }
301+ }
302+
303+ async function updateHintsPosition ( ) : Promise < void > {
273304 if (
274305 ActivePage . get ( ) !== "test" ||
275306 resultVisible ||
276307 Config . indicateTypos !== "below"
277308 )
278309 return ;
279310
280- const currentLanguage = await JSONData . getCurrentLanguage ( Config . language ) ;
281- const isLanguageRTL = currentLanguage . rightToLeft ;
311+ let previousHintsContainer : HTMLElement | undefined ;
312+ let hintIndices : number [ ] [ ] = [ ] ;
313+ let hintText : string [ ] = [ ] ;
282314
283- let wordEl : HTMLElement | undefined ;
284- let letterElements : NodeListOf < Element > | undefined ;
315+ const hintElements = document . querySelectorAll < HTMLElement > ( ".hints > hint" ) ;
285316
286- const hintElements = document
287- . getElementById ( "words" )
288- ?. querySelectorAll ( "div.word > div.hints > hint" ) ;
289- for ( let i = 0 ; i < ( hintElements ?. length ?? 0 ) ; i ++ ) {
290- const hintEl = hintElements ?. [ i ] as HTMLElement ;
317+ for ( const hintEl of hintElements ) {
318+ const hintsContainer = hintEl . parentElement as HTMLElement ;
291319
292- if ( ! wordEl || hintEl . parentElement ?. parentElement !== wordEl ) {
293- wordEl = hintEl . parentElement ?. parentElement as HTMLElement ;
294- letterElements = wordEl ?. querySelectorAll ( "letter" ) ;
320+ if ( hintsContainer !== previousHintsContainer ) {
321+ await adjustHintsContainer ( previousHintsContainer , hintIndices , hintText ) ;
322+ previousHintsContainer = hintsContainer ;
323+ hintIndices = [ ] ;
324+ hintText = [ ] ;
295325 }
296326
297327 const letterIndices = hintEl . dataset [ "charsIndex" ]
298- ?. slice ( 1 , - 1 )
299- . split ( "," )
300- . map ( ( indx ) => parseInt ( indx ) ) ;
301- const leftmostIndx = isLanguageRTL
302- ? parseInt ( hintEl . dataset [ "length" ] ?? "1" ) - 1
303- : 0 ;
304-
305- const el = letterElements ?. [
306- letterIndices ?. [ leftmostIndx ] ?? 0
307- ] as HTMLElement ;
308- let newLeft = el . offsetLeft ;
309- const lettersWidth =
310- letterIndices ?. reduce ( ( accum , curr ) => {
311- const el = letterElements ?. [ curr ] as HTMLElement ;
312- return accum + el . offsetWidth ;
313- } , 0 ) ?? 0 ;
314- newLeft += lettersWidth / 2 ;
315-
316- hintEl . style . left = newLeft . toString ( ) + "px" ;
328+ ?. split ( "," )
329+ . map ( ( index ) => parseInt ( index ) ) ;
330+
331+ if ( letterIndices === undefined || letterIndices . length === 0 ) continue ;
332+
333+ for ( const currentLetterIndex of letterIndices ) {
334+ const lastBlock = hintIndices [ hintIndices . length - 1 ] ;
335+ if (
336+ lastBlock &&
337+ lastBlock [ lastBlock . length - 1 ] === currentLetterIndex - 1
338+ ) {
339+ lastBlock . push ( currentLetterIndex ) ;
340+ } else {
341+ hintIndices . push ( [ currentLetterIndex ] ) ;
342+ }
343+ }
344+
345+ hintText . push ( ...Strings . splitIntoCharacters ( hintEl . innerHTML ) ) ;
346+ }
347+ await adjustHintsContainer ( previousHintsContainer , hintIndices , hintText ) ;
348+
349+ async function adjustHintsContainer (
350+ hintsContainer : HTMLElement | undefined ,
351+ hintIndices : number [ ] [ ] ,
352+ hintText : string [ ]
353+ ) : Promise < void > {
354+ if ( ! hintsContainer || hintIndices . length === 0 ) return ;
355+
356+ const wordElement = hintsContainer . parentElement as HTMLElement ;
357+ const letterElements = wordElement . querySelectorAll < HTMLElement > ( "letter" ) ;
358+
359+ hintsContainer . innerHTML = createHintsHtml (
360+ hintIndices ,
361+ letterElements ,
362+ hintText ,
363+ false
364+ ) ;
365+ const wordHintsElements = wordElement . getElementsByTagName ( "hint" ) ;
366+ await joinOverlappingHints ( hintIndices , letterElements , wordHintsElements ) ;
317367 }
318368}
319369
@@ -589,7 +639,7 @@ export function updateWordsWrapperHeight(force = false): void {
589639
590640function updateWordsMargin ( ) : void {
591641 if ( Config . tapeMode !== "off" ) {
592- void scrollTape ( true ) ;
642+ void scrollTape ( true , updateHintsPositionDebounced ) ;
593643 } else {
594644 const wordsEl = document . getElementById ( "words" ) as HTMLElement ;
595645 const afterNewlineEls =
@@ -603,6 +653,7 @@ function updateWordsMargin(): void {
603653 {
604654 duration : SlowTimer . get ( ) ? 0 : 125 ,
605655 queue : "leftMargin" ,
656+ complete : updateHintsPositionDebounced ,
606657 }
607658 ) ;
608659 jqWords . dequeue ( "leftMargin" ) ;
@@ -614,6 +665,7 @@ function updateWordsMargin(): void {
614665 for ( const afterNewline of afterNewlineEls ) {
615666 afterNewline . style . marginLeft = `0` ;
616667 }
668+ void updateHintsPositionDebounced ( ) ;
617669 }
618670 }
619671}
@@ -760,12 +812,10 @@ export async function updateActiveWordLetters(
760812 : currentLetter ) +
761813 "</letter>" ;
762814 if ( Config . indicateTypos === "below" ) {
763- if ( ! hintIndices ?. length ) hintIndices . push ( [ i ] ) ;
764- else {
765- const lastblock = hintIndices [ hintIndices . length - 1 ] ;
766- if ( lastblock ?. [ lastblock . length - 1 ] === i - 1 ) lastblock . push ( i ) ;
767- else hintIndices . push ( [ i ] ) ;
768- }
815+ const lastBlock = hintIndices [ hintIndices . length - 1 ] ;
816+ if ( lastBlock && lastBlock [ lastBlock . length - 1 ] === i - 1 )
817+ lastBlock . push ( i ) ;
818+ else hintIndices . push ( [ i ] ) ;
769819 }
770820 }
771821 }
@@ -827,7 +877,10 @@ function getNlCharWidth(
827877 return nlChar . offsetWidth + letterMargin ;
828878}
829879
830- export async function scrollTape ( noRemove = false ) : Promise < void > {
880+ export async function scrollTape (
881+ noRemove = false ,
882+ afterCompleteFn ?: ( ) => void
883+ ) : Promise < void > {
831884 if ( ActivePage . get ( ) !== "test" || resultVisible ) return ;
832885
833886 await centeringActiveLine ;
@@ -1007,6 +1060,7 @@ export async function scrollTape(noRemove = false): Promise<void> {
10071060 {
10081061 duration : SlowTimer . get ( ) ? 0 : 125 ,
10091062 queue : "leftMargin" ,
1063+ complete : afterCompleteFn ,
10101064 }
10111065 ) ;
10121066 jqWords . dequeue ( "leftMargin" ) ;
@@ -1022,6 +1076,7 @@ export async function scrollTape(noRemove = false): Promise<void> {
10221076 const newMargin = afterNewlinesNewMargins [ i ] ?? 0 ;
10231077 ( afterNewLineEls [ i ] as HTMLElement ) . style . marginLeft = `${ newMargin } px` ;
10241078 }
1079+ if ( afterCompleteFn ) afterCompleteFn ( ) ;
10251080 }
10261081}
10271082
0 commit comments