Skip to content

Commit 9a0fee2

Browse files
nadalabaMiodec
andauthored
impr(tape mode): add multiline support for tape mode (@nadalaba, @Miodec) (monkeytypegame#5868)
![newTape](https://github.com/user-attachments/assets/207b2ac0-e6c2-404a-95b3-e7633a5c375e) ***format:*** *file-name: line number on pr: `function name`* *- message* 1. test-ui.ts: 448: `updateWordsInputPosition` & ui.ts: 109: `debouncedEvent` - calculate #wordsInput left position in tape mode same way when tapeMode is off (because tape mode can have zen mode and will accept RTL later. Also, now tape mode can have 3 lines, and #wordsInput needs to go down to the 2nd line). - call updateWordsInputPosition() on window resize which solves monkeytypegame#6093 in a simpler way than a2f6c1f. 2. test-ui.ts: 551: `updateWordsWrapperHeight` - on zen mode make wrapper height 2 lines, and move conditions around. - on tape mode make wrapper height min(#words.Height, .word.Height * 3) because #words now have some padding to prevent hints from being cut off in 1-line tap mode. 3. test-ui.ts: 285: `updateActiveElement` - don't call scrollTape() on initial call of updateActiveElement() because it'll be called by other functions (i.e, showWords() and updateActiveWordLetters()). 4. test-ui.ts: 796: `updateActiveWordLetters` - move activeWord definition to the top of the function to return if it's not defined, and use type argument instead of "as". 5. test-ui.ts: 356: `getWordHTML` & test-ui.ts: 945: `updateActiveWordLetters` - add .beforeNewline and .afterNewline elements with each added .newline element. - the point of .afterNewline is to indent the next line, and the point of .beforeNewline is to act as a filler for the top line when the words of that line get removed in scrollTape() because they were overflown horizontally. 6. test.scss: 176+188+279: `#words`+`#words.tape` - change #words display from flex to block and make .word, .afterNewline and .beforeNewline elements display: inline-block (but keep .newline as block) in order to use white-space: nowrap, but still be able to break on demand with block elements .newline. - also, make default .word margin-bottom in tape mode 0.25em just like in non-tape mode, since they are now practically similar. - make the height of .beforeNewline identical to the height of .word so that when all top-line-words are removed, the user won't feel a vertical shift in lines. - use vertical-align: top in .word and .beforeNewline, because in lineJump(), we rely on their offsetTop to be the same if they are on the same line. - add padding-bottom: 0.5em to #words to prevent hints from being cut-off in the last line of test when showAllLines= on and in 1-line tape mode. 7. input-controller: 169: `backspaceToPrevious` - remove .beforeNewline and .afterNewline elements with .newline elements when backspacing to a higher line in zen mode. 8. test-ui.ts: 590: `updateWordsMargin` - when tape mode is turned on, first adjust the margins and then remove overflown elements (this is what passing true to scrollTape() does), and when it's turned off unset the margins of both #words and .afterNewline. 9. test-ui.ts: 1168: `removeElementsBeforeWord` - add a new helper function that removes all elements (except .smoothScroller), before (and including) the input element and returns the removed .word elements (removes .newline/.afterNewline/.beforeNewline/.word elements, but returns number of removed .words only) 10. test-ui.ts: 1191: `lineJump` - some refactoring: save HTMLelements in const instead of repeatedly querying the DOM. - allow lineJump() to be called in force (even when currentTestLine === 0), which is useful when changing Config.showAllLines to off (currentTestLine stays at zero in showAllLines=on), in order to lineJump and keep the active word on the 2nd line. - make the conditions to run lineJump() similar in tapeMode on and off (currentTestLine > 0, hideBound = currentTop - 10). - when determining the elements to hide, save the index of the last element to hide in a const and then remove it and everything before it when the animation completes. It is done like this instead of saving what needs to be hidden in an array, because .afterNewline elements have offsetTops that cannot be relied upon to determine if they need to be hidden or not. The new function removeElementsBeforeWord() does that. - last element to hide is now the last .word or .newline that is higher than the hideBound. - #words margin-top animation is done in its own queue so that we only .stop() margin-top animation without affecting margin-left animation. 11. test-logic.ts: 1434: `ConfigEvent.subscribe` - remove the restriction to allow changing showAllLines without restarting the test. 12. test-ui.ts: 492: `centerActiveLine` & ui.ts: 107: `debouncedEvent` - add a function centerActiveLine() that finds the top of the previous line and calls lineJump() passing that top to it. If the active word is on the 1st line it does nothing. - this is useful on window resize because it used to call lineJump() passing the top of the previous word to it, causing unnecessary line jumping if the active word was in the middle of the 2nd line (see gif below). - this is also useful when turning ShowAllLines off to hide (remove) all lines higher than the previous line. ![2025-04-06 18-18-34](https://github.com/user-attachments/assets/2a4a6842-1d61-44f2-a913-c0c6c7bbee27) 13. test-ui.ts: 190: `ConfigEvent.subscribe()` - call updateWordsWrapperHeight() on showAllLines change, and if the change was to 'off' call centerActiveLine() in order to keep the active word in the middle line. 14. test-ui.ts: 954: `getNlCharWidth` - add a new helper function that calculates the width of the nlChar letter that is in the last .word element before the input element, and check if the nlChar placeholder was incorrectly typed, if so return a width of 0. This last check is to minimize next line shifting behavior, see [video](https://discord.com/channels/713194177403420752/713196019206324306/1283880903382274119) 15. test-ui.ts: 978: `scrollTape` - remove leading .afterNewline elements. - get last element to loop over which is the 2nd .afterNewline after active word, or else the 1st one after active, or else stop at the active word. - in the main loop sum the widths of words before new line then add it to the left margin of the next .afterNewline, while also determining the widths of words before the active word (which will be in the new margin-left of #words), and determine what words have overflown the wrapper and need to be hidden. - if there is anything to remove, remove the overflown elements, and adjust margin-left of #words and .afterNewline by the width of what was removed. - calculate the width of the current word in tape=letter just like before then animate the new #words margin-left and .afterNewline elements' margin-left. - #words margin-left animation is done in its own queue so that when we use .stop() before the animation we'll only be stopping the margin-left animation and not the margin-top animation which is performed in lineJump(). 16. test-ui.ts: 492: `centerActiveLine` - scrollTape now awaits the promise centeringActiveLine which is always resolved except if showAllLines was on and tape mode was turned on through the commandline mid-test. In that case centeringActiveLine won't be resolved until lineJump completes its animation/style-change, so that scrollTape removing words won't conflict with lineJump removing words. Closes monkeytypegame#3907 --------- Co-authored-by: Miodec <[email protected]>
1 parent e436671 commit 9a0fee2

File tree

5 files changed

+372
-145
lines changed

5 files changed

+372
-145
lines changed

frontend/src/styles/test.scss

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@
173173
#words {
174174
height: fit-content;
175175
height: -moz-fit-content;
176+
padding-bottom: 0.5em; // to account for hints of the bottom line
176177
display: flex;
177178
flex-wrap: wrap;
178179
width: 100%;
@@ -184,6 +185,20 @@
184185
width: inherit;
185186
}
186187

188+
.beforeNewline {
189+
display: inline-block;
190+
vertical-align: top;
191+
margin: 0.25em 0;
192+
box-sizing: content-box;
193+
height: 1em; //.word line-height
194+
border-top: 0.05em solid transparent; // letter border-bottom
195+
border-bottom: 2px solid transparent; //.word border
196+
}
197+
198+
.afterNewline {
199+
display: inline-block;
200+
}
201+
187202
--correct-letter-color: var(--text-color);
188203
--correct-letter-animation: none;
189204
--untyped-letter-color: var(--sub-color);
@@ -261,10 +276,13 @@
261276
}
262277

263278
&.tape {
279+
display: block;
280+
white-space: nowrap;
264281
width: 200vw;
265282
.word {
266-
margin: 0.25em 0.6em 0.75em 0;
267-
white-space: nowrap;
283+
margin: 0.25em 0.6em 0.25em 0;
284+
display: inline-block;
285+
vertical-align: top;
268286
}
269287
}
270288

frontend/src/ts/controllers/input-controller.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,11 @@ function backspaceToPrevious(): void {
166166

167167
for (let i = els.length - 1; i >= 0; i--) {
168168
const el = els[i] as HTMLElement;
169-
if (el.classList.contains("newline")) {
169+
if (
170+
el.classList.contains("newline") ||
171+
el.classList.contains("beforeNewline") ||
172+
el.classList.contains("afterNewline")
173+
) {
170174
el.remove();
171175
} else {
172176
break;
@@ -351,7 +355,7 @@ async function handleSpace(): Promise<void> {
351355
}
352356

353357
if (nextTop > currentTop) {
354-
TestUI.lineJump(currentTop);
358+
void TestUI.lineJump(currentTop);
355359
} //end of line wrap
356360
}
357361

@@ -730,7 +734,7 @@ function handleChar(
730734
TestInput.input.current.length > 1
731735
) {
732736
if (Config.mode === "zen") {
733-
if (!Config.showAllLines) TestUI.lineJump(activeWordTopBeforeJump);
737+
if (!Config.showAllLines) void TestUI.lineJump(activeWordTopBeforeJump);
734738
} else {
735739
TestInput.input.current = TestInput.input.current.slice(0, -1);
736740
void TestUI.updateActiveWordLetters();

frontend/src/ts/test/test-logic.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1431,7 +1431,6 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
14311431
restart();
14321432
}
14331433
if (eventKey === "difficulty" && !nosave) restart();
1434-
if (eventKey === "showAllLines" && !nosave) restart();
14351434
if (
14361435
eventKey === "customLayoutFluid" &&
14371436
Config.funbox.includes("layoutfluid")

0 commit comments

Comments
 (0)