Added tape mode (#2862)

* added config param for tape mode

* added settings section

* added commands

* moving caret to the correct place with tape mode is active

* only jumping line when not using tape mode

* prettier

* scrolling tape

* restarting test when tape mode changed

* showing one line when tape mode is active

* added function for scrolling words horizontally

* scrolling tape when layout emulator is enabled

* removed left margin when tape is active

* disable tape for some challenges

* fixed incorrect caret position when going back to a previous word

* ignoring no-this-alias

* fixed lines scrolling to often when tape mode was disabled

* storing caret width in a variable

* not allowing tape mode and show all lines together

* removed unnecessary comment
This commit is contained in:
Jack 2022-04-24 17:04:46 +02:00 committed by GitHub
parent d65fcff79d
commit 9ca477da54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 283 additions and 24 deletions

View file

@ -88,6 +88,7 @@ const CONFIG_SCHEMA = joi.object({
minWpm: joi.string().valid("off", "custom"),
minWpmCustomSpeed: joi.number().min(0),
highlightMode: joi.string().valid("off", "letter", "word"),
tapeMode: joi.string().valid("off", "letter", "word"),
alwaysShowCPM: joi.boolean(),
enableAds: joi.string().valid("off", "on", "max"),
hideExtraLetters: joi.boolean(),

View file

@ -528,6 +528,11 @@ export function setCapsLockWarning(val: boolean, nosave?: boolean): boolean {
export function setShowAllLines(sal: boolean, nosave?: boolean): boolean {
if (!isConfigValueValid("show all lines", sal, ["boolean"])) return false;
if (sal && config.tapeMode !== "off") {
Notifications.add("Show all lines doesn't support tape mode", 0);
return false;
}
config.showAllLines = sal;
saveToLocalStorage("showAllLines", nosave);
ConfigEvent.dispatch("showAllLines", config.showAllLines, nosave);
@ -832,6 +837,25 @@ export function setHighlightMode(
return true;
}
export function setTapeMode(
mode: MonkeyTypes.TapeMode,
nosave?: boolean
): boolean {
if (!isConfigValueValid("tape mode", mode, [["off", "letter", "word"]])) {
return false;
}
if (mode !== "off" && config.showAllLines === true) {
setShowAllLines(false, true);
}
config.tapeMode = mode;
saveToLocalStorage("tapeMode", nosave);
ConfigEvent.dispatch("tapeMode", config.tapeMode);
return true;
}
export function setHideExtraLetters(val: boolean, nosave?: boolean): boolean {
if (!isConfigValueValid("hide extra letters", val, ["boolean"])) return false;
@ -1793,6 +1817,7 @@ export function apply(
setBritishEnglish(configObj.britishEnglish, true);
setLazyMode(configObj.lazyMode, true);
setShowAverage(configObj.showAverage, true);
setTapeMode(configObj.tapeMode, true);
try {
setEnableAds(configObj.enableAds, true);

View file

@ -96,4 +96,5 @@ export default <MonkeyTypes.Config>{
britishEnglish: false,
lazyMode: false,
showAverage: "off",
tapeMode: "off",
};

View file

@ -255,7 +255,11 @@ function handleSpace(): void {
nextTop = 0;
}
if (nextTop > currentTop && !TestUI.lineTransition) {
if (
Config.tapeMode === "off" &&
nextTop > currentTop &&
!TestUI.lineTransition
) {
TestUI.lineJump(currentTop);
}
} //end of line wrap
@ -816,6 +820,9 @@ $(document).keydown(async (event) => {
handleChar(char, TestInput.input.current.length);
updateUI();
setWordsInput(" " + TestInput.input.current);
if (Config.tapeMode !== "off") {
TestUI.scrollTape();
}
}
}
});
@ -888,6 +895,9 @@ $("#wordsInput").on("input", (event) => {
setWordsInput(" " + TestInput.input.current);
updateUI();
if (Config.tapeMode !== "off") {
TestUI.scrollTape();
}
// force caret at end of input
// doing it on next cycle because Chromium on Android won't let me edit

View file

@ -1729,6 +1729,37 @@ const commandsHighlightMode: MonkeyTypes.CommandsGroup = {
],
};
const commandsTapeMode: MonkeyTypes.CommandsGroup = {
title: "Tape mode...",
configKey: "tapeMode",
list: [
{
id: "setTapeModeOff",
display: "off",
configValue: "off",
exec: (): void => {
UpdateConfig.setTapeMode("off");
},
},
{
id: "setTapeModeLetter",
display: "letter",
configValue: "letter",
exec: (): void => {
UpdateConfig.setTapeMode("letter");
},
},
{
id: "setTapeModeWord",
display: "word",
configValue: "word",
exec: (): void => {
UpdateConfig.setTapeMode("word");
},
},
],
};
const commandsTimerStyle: MonkeyTypes.CommandsGroup = {
title: "Timer/progress style...",
configKey: "timerStyle",
@ -2948,6 +2979,12 @@ export const defaultCommands: MonkeyTypes.CommandsGroup = {
icon: "fa-highlighter",
subgroup: commandsHighlightMode,
},
{
id: "changeTapeMode",
display: "Tape mode...",
icon: "fa-tape",
subgroup: commandsTapeMode,
},
{
id: "changeShowAverage",
display: "Show average...",

View file

@ -334,6 +334,11 @@ async function initGroups(): Promise<void> {
UpdateConfig.setHighlightMode,
"button"
);
groups["tapeMode"] = new SettingsGroup(
"tapeMode",
UpdateConfig.setTapeMode,
"button"
);
groups["timerOpacity"] = new SettingsGroup(
"timerOpacity",
UpdateConfig.setTimerOpacity,

View file

@ -36,6 +36,9 @@ export async function updatePosition(): Promise<void> {
// }
const caret = $("#caret");
const caretWidth = Math.round(
document.querySelector("#caret")?.getBoundingClientRect().width ?? 0
);
let inputLen = TestInput.input.current.length;
inputLen = Misc.trailingComposeChars.test(TestInput.input.current)
@ -78,18 +81,43 @@ export async function updatePosition(): Promise<void> {
let newLeft = 0;
newTop = currentLetterPosTop - Math.round(letterHeight / 5);
if (inputLen == 0) {
newLeft = isLanguageLeftToRight
? currentLetterPosLeft - (caret.width() as number) / 2
: currentLetterPosLeft + (caret.width() as number) / 2;
if (Config.tapeMode === "letter") {
newLeft =
($(<HTMLElement>document.querySelector("#wordsWrapper")).width() ?? 0) /
2 -
caretWidth / 2;
} else if (Config.tapeMode === "word") {
if (inputLen == 0) {
newLeft =
($(<HTMLElement>document.querySelector("#wordsWrapper")).width() ?? 0) /
2 -
caretWidth / 2;
} else {
let inputWidth = 0;
for (let i = 0; i < inputLen; i++) {
inputWidth += $(currentWordNodeList[i]).outerWidth(true) as number;
}
newLeft =
($(<HTMLElement>document.querySelector("#wordsWrapper")).width() ?? 0) /
2 +
inputWidth -
caretWidth / 2;
}
} else {
newLeft = isLanguageLeftToRight
? currentLetterPosLeft +
($(currentLetter).width() as number) -
(caret.width() as number) / 2
: currentLetterPosLeft -
($(currentLetter).width() as number) +
(caret.width() as number) / 2;
if (inputLen == 0) {
newLeft = isLanguageLeftToRight
? currentLetterPosLeft - caretWidth / 2
: currentLetterPosLeft + caretWidth / 2;
} else {
newLeft = isLanguageLeftToRight
? currentLetterPosLeft +
($(currentLetter).width() as number) -
caretWidth / 2
: currentLetterPosLeft -
($(currentLetter).width() as number) +
caretWidth / 2;
}
}
let smoothlinescroll = $("#words .smoothScroller").height();

View file

@ -1736,8 +1736,9 @@ $(document).on("click", "#top #menu #startTestButton, #top .logo", () => {
ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
if (eventKey === "difficulty" && !nosave) restart(false, nosave);
if (eventKey === "showAllLines" && !nosave) restart();
if (eventKey === "showAllLines" && !nosave) restart(false, nosave);
if (eventKey === "keymapMode" && !nosave) restart(false, nosave);
if (eventKey === "tapeMode" && !nosave) restart(false, nosave);
if (eventKey === "lazyMode" && eventValue === false && !nosave) {
rememberLazyMode = false;
}

View file

@ -167,13 +167,29 @@ export function showWords(): void {
}
$(".outOfFocusWarning").css("line-height", nh + "px");
} else {
$("#words")
.css("height", wordHeight * 4 + "px")
.css("overflow", "hidden");
$("#wordsWrapper")
.css("height", wordHeight * 3 + "px")
.css("overflow", "hidden");
$(".outOfFocusWarning").css("line-height", wordHeight * 3 + "px");
if (Config.tapeMode !== "off") {
$("#words")
.css("height", wordHeight * 2 + "px")
.css("overflow", "hidden")
.css("width", "200%")
.css("margin-left", "50%");
$("#words").addClass("tape");
$("#wordsWrapper")
.css("height", wordHeight * 1 + "px")
.css("overflow", "hidden");
$(".outOfFocusWarning").css("line-height", wordHeight * 1 + "px");
} else {
$("#words")
.css("height", wordHeight * 4 + "px")
.css("overflow", "hidden")
.css("width", "100%")
.css("margin-left", "unset");
$("#words").removeClass("tape");
$("#wordsWrapper")
.css("height", wordHeight * 3 + "px")
.css("overflow", "hidden");
$(".outOfFocusWarning").css("line-height", wordHeight * 3 + "px");
}
}
if (Config.mode === "zen") {
@ -481,6 +497,60 @@ export function updateWordElement(showError = !Config.blindMode): void {
if (newlineafter) $("#words").append("<div class='newline'></div>");
}
export function scrollTape(): void {
const wordsWrapperWidth = (<HTMLElement>(
document.querySelector("#wordsWrapper")
)).offsetWidth;
let fullWordsWidth = 0;
const toHide: JQuery<HTMLElement>[] = [];
let widthToHide = 0;
if (currentWordElementIndex > 0) {
for (let i = 0; i < currentWordElementIndex; i++) {
const word = <HTMLElement>document.querySelectorAll("#words .word")[i];
fullWordsWidth += $(word).outerWidth(true) ?? 0;
const forWordLeft = Math.floor(word.offsetLeft);
const forWordWidth = Math.floor(word.offsetWidth);
if (forWordLeft < 0 - forWordWidth) {
const toPush = $($("#words .word")[i]);
toHide.push(toPush);
widthToHide += toPush.outerWidth(true) ?? 0;
}
}
if (toHide.length > 0) {
currentWordElementIndex -= toHide.length;
toHide.forEach((e) => e.remove());
fullWordsWidth -= widthToHide;
const currentMargin = parseInt($("#words").css("margin-left"), 10);
$("#words").css("margin-left", `${currentMargin + widthToHide}px`);
}
}
let currentWordWidth = 0;
if (Config.tapeMode === "letter") {
if (TestInput.input.current.length > 0) {
for (let i = 0; i < TestInput.input.current.length; i++) {
const words = document.querySelectorAll("#words .word");
currentWordWidth +=
$(
words[currentWordElementIndex].querySelectorAll("letter")[i]
).outerWidth(true) ?? 0;
}
}
}
const newMargin = wordsWrapperWidth / 2 - (fullWordsWidth + currentWordWidth);
if (Config.smoothLineScroll) {
$("#words")
.stop(true, false)
.animate(
{
marginLeft: newMargin,
},
SlowTimer.get() ? 0 : 125
);
} else {
$("#words").css("margin-left", `${newMargin}px`);
}
}
export function lineJump(currentTop: number): void {
//last word of the line
if (currentTestLine > 0) {

View file

@ -71,6 +71,8 @@ declare namespace MonkeyTypes {
type ShowAverage = "off" | "wpm" | "acc" | "both";
type TapeMode = "off" | "letter" | "word";
type SingleListCommandLine = "manual" | "on";
/*
@ -377,6 +379,7 @@ declare namespace MonkeyTypes {
britishEnglish: boolean;
lazyMode: boolean;
showAverage: ShowAverage;
tapeMode: TapeMode;
}
type ConfigValues =

View file

@ -253,6 +253,33 @@
}
}
}
&.tape {
&.size125 .word {
margin: 0.31rem;
margin: 0.31rem 0.62rem 0.31rem 0;
}
&.size15 .word {
margin: 0.37rem;
margin: 0.37rem 0.74rem 0.37rem 0;
}
&.size2 .word {
margin: 0.5rem;
margin: 0.5rem 1rem 0.5rem 0;
}
&.size3 .word {
margin: 0.75rem;
margin: 0.75rem 1.5rem 0.75rem 0;
}
&.size4 .word {
margin: 1rem;
margin: 1rem 2rem 1rem 0;
}
}
}
.word {

View file

@ -246,19 +246,34 @@
"name": "inAGalaxyFarFarAway",
"display": "In a galaxy far far away",
"type": "script",
"parameters": ["episode4.txt",null,"space_balls"]
"parameters": ["episode4.txt",null,"space_balls"],
"requirements": {
"config": {
"tapeMode": "off"
}
}
}
,{
"name": "whosYourDaddy",
"display": "Who's your daddy?",
"type": "script",
"parameters": ["episode5.txt",null,"space_balls"]
"parameters": ["episode5.txt",null,"space_balls"],
"requirements": {
"config": {
"tapeMode": "off"
}
}
}
,{
"name": "itsATrap",
"display": "It's a trap!",
"type": "script",
"parameters": ["episode6.txt",null,"space_balls"]
"parameters": ["episode6.txt",null,"space_balls"],
"requirements": {
"config": {
"tapeMode": "off"
}
}
}
,{
"name": "jolly",
@ -399,7 +414,12 @@
"name": "mnemonist",
"display": "Mnemonist",
"type": "funbox",
"parameters": ["memory","words",25,"master"]
"parameters": ["memory","words",25,"master"],
"requirements": {
"config": {
"tapeMode": "off"
}
}
}
,{
"name": "earfquake",
@ -461,6 +481,9 @@
},
"funbox": {
"exact": "read_ahead"
},
"config": {
"tapeMode": "off"
}
}
}
@ -479,6 +502,9 @@
},
"funbox": {
"exact": "read_ahead_hard"
},
"config": {
"tapeMode": "off"
}
}
}

View file

@ -1257,6 +1257,31 @@
</div>
</div>
</div>
<div class="section tapeMode" section="">
<h1>tape mode</h1>
<div class="text">
Only shows one line which scrolls horizontally. Setting this to 'word'
will make it scroll after every word and 'letter' will scroll after
every keypress. Works best with smooth line scroll enabled and a
monospace font.
</div>
<div class="buttons">
<div class="button" tapeMode="off" tabindex="0" onclick="this.blur();">
off
</div>
<div
class="button"
tapeMode="letter"
tabindex="0"
onclick="this.blur();"
>
letter
</div>
<div class="button" tapeMode="word" tabindex="0" onclick="this.blur();">
word
</div>
</div>
</div>
<div class="section smoothLineScroll">
<h1>smooth line scroll</h1>
<div class="text">