mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-12-17 21:49:36 +08:00
fix(indicate typos): letters not displaying correctly in RTL languages or with ligatures when set to below (NadAlaba) (#5113)
This commit is contained in:
parent
b0cf7bc4be
commit
c20964d185
5 changed files with 202 additions and 35 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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>");
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue