From 3a7fab530a43853907f6795382b9a28206820536 Mon Sep 17 00:00:00 2001 From: bakk Date: Sat, 29 May 2021 01:12:45 +0200 Subject: [PATCH] kalk_web: Switched out contenteditable div to overlaid textarea --- kalk_web/public/index.html | 7 +- kalk_web/src/KalkCalculator.svelte | 289 +++++++++++++---------------- 2 files changed, 136 insertions(+), 160 deletions(-) diff --git a/kalk_web/public/index.html b/kalk_web/public/index.html index e9c9d71..c711417 100644 --- a/kalk_web/public/index.html +++ b/kalk_web/public/index.html @@ -3,7 +3,7 @@ - + Svelte app @@ -16,6 +16,11 @@ background-color: #212121; } + html, + body { + overflow-x: hidden; + } + .hint { color: #9c9c9c; } diff --git a/kalk_web/src/KalkCalculator.svelte b/kalk_web/src/KalkCalculator.svelte index 1825f09..e1b5f00 100644 --- a/kalk_web/src/KalkCalculator.svelte +++ b/kalk_web/src/KalkCalculator.svelte @@ -43,15 +43,24 @@ let kalkContext: Context; let selectedLineOffset: number = 0; let calculatorElement: HTMLElement; - let inputElement: HTMLInputElement; + let inputElement: HTMLTextAreaElement; + let highlightedTextElement: HTMLElement; + let hasBeenInteractedWith = false; - afterUpdate(() => { - // Scroll to bottom - outputElement.children[ - outputElement.children.length - 1 - ].scrollIntoView(false); - calculatorElement.scrollIntoView(); - }); + function setText(text: string) { + inputElement.value = text; + const highlighted = highlight(text); + setHtml(highlighted); + } + + function setHtml(html: string) { + highlightedTextElement.innerHTML = html; + inputElement.value = highlightedTextElement.textContent; + } + + function getHtml(): string { + return highlightedTextElement.innerHTML; + } function calculate( kalk: Kalk, @@ -68,36 +77,44 @@ } function handleKeyDown(event: KeyboardEvent, kalk: Kalk) { + hasBeenInteractedWith = true; if (event.key == "Enter") { selectedLineOffset = 0; - const target = event.target as HTMLInputElement; - const input = target.textContent; + const input = inputElement.value; let output: string; if (input.trim() == "help") { output = `Link to usage guide`; } else if (input.trim() == "clear") { outputLines = []; - target.innerHTML = ""; + setText(""); return; } else { - const [result, success] = calculate( - kalk, - input.replace(/\s+/g, "") // Temporary fix, since it for some reason complains about spaces on chrome - ); + const [result, success] = calculate(kalk, input); output = success - ? highlight(result)[0] + ? highlight(result) : `${result}`; } outputLines = output - ? [...outputLines, [target.innerHTML, true], [output, false]] - : [...outputLines, [target.innerHTML, true]]; + ? [...outputLines, [getHtml(), true], [output, false]] + : [...outputLines, [getHtml(), true]]; - target.innerHTML = ""; + setText(""); + + let i = 0; + setInterval(() => { + if (i == 60) return; + outputElement.children[ + outputElement.children.length - 1 + ].scrollIntoView(); + + calculatorElement.scrollIntoView(false); + i++; + }, 10); } } @@ -106,12 +123,11 @@ // of the input field. This piece of code will put the cursor at the end, // which therefore will need to be done afterwards, so that it doesn't just get moved back again. if (event.key == "ArrowUp" || event.key == "ArrowDown") { - const target = event.target as HTMLInputElement; const change = event.key == "ArrowUp" ? 1 : -1; selectedLineOffset += change; if (selectedLineOffset < 0) { - target.innerHTML = ""; + setText(""); selectedLineOffset = 0; return; } @@ -126,8 +142,7 @@ } if (line) { - target.innerHTML = line[0]; - setCursorPosEnd(target); + setHtml(line[0]); } if (selectedLineOffset >= outputLines.length) { @@ -138,16 +153,27 @@ function handleInput(event: Event) { const target = event.target as HTMLInputElement; - const cursorPos = getCursorPos(target); - const [highlighted, offset] = highlight(target.textContent); - target.innerHTML = highlighted; - setCursorPos(target, cursorPos - offset); + // Make sure it doesn't mess with the HTML. + target.value = target.value + .replaceAll("\n", "") + .replaceAll(" ", " ") + .replaceAll("&", "") + .replaceAll("<", ""); + setText(target.value); } function handleTouchLine(event: Event) { - if (!inputElement.innerHTML) { + if (!inputElement.value) { const target = event.currentTarget as HTMLElement; - inputElement.innerHTML = target.querySelector(".value").innerHTML; + setHtml(target.innerHTML); + + // Sighs... What else? + let i = 0; + setInterval(() => { + if (i == 40) return; + inputElement.focus({ preventScroll: true }); + i++; + }, 1); } } @@ -158,128 +184,53 @@ } function handleArrowClick(event: Event, left: boolean) { - const target = event.target as HTMLElement; - const cursorPos = getCursorPos(inputElement); - target.blur(); - setCursorPos(inputElement, cursorPos + (left ? -1 : 1)); + const length = inputElement.value.length; + const selection = inputElement.selectionEnd + (left ? -1 : 1); + inputElement.selectionEnd = Math.min(Math.max(selection, 0), length); + inputElement.selectionStart = inputElement.selectionEnd; + inputElement.focus({ preventScroll: true }); } function insertText(input: string) { - inputElement.focus({ preventScroll: true }); - let cursorPos = getCursorPos(inputElement); - const textContent = inputElement.textContent; - let movementOffset = input.length; - + let offset = 0; if (input == "(") { input += ")"; } else if (input == "=") { input = " = "; - movementOffset = 3; } else if (input == "Σ") { input += "()"; - movementOffset = 2; + offset = -1; } else if (input == "∫") { input += "()"; - movementOffset = 2; + offset = -1; } else if (input == "⌊") { input += "⌋"; + offset = -1; } else if (input == "⌈") { input += "⌉"; + offset = -1; } else if (input == ",") { input = ", "; - movementOffset = 2; } - const newString = - textContent.slice(0, cursorPos) + - input + - textContent.slice(cursorPos); - const [highlighted, offset] = highlight(newString); - - inputElement.innerHTML = highlighted; + inputElement.setRangeText( + input, + inputElement.selectionStart, + inputElement.selectionEnd, + "end" + ); + inputElement.selectionEnd += offset; + setText(inputElement.value); inputElement.focus({ preventScroll: true }); - setCursorPos(inputElement, cursorPos - offset + movementOffset); - - // I know this sucks, but it keeps scrolling away on some browsers >:( - let i = 0; - setInterval(() => { - if (i == 60) return; - calculatorElement.scrollIntoView(); - i++; - }, 20); } - function focus(element: HTMLInputElement) { + function handleLoad(element: HTMLElement) { if (autofocus) element.focus(); } - function getCursorPos(element: HTMLInputElement): number { - const shadowRoot = calculatorElement.getRootNode() as ShadowRoot; - const range = shadow.getRange(shadowRoot); - //const selection = shadowRoot.getSelection(); - //const range = selection.getRangeAt(0); - const preCaretRange = range.cloneRange(); - preCaretRange.selectNodeContents(element); - preCaretRange.setEnd(range.endContainer, range.endOffset); - - return preCaretRange.toString().length; - } - - function setCursorPos(element: HTMLElement, indexToSelect: number) { - const range = document.createRange(); - range.selectNodeContents(element); - const textNodes = getTextNodesIn(element); - - let nodeEndPos = 0; - for (const textNode of textNodes) { - const previousNodeEndPos = nodeEndPos; - nodeEndPos += textNode.length; - - // If the index that should be selected is - // less than or equal to the current position (the end of the text node), - // then the index points to somewhere inside the current text node. - // This text node along with indexToSelect will then be used when setting the cursor position. - if (indexToSelect <= nodeEndPos) { - range.setStart(textNode, indexToSelect - previousNodeEndPos); - range.setEnd(textNode, indexToSelect - previousNodeEndPos); - break; - } - } - - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - } - - function setCursorPosEnd(element: HTMLElement) { - const range = document.createRange(); - const selection = window.getSelection(); - range.selectNodeContents(element); - range.setStart(element, range.endOffset); - selection.removeAllRanges(); - selection.addRange(range); - } - - function getTextNodesIn(node: Node): Text[] { - const textNodes: Text[] = []; - - // If it's text node, add it to the list directly, - // otherwise go through it recursively and find text nodes within it. - if (node.nodeType == Node.TEXT_NODE) { - textNodes.push(node as Text); - } else { - for (const child of node.childNodes) { - textNodes.push(...getTextNodesIn(child)); - } - } - - return textNodes; - } - - function highlight(input: string): [string, number] { - if (!input) return ["", 0]; + function highlight(input: string): string { + if (!input) return ""; let result = input; - let offset = 0; result = result.replace( /(?[^!-@\s_|^⌊⌋⌈⌉≈]+(_\d+)?)|(?[+\-/*%^!≈])/g, (substring, identifier, _, op) => { @@ -320,8 +271,6 @@ } } - offset += substring.length - newSubstring.length; - return `${newSubstring}`; } @@ -333,9 +282,7 @@ } ); - if (result.endsWith(" ")) result = result.slice(0, -1) + " "; - - return [result, offset]; + return result; } @@ -343,11 +290,11 @@
{#each outputLines as line} - + {#if line[1]} >> {/if} - + {@html line[0]} @@ -358,22 +305,27 @@ {#await import("@paddim8/kalk")} Loading... {:then kalk} -
handleKeyDown(event, kalk)} - on:keyup={handleKeyUp} - on:input={handleInput} - role="textbox" - /> +
+
+