mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-12-29 11:26:13 +08:00
* Added fail on incorrect opposite shift for master and expert mode * Changed so the test only fails on master mode
849 lines
24 KiB
JavaScript
849 lines
24 KiB
JavaScript
import * as TestLogic from "./test-logic";
|
||
import * as TestUI from "./test-ui";
|
||
import * as TestStats from "./test-stats";
|
||
import * as Monkey from "./monkey";
|
||
import Config, * as UpdateConfig from "./config";
|
||
import * as Keymap from "./keymap";
|
||
import * as Misc from "./misc";
|
||
import * as LiveAcc from "./live-acc";
|
||
import * as LiveBurst from "./live-burst";
|
||
import * as Funbox from "./funbox";
|
||
import * as Sound from "./sound";
|
||
import * as Caret from "./caret";
|
||
import * as ManualRestart from "./manual-restart-tracker";
|
||
import * as Notifications from "./notifications";
|
||
import * as CustomText from "./custom-text";
|
||
import * as UI from "./ui";
|
||
import * as Settings from "./settings";
|
||
import * as LayoutEmulator from "./layout-emulator";
|
||
import * as PaceCaret from "./pace-caret";
|
||
import * as TimerProgress from "./timer-progress";
|
||
import * as TestTimer from "./test-timer";
|
||
import * as Focus from "./focus";
|
||
import * as ShiftTracker from "./shift-tracker";
|
||
import * as Replay from "./replay.js";
|
||
import * as MonkeyPower from "./monkey-power";
|
||
import * as WeakSpot from "./weak-spot";
|
||
|
||
let dontInsertSpace = false;
|
||
let correctShiftUsed = true;
|
||
|
||
function setWordsInput(value) {
|
||
// Only change #wordsInput if it's not already the wanted value
|
||
// Avoids Safari triggering unneeded events, causing issues with
|
||
// dead keys.
|
||
// console.log("settings words input to " + value);
|
||
if (value !== $("#wordsInput").val()) {
|
||
$("#wordsInput").val(value);
|
||
}
|
||
}
|
||
|
||
function updateUI() {
|
||
let acc = Misc.roundTo2(TestStats.calculateAccuracy());
|
||
if (!isNaN(acc)) LiveAcc.update(acc);
|
||
|
||
if (Config.keymapMode === "next" && Config.mode !== "zen") {
|
||
Keymap.highlightKey(
|
||
TestLogic.words
|
||
.getCurrent()
|
||
.charAt(TestLogic.input.current.length)
|
||
.toString()
|
||
.toUpperCase()
|
||
);
|
||
}
|
||
}
|
||
|
||
function backspaceToPrevious() {
|
||
if (!TestLogic.active) return;
|
||
|
||
if (
|
||
TestLogic.input.history.length == 0 ||
|
||
TestUI.currentWordElementIndex == 0
|
||
)
|
||
return;
|
||
|
||
if (
|
||
(TestLogic.input.history[TestLogic.words.currentIndex - 1] ==
|
||
TestLogic.words.get(TestLogic.words.currentIndex - 1) &&
|
||
!Config.freedomMode) ||
|
||
$($(".word")[TestLogic.words.currentIndex - 1]).hasClass("hidden")
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if (Config.confidenceMode === "on" || Config.confidenceMode === "max") {
|
||
return;
|
||
}
|
||
|
||
TestUI.updateWordElement();
|
||
TestLogic.input.current = TestLogic.input.popHistory();
|
||
TestLogic.corrected.popHistory();
|
||
|
||
if (Config.funbox === "nospace") {
|
||
TestLogic.input.current = TestLogic.input.current.slice(0, -1);
|
||
}
|
||
|
||
TestLogic.words.decreaseCurrentIndex();
|
||
TestUI.setCurrentWordElementIndex(TestUI.currentWordElementIndex - 1);
|
||
TestUI.updateActiveElement(true);
|
||
Funbox.toggleScript(TestLogic.words.getCurrent());
|
||
|
||
Caret.updatePosition();
|
||
Replay.addReplayEvent("backWord");
|
||
}
|
||
|
||
function handleSpace() {
|
||
if (!TestLogic.active) return;
|
||
|
||
if (TestLogic.input.current === "") return;
|
||
|
||
if (Config.mode == "zen") {
|
||
$("#words .word.active").removeClass("active");
|
||
$("#words").append("<div class='word active'></div>");
|
||
}
|
||
|
||
let currentWord = TestLogic.words.getCurrent();
|
||
if (Config.funbox === "layoutfluid" && Config.mode !== "time") {
|
||
// here I need to check if Config.customLayoutFluid exists because of my scuffed solution of returning whenever value is undefined in the setCustomLayoutfluid function
|
||
const layouts = Config.customLayoutfluid
|
||
? Config.customLayoutfluid.split("#")
|
||
: ["qwerty", "dvorak", "colemak"];
|
||
let index = 0;
|
||
let outof = TestLogic.words.length;
|
||
index = Math.floor(
|
||
(TestLogic.input.history.length + 1) / (outof / layouts.length)
|
||
);
|
||
if (Config.layout !== layouts[index] && layouts[index] !== undefined) {
|
||
Notifications.add(`--- !!! ${layouts[index]} !!! ---`, 0);
|
||
}
|
||
UpdateConfig.setLayout(layouts[index]);
|
||
UpdateConfig.setKeymapLayout(layouts[index]);
|
||
Keymap.highlightKey(
|
||
TestLogic.words
|
||
.getCurrent()
|
||
.charAt(TestLogic.input.current.length)
|
||
.toString()
|
||
.toUpperCase()
|
||
);
|
||
Settings.groups.layout.updateButton();
|
||
}
|
||
dontInsertSpace = true;
|
||
|
||
let burst = TestStats.calculateBurst();
|
||
LiveBurst.update(Math.round(burst));
|
||
TestStats.pushBurstToHistory(burst);
|
||
|
||
//correct word or in zen mode
|
||
const isWordCorrect =
|
||
currentWord == TestLogic.input.current || Config.mode == "zen";
|
||
MonkeyPower.addPower(isWordCorrect, true);
|
||
TestStats.incrementAccuracy(isWordCorrect);
|
||
if (isWordCorrect) {
|
||
PaceCaret.handleSpace(true, currentWord);
|
||
TestLogic.input.pushHistory();
|
||
TestLogic.words.increaseCurrentIndex();
|
||
TestUI.setCurrentWordElementIndex(TestUI.currentWordElementIndex + 1);
|
||
TestUI.updateActiveElement();
|
||
Funbox.toggleScript(TestLogic.words.getCurrent());
|
||
Caret.updatePosition();
|
||
TestStats.incrementKeypressCount();
|
||
TestStats.pushKeypressWord(TestLogic.words.currentIndex);
|
||
if (Config.funbox !== "nospace") {
|
||
Sound.playClick(Config.playSoundOnClick);
|
||
}
|
||
Replay.addReplayEvent("submitCorrectWord");
|
||
} else {
|
||
if (Config.funbox !== "nospace") {
|
||
if (!Config.playSoundOnError || Config.blindMode) {
|
||
Sound.playClick(Config.playSoundOnClick);
|
||
} else {
|
||
Sound.playError(Config.playSoundOnError);
|
||
}
|
||
}
|
||
TestStats.pushMissedWord(TestLogic.words.getCurrent());
|
||
TestStats.incrementKeypressErrors();
|
||
let cil = TestLogic.input.current.length;
|
||
if (cil <= TestLogic.words.getCurrent().length) {
|
||
if (cil >= TestLogic.corrected.current.length) {
|
||
TestLogic.corrected.current += "_";
|
||
} else {
|
||
TestLogic.corrected.current =
|
||
TestLogic.corrected.current.substring(0, cil) +
|
||
"_" +
|
||
TestLogic.corrected.current.substring(cil + 1);
|
||
}
|
||
}
|
||
if (Config.stopOnError != "off") {
|
||
if (Config.difficulty == "expert" || Config.difficulty == "master") {
|
||
//failed due to diff when pressing space
|
||
TestLogic.fail("difficulty");
|
||
return;
|
||
}
|
||
if (Config.stopOnError == "word") {
|
||
dontInsertSpace = false;
|
||
Replay.addReplayEvent("incorrectLetter", "_");
|
||
TestUI.updateWordElement(true);
|
||
Caret.updatePosition();
|
||
}
|
||
return;
|
||
}
|
||
PaceCaret.handleSpace(false, currentWord);
|
||
if (Config.blindMode) $("#words .word.active letter").addClass("correct");
|
||
TestLogic.input.pushHistory();
|
||
TestUI.highlightBadWord(TestUI.currentWordElementIndex, !Config.blindMode);
|
||
TestLogic.words.increaseCurrentIndex();
|
||
TestUI.setCurrentWordElementIndex(TestUI.currentWordElementIndex + 1);
|
||
TestUI.updateActiveElement();
|
||
Funbox.toggleScript(TestLogic.words.getCurrent());
|
||
Caret.updatePosition();
|
||
TestStats.incrementKeypressCount();
|
||
TestStats.pushKeypressWord(TestLogic.words.currentIndex);
|
||
TestStats.updateLastKeypress();
|
||
if (Config.difficulty == "expert" || Config.difficulty == "master") {
|
||
TestLogic.fail("difficulty");
|
||
return;
|
||
} else if (TestLogic.words.currentIndex == TestLogic.words.length) {
|
||
//submitted last word that is incorrect
|
||
TestLogic.finish();
|
||
return;
|
||
}
|
||
Replay.addReplayEvent("submitErrorWord");
|
||
}
|
||
|
||
let wordLength;
|
||
if (Config.mode === "zen") {
|
||
wordLength = TestLogic.input.current.length;
|
||
} else {
|
||
wordLength = TestLogic.words.getCurrent().length;
|
||
}
|
||
|
||
let flex = Misc.whorf(Config.minBurstCustomSpeed, wordLength);
|
||
if (
|
||
(Config.minBurst === "fixed" && burst < Config.minBurstCustomSpeed) ||
|
||
(Config.minBurst === "flex" && burst < flex)
|
||
) {
|
||
TestLogic.fail("min burst");
|
||
return;
|
||
}
|
||
|
||
TestLogic.corrected.pushHistory();
|
||
|
||
if (
|
||
!Config.showAllLines ||
|
||
Config.mode == "time" ||
|
||
(CustomText.isWordRandom && CustomText.word == 0) ||
|
||
CustomText.isTimeRandom
|
||
) {
|
||
let currentTop = Math.floor(
|
||
document.querySelectorAll("#words .word")[
|
||
TestUI.currentWordElementIndex - 1
|
||
].offsetTop
|
||
);
|
||
let nextTop;
|
||
try {
|
||
nextTop = Math.floor(
|
||
document.querySelectorAll("#words .word")[
|
||
TestUI.currentWordElementIndex
|
||
].offsetTop
|
||
);
|
||
} catch (e) {
|
||
nextTop = 0;
|
||
}
|
||
|
||
if (nextTop > currentTop && !TestUI.lineTransition) {
|
||
TestUI.lineJump(currentTop);
|
||
}
|
||
} //end of line wrap
|
||
|
||
if (Config.keymapMode === "react") {
|
||
Keymap.flashKey("Space", true);
|
||
}
|
||
if (
|
||
Config.mode === "words" ||
|
||
Config.mode === "custom" ||
|
||
Config.mode === "quote" ||
|
||
Config.mode === "zen"
|
||
) {
|
||
TimerProgress.update(TestTimer.time);
|
||
}
|
||
if (
|
||
Config.mode == "time" ||
|
||
Config.mode == "words" ||
|
||
Config.mode == "custom" ||
|
||
Config.mode == "quote"
|
||
) {
|
||
TestLogic.addWord();
|
||
}
|
||
}
|
||
|
||
function isCharCorrect(char, charIndex) {
|
||
if (!correctShiftUsed) return false;
|
||
|
||
if (Config.mode == "zen") {
|
||
return true;
|
||
}
|
||
|
||
const originalChar = TestLogic.words.getCurrent()[charIndex];
|
||
|
||
if (originalChar == char) {
|
||
return true;
|
||
}
|
||
|
||
if (Config.language.split("_")[0] == "russian") {
|
||
if ((char === "е" || char === "e") && originalChar == "ё") {
|
||
return true;
|
||
}
|
||
if (char === "ё" && (originalChar == "е" || originalChar === "e")) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
if (char === "’" && originalChar == "'") {
|
||
return true;
|
||
}
|
||
|
||
if (char === "'" && originalChar == "’") {
|
||
return true;
|
||
}
|
||
|
||
if (
|
||
(char === `’` || char === "'") &&
|
||
(originalChar == `’` || originalChar === "'")
|
||
) {
|
||
return true;
|
||
}
|
||
|
||
if (
|
||
(char === `"` || char === "”" || char == "“" || char === "„") &&
|
||
(originalChar == `"` ||
|
||
originalChar === "”" ||
|
||
originalChar === "“" ||
|
||
originalChar === "„")
|
||
) {
|
||
return true;
|
||
}
|
||
|
||
if (
|
||
(char === "–" || char === "—" || char == "-") &&
|
||
(originalChar == "-" || originalChar === "–" || originalChar === "—")
|
||
) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function handleChar(char, charIndex) {
|
||
if (TestUI.resultCalculating || TestUI.resultVisible) {
|
||
return;
|
||
}
|
||
|
||
if (char === "\n" && Config.funbox === "58008") {
|
||
char = " ";
|
||
}
|
||
|
||
if (char !== "\n" && char !== "\t" && /\s/.test(char)) {
|
||
handleSpace();
|
||
|
||
//insert space for expert and master or strict space,
|
||
//or for stop on error set to word,
|
||
//otherwise dont do anything
|
||
if (
|
||
Config.difficulty !== "normal" ||
|
||
Config.strictSpace ||
|
||
Config.stopOnError === "word"
|
||
) {
|
||
if (dontInsertSpace) {
|
||
dontInsertSpace = false;
|
||
return;
|
||
}
|
||
} else {
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (
|
||
Config.mode !== "zen" &&
|
||
TestLogic.words.getCurrent()[charIndex] !== "\n" &&
|
||
char === "\n"
|
||
) {
|
||
return;
|
||
}
|
||
|
||
//start the test
|
||
if (!TestLogic.active && !TestLogic.startTest()) {
|
||
return;
|
||
}
|
||
|
||
Focus.set(true);
|
||
Caret.stopAnimation();
|
||
|
||
let thisCharCorrect = isCharCorrect(char, charIndex);
|
||
|
||
if (thisCharCorrect && Config.mode !== "zen") {
|
||
char = TestLogic.words.getCurrent().charAt(charIndex);
|
||
}
|
||
|
||
if (!thisCharCorrect && char === "\n") {
|
||
if (TestLogic.input.current === "") return;
|
||
char = " ";
|
||
}
|
||
|
||
if (TestLogic.input.current === "") {
|
||
TestStats.setBurstStart(performance.now());
|
||
}
|
||
|
||
const resultingWord =
|
||
TestLogic.input.current.substring(0, charIndex) +
|
||
char +
|
||
TestLogic.input.current.substring(charIndex + 1);
|
||
|
||
if (!thisCharCorrect && Misc.trailingComposeChars.test(resultingWord)) {
|
||
TestLogic.input.current = resultingWord;
|
||
TestUI.updateWordElement();
|
||
Caret.updatePosition();
|
||
return;
|
||
}
|
||
|
||
MonkeyPower.addPower(thisCharCorrect);
|
||
TestStats.incrementAccuracy(thisCharCorrect);
|
||
|
||
if (!thisCharCorrect) {
|
||
TestStats.incrementKeypressErrors();
|
||
TestStats.pushMissedWord(TestLogic.words.getCurrent());
|
||
}
|
||
|
||
WeakSpot.updateScore(
|
||
Config.mode === "zen" ? char : TestLogic.words.getCurrent()[charIndex],
|
||
thisCharCorrect
|
||
);
|
||
|
||
if (thisCharCorrect) {
|
||
Sound.playClick(Config.playSoundOnClick);
|
||
} else {
|
||
if (!Config.playSoundOnError || Config.blindMode) {
|
||
Sound.playClick(Config.playSoundOnClick);
|
||
} else {
|
||
Sound.playError(Config.playSoundOnError);
|
||
}
|
||
}
|
||
|
||
if (!correctShiftUsed && Config.difficulty != "master") return;
|
||
|
||
//update current corrected version. if its empty then add the current char. if its not then replace the last character with the currently pressed one / add it
|
||
if (TestLogic.corrected.current === "") {
|
||
TestLogic.corrected.current += resultingWord;
|
||
} else {
|
||
if (charIndex >= TestLogic.corrected.current.length) {
|
||
TestLogic.corrected.current += char;
|
||
} else if (!thisCharCorrect) {
|
||
TestLogic.corrected.current =
|
||
TestLogic.corrected.current.substring(0, charIndex) +
|
||
char +
|
||
TestLogic.corrected.current.substring(charIndex + 1);
|
||
}
|
||
}
|
||
|
||
TestStats.incrementKeypressCount();
|
||
TestStats.updateLastKeypress();
|
||
TestStats.pushKeypressWord(TestLogic.words.currentIndex);
|
||
|
||
if (Config.stopOnError == "letter" && !thisCharCorrect) {
|
||
return;
|
||
}
|
||
|
||
Replay.addReplayEvent(
|
||
thisCharCorrect ? "correctLetter" : "incorrectLetter",
|
||
char
|
||
);
|
||
|
||
//update the active word top, but only once
|
||
if (
|
||
TestLogic.input.current.length === 1 &&
|
||
TestLogic.words.currentIndex === 0
|
||
) {
|
||
TestUI.setActiveWordTop(document.querySelector("#words .active").offsetTop);
|
||
}
|
||
|
||
//max length of the input is 20 unless in zen mode then its 30
|
||
if (
|
||
(Config.mode === "zen" && charIndex < 30) ||
|
||
(Config.mode !== "zen" &&
|
||
charIndex < TestLogic.words.getCurrent().length + 20)
|
||
) {
|
||
TestLogic.input.current = resultingWord;
|
||
}
|
||
|
||
if (!thisCharCorrect && Config.difficulty == "master") {
|
||
TestLogic.input.pushHistory();
|
||
TestLogic.corrected.pushHistory();
|
||
TestLogic.fail("difficulty");
|
||
return;
|
||
}
|
||
|
||
//keymap
|
||
if (Config.keymapMode === "react") {
|
||
Keymap.flashKey(char, thisCharCorrect);
|
||
}
|
||
|
||
if (Config.mode != "zen") {
|
||
//not applicable to zen mode
|
||
//auto stop the test if the last word is correct
|
||
let currentWord = TestLogic.words.getCurrent();
|
||
let lastindex = TestLogic.words.currentIndex;
|
||
if (
|
||
(currentWord == TestLogic.input.current ||
|
||
(Config.quickEnd &&
|
||
currentWord.length == TestLogic.input.current.length &&
|
||
Config.stopOnError == "off")) &&
|
||
lastindex == TestLogic.words.length - 1
|
||
) {
|
||
TestLogic.input.pushHistory();
|
||
TestLogic.corrected.pushHistory();
|
||
TestLogic.finish();
|
||
return;
|
||
}
|
||
}
|
||
|
||
let activeWordTopBeforeJump = document.querySelector("#words .word.active")
|
||
.offsetTop;
|
||
TestUI.updateWordElement();
|
||
|
||
if (!Config.hideExtraLetters) {
|
||
let newActiveTop = document.querySelector("#words .word.active").offsetTop;
|
||
//stop the word jump by slicing off the last character, update word again
|
||
if (
|
||
activeWordTopBeforeJump < newActiveTop &&
|
||
!TestUI.lineTransition &&
|
||
TestLogic.input.current.length > 1
|
||
) {
|
||
if (Config.mode == "zen") {
|
||
let currentTop = Math.floor(
|
||
document.querySelectorAll("#words .word")[
|
||
TestUI.currentWordElementIndex - 1
|
||
].offsetTop
|
||
);
|
||
if (!Config.showAllLines) TestUI.lineJump(currentTop);
|
||
} else {
|
||
TestLogic.input.current = TestLogic.input.current.slice(0, -1);
|
||
TestUI.updateWordElement();
|
||
}
|
||
}
|
||
}
|
||
|
||
//simulate space press in nospace funbox
|
||
if (
|
||
(Config.funbox === "nospace" &&
|
||
TestLogic.input.current.length === TestLogic.words.getCurrent().length) ||
|
||
(char === "\n" && thisCharCorrect)
|
||
) {
|
||
handleSpace();
|
||
}
|
||
|
||
if (char !== "\n") {
|
||
Caret.updatePosition();
|
||
}
|
||
}
|
||
|
||
function handleTab(event) {
|
||
if (TestUI.resultCalculating) {
|
||
event.preventDefault();
|
||
}
|
||
if (
|
||
!$("#presetWrapper").hasClass("hidden") ||
|
||
!$("#tagsWrapper").hasClass("hidden")
|
||
) {
|
||
event.preventDefault();
|
||
return;
|
||
}
|
||
if ($("#customTextPopup .textarea").is(":focus")) {
|
||
event.preventDefault();
|
||
|
||
let area = $("#customTextPopup .textarea")[0];
|
||
|
||
var start = area.selectionStart;
|
||
var end = area.selectionEnd;
|
||
|
||
// set textarea value to: text before caret + tab + text after caret
|
||
area.value =
|
||
area.value.substring(0, start) + "\t" + area.value.substring(end);
|
||
|
||
// put caret at right position again
|
||
area.selectionStart = area.selectionEnd = start + 1;
|
||
|
||
return;
|
||
} else if (
|
||
!TestUI.resultCalculating &&
|
||
$("#commandLineWrapper").hasClass("hidden") &&
|
||
$("#simplePopupWrapper").hasClass("hidden") &&
|
||
$("#quoteSubmitPopupWrapper").hasClass("hidden") &&
|
||
!$(".page.pageLogin").hasClass("active")
|
||
) {
|
||
if ($(".pageTest").hasClass("active")) {
|
||
if (Config.quickTab) {
|
||
if (
|
||
TestUI.resultVisible ||
|
||
!(
|
||
(Config.mode == "zen" && !event.shiftKey) ||
|
||
(TestLogic.hasTab && !event.shiftKey)
|
||
)
|
||
) {
|
||
if (event.shiftKey) {
|
||
ManualRestart.set();
|
||
} else {
|
||
ManualRestart.reset();
|
||
}
|
||
event.preventDefault();
|
||
if (
|
||
TestLogic.active &&
|
||
Config.repeatQuotes === "typing" &&
|
||
Config.mode === "quote"
|
||
) {
|
||
TestLogic.restart(true, false, event);
|
||
} else {
|
||
TestLogic.restart(false, false, event);
|
||
}
|
||
} else {
|
||
event.preventDefault();
|
||
handleChar("\t", TestLogic.input.current.length);
|
||
setWordsInput(" " + TestLogic.input.current);
|
||
}
|
||
} else if (!TestUI.resultVisible) {
|
||
if (
|
||
(TestLogic.hasTab && event.shiftKey) ||
|
||
(!TestLogic.hasTab && Config.mode !== "zen") ||
|
||
(Config.mode === "zen" && event.shiftKey)
|
||
) {
|
||
event.preventDefault();
|
||
$("#restartTestButton").focus();
|
||
} else {
|
||
event.preventDefault();
|
||
handleChar("\t", TestLogic.input.current.length);
|
||
setWordsInput(" " + TestLogic.input.current);
|
||
}
|
||
}
|
||
} else if (Config.quickTab) {
|
||
UI.changePage("test");
|
||
}
|
||
}
|
||
}
|
||
|
||
$(document).keydown((event) => {
|
||
if ($(".pageLoading").hasClass("active")) return event.preventDefault();
|
||
|
||
//autofocus
|
||
const wordsFocused = $("#wordsInput").is(":focus");
|
||
const pageTestActive = !$(".pageTest").hasClass("hidden");
|
||
const commandLineVisible = !$("#commandLineWrapper").hasClass("hidden");
|
||
const leaderboardsVisible = !$("#leaderboardsWrapper").hasClass("hidden");
|
||
const modePopupVisible =
|
||
!$("#customTextPopupWrapper").hasClass("hidden") ||
|
||
!$("#customWordAmountPopupWrapper").hasClass("hidden") ||
|
||
!$("#customTestDurationPopupWrapper").hasClass("hidden") ||
|
||
!$("#quoteSearchPopupWrapper").hasClass("hidden") ||
|
||
!$("#quoteSubmitPopupWrapper").hasClass("hidden") ||
|
||
!$("#quoteApprovePopupWrapper").hasClass("hidden") ||
|
||
!$("#wordFilterPopupWrapper").hasClass("hidden");
|
||
|
||
const allowTyping =
|
||
pageTestActive &&
|
||
!commandLineVisible &&
|
||
!leaderboardsVisible &&
|
||
!modePopupVisible &&
|
||
!TestUI.resultVisible &&
|
||
(wordsFocused || event.key !== "Enter");
|
||
|
||
if (allowTyping && !wordsFocused && !$("#restartTestButton").is(":focus")) {
|
||
TestUI.focusWords();
|
||
if (Config.showOutOfFocusWarning) {
|
||
event.preventDefault();
|
||
}
|
||
}
|
||
|
||
//tab
|
||
if (
|
||
(event.key == "Tab" && !Config.swapEscAndTab) ||
|
||
(event.key == "Escape" && Config.swapEscAndTab)
|
||
) {
|
||
handleTab(event);
|
||
}
|
||
|
||
if (!allowTyping) return;
|
||
|
||
if (!event.originalEvent?.isTrusted || TestUI.testRestarting) {
|
||
event.preventDefault();
|
||
return;
|
||
}
|
||
|
||
TestStats.recordKeypressSpacing();
|
||
TestStats.setKeypressDuration(performance.now());
|
||
TestStats.setKeypressNotAfk();
|
||
|
||
//blocking firefox from going back in history with backspace
|
||
if (event.key === "Backspace") {
|
||
Sound.playClick(Config.playSoundOnClick);
|
||
let t = /INPUT|SELECT|TEXTAREA/i;
|
||
if (
|
||
!t.test(event.target.tagName) ||
|
||
event.target.disabled ||
|
||
event.target.readOnly
|
||
) {
|
||
event.preventDefault();
|
||
}
|
||
|
||
if (Config.confidenceMode === "max") {
|
||
event.preventDefault();
|
||
return;
|
||
}
|
||
}
|
||
|
||
Monkey.type();
|
||
|
||
if (event.key === "Backspace" && TestLogic.input.current.length === 0) {
|
||
backspaceToPrevious();
|
||
if (TestLogic.input.current)
|
||
setWordsInput(" " + TestLogic.input.current + " ");
|
||
}
|
||
|
||
if (event.key === "Enter") {
|
||
if (event.shiftKey && Config.mode == "zen") {
|
||
TestLogic.finish();
|
||
} else if (
|
||
event.shiftKey &&
|
||
((Config.mode == "time" && Config.time === 0) ||
|
||
(Config.mode == "words" && Config.words === 0))
|
||
) {
|
||
TestLogic.setBailout(true);
|
||
TestLogic.finish();
|
||
} else {
|
||
handleChar("\n", TestLogic.input.current.length);
|
||
setWordsInput(" " + TestLogic.input.current);
|
||
}
|
||
}
|
||
|
||
//show dead keys
|
||
if (
|
||
event.key === "Dead" &&
|
||
!Misc.trailingComposeChars.test(TestLogic.input.current)
|
||
) {
|
||
Sound.playClick(Config.playSoundOnClick);
|
||
$(
|
||
document.querySelector("#words .word.active").querySelectorAll("letter")[
|
||
TestLogic.input.current.length
|
||
]
|
||
).toggleClass("dead");
|
||
}
|
||
|
||
if (Config.oppositeShiftMode !== "off") {
|
||
correctShiftUsed = ShiftTracker.isUsingOppositeShift(event) !== false;
|
||
}
|
||
|
||
if (
|
||
Config.layout !== "default" &&
|
||
!(
|
||
event.ctrlKey ||
|
||
(event.altKey && window.navigator.platform.search("Linux") > -1)
|
||
)
|
||
) {
|
||
const char = LayoutEmulator.getCharFromEvent(event);
|
||
if (char !== null) {
|
||
event.preventDefault();
|
||
handleChar(char, TestLogic.input.current.length);
|
||
updateUI();
|
||
setWordsInput(" " + TestLogic.input.current);
|
||
}
|
||
}
|
||
});
|
||
|
||
$("#wordsInput").keyup((event) => {
|
||
if (!event.originalEvent?.isTrusted || TestUI.testRestarting) {
|
||
event.preventDefault();
|
||
return;
|
||
}
|
||
|
||
if (TestUI.resultVisible) return;
|
||
let now = performance.now();
|
||
if (TestStats.keypressTimings.duration.current !== -1) {
|
||
let diff = Math.abs(TestStats.keypressTimings.duration.current - now);
|
||
TestStats.pushKeypressDuration(diff);
|
||
}
|
||
TestStats.setKeypressDuration(now);
|
||
Monkey.stop();
|
||
});
|
||
|
||
$("#wordsInput").on("beforeinput", (event) => {
|
||
if (!event.originalEvent?.isTrusted) return;
|
||
if (event.target.value === "") {
|
||
event.target.value = " ";
|
||
}
|
||
});
|
||
|
||
$("#wordsInput").on("input", (event) => {
|
||
if (!event.originalEvent?.isTrusted || TestUI.testRestarting) {
|
||
event.target.value = " ";
|
||
return;
|
||
}
|
||
|
||
TestStats.setKeypressNotAfk();
|
||
|
||
const realInputValue = event.target.value.normalize();
|
||
const inputValue = realInputValue.slice(1);
|
||
|
||
// input will be modified even with the preventDefault() in
|
||
// beforeinput/keydown if it's part of a compose sequence. this undoes
|
||
// the effects of that and takes the input out of compose mode.
|
||
if (
|
||
Config.layout !== "default" &&
|
||
inputValue.length >= TestLogic.input.current.length
|
||
) {
|
||
setWordsInput(" " + TestLogic.input.current);
|
||
return;
|
||
}
|
||
|
||
if (realInputValue.length === 0 && TestLogic.input.current.length === 0) {
|
||
// fallback for when no Backspace keydown event (mobile)
|
||
backspaceToPrevious();
|
||
} else if (inputValue.length < TestLogic.input.current.length) {
|
||
TestLogic.input.current = inputValue;
|
||
TestUI.updateWordElement();
|
||
Caret.updatePosition();
|
||
if (!Misc.trailingComposeChars.test(TestLogic.input.current)) {
|
||
Replay.addReplayEvent("setLetterIndex", TestLogic.input.current.length);
|
||
}
|
||
} else if (inputValue !== TestLogic.input.current) {
|
||
let diffStart = 0;
|
||
while (inputValue[diffStart] === TestLogic.input.current[diffStart])
|
||
diffStart++;
|
||
|
||
for (let i = diffStart; i < inputValue.length; i++) {
|
||
handleChar(inputValue[i], i);
|
||
}
|
||
}
|
||
|
||
setWordsInput(" " + TestLogic.input.current);
|
||
updateUI();
|
||
|
||
// force caret at end of input
|
||
// doing it on next cycle because Chromium on Android won't let me edit
|
||
// the selection inside the input event
|
||
setTimeout(() => {
|
||
if (
|
||
event.target.selectionStart !== event.target.value.length &&
|
||
(!Misc.trailingComposeChars.test(event.target.value) ||
|
||
event.target.selectionStart <
|
||
event.target.value.search(Misc.trailingComposeChars))
|
||
) {
|
||
event.target.selectionStart = event.target.selectionEnd =
|
||
event.target.value.length;
|
||
}
|
||
}, 0);
|
||
});
|
||
|
||
$("#wordsInput").focus((event) => {
|
||
event.target.selectionStart = event.target.selectionEnd =
|
||
event.target.value.length;
|
||
});
|
||
|
||
$("#wordsInput").on("copy paste", (event) => {
|
||
event.preventDefault();
|
||
});
|