Merge branch 'master' into feature/profile-colors-for-premium-users

This commit is contained in:
Christian Fehmer 2024-03-27 12:35:56 +01:00 committed by GitHub
commit 813dfcfb39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 228 additions and 54 deletions

View file

@ -347,6 +347,7 @@
}
.word {
position: relative;
font-size: 1em;
line-height: 1em;
margin: 0.25em;
@ -440,12 +441,11 @@
.word letter.incorrect {
color: var(--error-color);
position: relative;
}
.word letter.incorrect hint {
.word .hints hint {
position: absolute;
bottom: -1em;
bottom: -1.1em;
color: var(--text-color);
line-height: initial;
font-size: 0.75em;
@ -454,9 +454,9 @@
left: 0;
opacity: 0.5;
text-align: center;
width: 100%;
display: grid;
justify-content: center;
transform: translate(-50%);
}
.word letter.incorrect.extra {

View file

@ -248,7 +248,7 @@ export async function setup(challengeName: string): Promise<boolean> {
UpdateConfig.setDifficulty("normal", true);
} else if (challenge.type === "customText") {
CustomText.setDelimiter(" ");
CustomText.setPopupTextareaState(challenge.parameters[0] as string);
CustomText.setPopupTextareaState(challenge.parameters[0] as string, true);
CustomText.setText((challenge.parameters[0] as string).split(" "));
CustomText.setIsTimeRandom(false);
CustomText.setIsSectionRandom(false);
@ -269,7 +269,7 @@ export async function setup(challengeName: string): Promise<boolean> {
text = text.replace(/[\n\r\t ]/gm, " ");
text = text.replace(/ +/gm, " ");
CustomText.setDelimiter(" ");
CustomText.setPopupTextareaState(text);
CustomText.setPopupTextareaState(text, true);
CustomText.setText(text.split(" "));
CustomText.setIsWordRandom(false);
CustomText.setIsSectionRandom(false);

View file

@ -145,7 +145,7 @@ function backspaceToPrevious(): void {
TestUI.setCurrentWordElementIndex(TestUI.currentWordElementIndex - 1);
TestUI.updateActiveElement(true);
Funbox.toggleScript(TestWords.words.getCurrent());
TestUI.updateWordElement();
void TestUI.updateWordElement();
if (Config.mode === "zen") {
TimerProgress.update();
@ -210,7 +210,7 @@ function handleSpace(): void {
TestInput.incrementAccuracy(isWordCorrect);
if (isWordCorrect) {
if (Config.indicateTypos !== "off" && Config.stopOnError === "letter") {
TestUI.updateWordElement();
void TestUI.updateWordElement();
}
PaceCaret.handleSpace(true, currentWord);
TestInput.input.pushHistory();
@ -260,7 +260,7 @@ function handleSpace(): void {
if (Config.stopOnError === "word") {
dontInsertSpace = false;
Replay.addReplayEvent("incorrectLetter", "_");
TestUI.updateWordElement(true);
void TestUI.updateWordElement(true);
void Caret.updatePosition();
}
return;
@ -561,7 +561,7 @@ function handleChar(
!Config.language.startsWith("korean")
) {
TestInput.input.current = resultingWord;
TestUI.updateWordElement();
void TestUI.updateWordElement();
void Caret.updatePosition();
return;
}
@ -642,7 +642,7 @@ function handleChar(
!thisCharCorrect
) {
if (Config.indicateTypos !== "off") {
TestUI.updateWordElement(undefined, TestInput.input.current + char);
void TestUI.updateWordElement(undefined, TestInput.input.current + char);
}
return;
}
@ -708,7 +708,7 @@ function handleChar(
const activeWordTopBeforeJump = document.querySelector<HTMLElement>(
"#words .word.active"
)?.offsetTop as number;
TestUI.updateWordElement();
void TestUI.updateWordElement();
if (!Config.hideExtraLetters) {
const newActiveTop = document.querySelector<HTMLElement>(
@ -729,7 +729,7 @@ function handleChar(
if (!Config.showAllLines) TestUI.lineJump(currentTop);
} else {
TestInput.input.current = TestInput.input.current.slice(0, -1);
TestUI.updateWordElement();
void TestUI.updateWordElement();
}
}
}
@ -1353,7 +1353,7 @@ $("#wordsInput").on("input", (event) => {
TestInput.input.current = inputValue;
}
TestUI.updateWordElement();
void TestUI.updateWordElement();
void Caret.updatePosition();
if (!CompositionState.getComposing()) {
const keyStroke = event?.originalEvent as InputEvent;
@ -1395,7 +1395,7 @@ $("#wordsInput").on("input", (event) => {
const stateafter = CompositionState.getComposing();
if (statebefore !== stateafter) {
TestUI.updateWordElement();
void TestUI.updateWordElement();
}
// force caret at end of input

View file

@ -142,17 +142,7 @@ function hide(options = {} as HideOptions): void {
options.noAnim ? 0 : 125,
() => {
if (options.resetState) {
const newText = CustomText.text.map((word) => {
if (word.endsWith("|")) {
word = word.slice(0, -1);
}
return word;
});
CustomText.setPopupTextareaState(
// CustomText.text.join(CustomText.delimiter)
newText.join(CustomText.delimiter)
);
CustomText.setPopupTextareaStateToSaved();
}
$(wrapper).addClass("hidden");
@ -236,6 +226,8 @@ function apply(): void {
return;
}
CustomText.setPopupTextareaState(text, true);
text = text.trim();
// text = text.replace(/[\r]/gm, " ");

View file

@ -72,10 +72,14 @@ export async function updatePosition(noAnim = false): Promise<void> {
const inputLen = TestInput.input.current.length;
const currentLetterIndex = inputLen;
const activeWordEl = document?.querySelector("#words .active") as HTMLElement;
//insert temporary character so the caret will work in zen mode
const activeWordEmpty = $("#words .active").children().length === 0;
const activeWordEmpty = activeWordEl?.children.length === 0;
if (activeWordEmpty) {
$("#words .active").append('<letter style="opacity: 0;">_</letter>');
activeWordEl.insertAdjacentHTML(
"beforeend",
'<letter style="opacity: 0;">_</letter>'
);
}
const currentWordNodeList = document
@ -112,13 +116,16 @@ export async function updatePosition(noAnim = false): Promise<void> {
const diff = letterHeight - caret.offsetHeight;
let newTop = letterPosTop + diff / 2;
let newTop = activeWordEl.offsetTop + letterPosTop + diff / 2;
if (Config.caretStyle === "underline") {
newTop = letterPosTop - caret.offsetHeight / 2;
newTop = activeWordEl.offsetTop + letterPosTop - caret.offsetHeight / 2;
}
let newLeft = letterPosLeft - (fullWidthCaret ? 0 : caretWidth / 2);
let newLeft =
activeWordEl.offsetLeft +
letterPosLeft -
(fullWidthCaret ? 0 : caretWidth / 2);
const wordsWrapperWidth =
$(document.querySelector("#wordsWrapper") as HTMLElement).width() ?? 0;
@ -199,7 +206,7 @@ export async function updatePosition(noAnim = false): Promise<void> {
}
}
if (activeWordEmpty) {
$("#words .active").children().remove();
activeWordEl?.replaceChildren();
}
}

View file

@ -16,10 +16,18 @@ export let word = -1;
export let time = -1;
export let section = -1;
export let delimiter = " ";
export let popupTextareaState = "The quick brown fox jumps over the lazy dog";
export let popupTextareaState = text.join(" ");
export let savedPopupTextareaState = popupTextareaState;
export function setPopupTextareaState(value: string): void {
export function setPopupTextareaStateToSaved(): void {
popupTextareaState = savedPopupTextareaState;
}
export function setPopupTextareaState(value: string, save = false): void {
popupTextareaState = value;
if (save) {
savedPopupTextareaState = value;
}
}
export function setText(txt: string[]): void {

View file

@ -31,7 +31,7 @@ export function setLastTestWpm(wpm: number): void {
}
}
function resetCaretPosition(): void {
async function resetCaretPosition(): Promise<void> {
if (Config.paceCaret === "off" && !TestState.isPaceRepeat) return;
if (!$("#paceCaret").hasClass("hidden")) {
$("#paceCaret").addClass("hidden");
@ -47,10 +47,15 @@ function resetCaretPosition(): void {
if (firstLetter === undefined || firstLetterHeight === undefined) return;
const currentLanguage = await Misc.getCurrentLanguage(Config.language);
const isLanguageRightToLeft = currentLanguage.rightToLeft;
caret.stop(true, true).animate(
{
top: firstLetter.offsetTop - firstLetterHeight / 4,
left: firstLetter.offsetLeft,
left:
firstLetter.offsetLeft +
(isLanguageRightToLeft ? firstLetter.offsetWidth : 0),
},
0,
"linear"
@ -121,10 +126,10 @@ export async function init(): Promise<void> {
wordsStatus: {},
timeout: null,
};
resetCaretPosition();
await resetCaretPosition();
}
export function update(expectedStepEnd: number): void {
export async function update(expectedStepEnd: number): Promise<void> {
if (settings === null || !TestState.isActive || TestUI.resultVisible) {
return;
}
@ -210,15 +215,26 @@ export function update(expectedStepEnd: number): void {
);
}
const currentLanguage = await Misc.getCurrentLanguage(Config.language);
const isLanguageRightToLeft = currentLanguage.rightToLeft;
newTop =
word.offsetTop +
currentLetter.offsetTop -
Config.fontSize * Misc.convertRemToPixels(1) * 0.1;
newLeft;
if (settings.currentLetterIndex === -1) {
newLeft = currentLetter.offsetLeft;
newLeft =
word.offsetLeft +
currentLetter.offsetLeft -
caretWidth / 2 +
(isLanguageRightToLeft ? currentLetterWidth : 0);
} else {
newLeft =
currentLetter.offsetLeft + currentLetterWidth - caretWidth / 2;
word.offsetLeft +
currentLetter.offsetLeft -
caretWidth / 2 +
(isLanguageRightToLeft ? 0 : currentLetterWidth);
}
caret.removeClass("hidden");
} catch (e) {
@ -254,11 +270,9 @@ export function update(expectedStepEnd: number): void {
}
}
settings.timeout = setTimeout(() => {
try {
update(expectedStepEnd + (settings?.spc ?? 0) * 1000);
} catch (e) {
update(expectedStepEnd + (settings?.spc ?? 0) * 1000).catch(() => {
settings = null;
}
});
}, duration);
} catch (e) {
console.error(e);
@ -296,7 +310,7 @@ export function handleSpace(correct: boolean, currentWord: string): void {
}
export function start(): void {
update(performance.now() + (settings?.spc ?? 0) * 1000);
void update(performance.now() + (settings?.spc ?? 0) * 1000);
}
ConfigEvent.subscribe((eventKey) => {

View file

@ -116,7 +116,10 @@ export function init(missed: boolean, slow: boolean): boolean {
}
UpdateConfig.setMode("custom", true);
CustomText.setPopupTextareaState(newCustomText.join(CustomText.delimiter));
CustomText.setPopupTextareaState(
newCustomText.join(CustomText.delimiter),
true
);
CustomText.setText(newCustomText);
CustomText.setIsWordRandom(true);
CustomText.setIsTimeRandom(false);

View file

@ -1064,13 +1064,16 @@ export async function finish(difficultyFailed = false): Promise<void> {
let newText = CustomText.getCustomText(customTextName, true);
newText = newText.slice(newProgress);
CustomText.setPopupTextareaState(newText.join(CustomText.delimiter));
CustomText.setPopupTextareaState(
newText.join(CustomText.delimiter),
true
);
CustomText.setText(newText);
} else {
// They finished the test
CustomText.setCustomTextLongProgress(customTextName, 0);
const text = CustomText.getCustomText(customTextName, true);
CustomText.setPopupTextareaState(text.join(CustomText.delimiter));
CustomText.setPopupTextareaState(text.join(CustomText.delimiter), true);
CustomText.setText(text);
Notifications.add("Long custom text completed", 1, {
duration: 5,

View file

@ -29,6 +29,81 @@ async function gethtml2canvas(): Promise<typeof import("html2canvas").default> {
return (await import("html2canvas")).default;
}
function createHintsHtml(
incorrectLtrIndices: number[][],
activeWordLetters: NodeListOf<Element>
): string {
let hintsHtml = "";
for (const adjacentLetters of incorrectLtrIndices) {
for (const indx of adjacentLetters) {
const blockLeft = (activeWordLetters[indx] as HTMLElement).offsetLeft;
const blockWidth = (activeWordLetters[indx] as HTMLElement).offsetWidth;
const blockIndices = `[${indx}]`;
const blockChars = TestInput.input.current[indx];
hintsHtml +=
`<hint data-length=1 data-chars-index=${blockIndices}` +
` style="left: ${blockLeft + blockWidth / 2}px;">${blockChars}</hint>`;
}
}
hintsHtml = `<div class="hints">${hintsHtml}</div>`;
return hintsHtml;
}
async function joinOverlappingHints(
incorrectLtrIndices: number[][],
activeWordLetters: NodeListOf<Element>,
hintElements: HTMLCollection
): Promise<void> {
const currentLanguage = await Misc.getCurrentLanguage(Config.language);
const isLanguageRTL = currentLanguage.rightToLeft;
let i = 0;
for (const adjacentLetters of incorrectLtrIndices) {
for (let j = 0; j < adjacentLetters.length - 1; j++) {
const block1El = hintElements[i] as HTMLElement;
const block2El = hintElements[i + 1] as HTMLElement;
const leftBlock = isLanguageRTL ? block2El : block1El;
const rightBlock = isLanguageRTL ? block1El : block2El;
/** HintBlock.offsetLeft is at the center line of corresponding letters
* then "transform: translate(-50%)" aligns hints with letters */
if (
leftBlock.offsetLeft + leftBlock.offsetWidth / 2 >
rightBlock.offsetLeft - rightBlock.offsetWidth / 2
) {
block1El.dataset["length"] = (
parseInt(block1El.dataset["length"] ?? "1") +
parseInt(block2El.dataset["length"] ?? "1")
).toString();
const block1Indices = block1El.dataset["charsIndex"] ?? "[]";
const block2Indices = block2El.dataset["charsIndex"] ?? "[]";
block1El.dataset["charsIndex"] =
block1Indices.slice(0, -1) + "," + block2Indices.slice(1);
const letter1Index = adjacentLetters[j] ?? 0;
const newLeft =
(activeWordLetters[letter1Index] as HTMLElement).offsetLeft +
(isLanguageRTL
? (activeWordLetters[letter1Index] as HTMLElement).offsetWidth
: 0) +
(block2El.offsetLeft - block1El.offsetLeft);
block1El.style.left = newLeft.toString() + "px";
block1El.insertAdjacentHTML("beforeend", block2El.innerHTML);
block2El.remove();
adjacentLetters.splice(j + 1, 1);
i -= j === 0 ? 1 : 2;
j -= j === 0 ? 1 : 2;
}
i++;
}
i++;
}
}
const debouncedZipfCheck = debounce(250, async () => {
const supports = await Misc.checkIfLanguageSupportsZipf(Config.language);
if (supports === "no") {
@ -67,6 +142,10 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
updateWordsHeight(true);
updateWordsInputPosition(true);
}
if (eventKey === "fontSize" || eventKey === "fontFamily")
updateHintsPosition().catch((e) => {
console.error(e);
});
if (eventKey === "theme") void applyBurstHeatmap();
@ -79,7 +158,7 @@ ConfigEvent.subscribe((eventKey, eventValue, nosave) => {
if (typeof eventValue !== "boolean") return;
if (eventKey === "flipTestColors") flipColors(eventValue);
if (eventKey === "colorfulMode") colorful(eventValue);
if (eventKey === "highlightMode") updateWordElement(eventValue);
if (eventKey === "highlightMode") void updateWordElement(eventValue);
if (eventKey === "burstHeatmap") void applyBurstHeatmap();
});
@ -168,6 +247,53 @@ export function updateActiveElement(
}
}
async function updateHintsPosition(): Promise<void> {
if (
ActivePage.get() !== "test" ||
resultVisible ||
Config.indicateTypos !== "below"
)
return;
const currentLanguage = await Misc.getCurrentLanguage(Config.language);
const isLanguageRTL = currentLanguage.rightToLeft;
let wordEl: HTMLElement | undefined;
let letterElements: NodeListOf<Element> | undefined;
const hintElements = document
.getElementById("words")
?.querySelectorAll("div.word > div.hints > hint");
for (let i = 0; i < (hintElements?.length ?? 0); i++) {
const hintEl = hintElements?.[i] as HTMLElement;
if (!wordEl || hintEl.parentElement?.parentElement !== wordEl) {
wordEl = hintEl.parentElement?.parentElement as HTMLElement;
letterElements = wordEl?.querySelectorAll("letter");
}
const letterIndices = hintEl.dataset["charsIndex"]
?.slice(1, -1)
.split(",")
.map((indx) => parseInt(indx));
const leftmostIndx = isLanguageRTL
? parseInt(hintEl.dataset["length"] ?? "1") - 1
: 0;
let newLeft = (
letterElements?.[letterIndices?.[leftmostIndx] ?? 0] as HTMLElement
).offsetLeft;
const lettersWidth =
letterIndices?.reduce(
(accum, curr) =>
accum + (letterElements?.[curr] as HTMLElement).offsetWidth,
0
) ?? 0;
newLeft += lettersWidth / 2;
hintEl.style.left = newLeft.toString() + "px";
}
}
function getWordHTML(word: string): string {
let newlineafter = false;
let retval = `<div class='word'>`;
@ -570,15 +696,18 @@ export async function screenshot(): Promise<void> {
}, 3000);
}
export function updateWordElement(
export async function updateWordElement(
showError = !Config.blindMode,
inputOverride?: string
): void {
): Promise<void> {
const input = inputOverride ?? TestInput.input.current;
const wordAtIndex = document.querySelector("#words .word.active") as Element;
const wordAtIndex = document.querySelector(
"#words .word.active"
) as HTMLElement;
const currentWord = TestWords.words.getCurrent();
if (!currentWord && Config.mode !== "zen") return;
let ret = "";
const hintIndices: number[][] = [];
let newlineafter = false;
@ -711,8 +840,15 @@ export function updateWordElement(
? "_"
: input[i]
: currentLetter) +
(Config.indicateTypos === "below" ? `<hint>${input[i]}</hint>` : "") +
"</letter>";
if (Config.indicateTypos === "below") {
if (!hintIndices?.length) hintIndices.push([i]);
else {
const lastblock = hintIndices[hintIndices.length - 1];
if (lastblock?.[lastblock.length - 1] === i - 1) lastblock.push(i);
else hintIndices.push([i]);
}
}
}
}
@ -741,7 +877,17 @@ export function updateWordElement(
}
}
}
wordAtIndex.innerHTML = ret;
if (hintIndices?.length) {
const activeWordLetters = wordAtIndex.querySelectorAll("letter");
const hintsHtml = createHintsHtml(hintIndices, activeWordLetters);
wordAtIndex.insertAdjacentHTML("beforeend", hintsHtml);
const hintElements = wordAtIndex.getElementsByTagName("hint");
await joinOverlappingHints(hintIndices, activeWordLetters, hintElements);
}
if (newlineafter) $("#words").append("<div class='newline'></div>");
}

View file

@ -147,7 +147,8 @@ export function loadTestSettingsFromUrl(getOverride?: string): void {
if (de[2] !== null) {
const customTextSettings = de[2];
CustomText.setPopupTextareaState(
customTextSettings.text.join(customTextSettings.delimiter)
customTextSettings.text.join(customTextSettings.delimiter),
true
);
CustomText.setText(customTextSettings.text);
CustomText.setIsTimeRandom(customTextSettings.isTimeRandom);