kalk_web: Switched out contenteditable div to overlaid textarea

This commit is contained in:
bakk 2021-05-29 01:12:45 +02:00
parent a68efe59e7
commit 3a7fab530a
2 changed files with 136 additions and 160 deletions

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Svelte app</title> <title>Svelte app</title>
@ -16,6 +16,11 @@
background-color: #212121; background-color: #212121;
} }
html,
body {
overflow-x: hidden;
}
.hint { .hint {
color: #9c9c9c; color: #9c9c9c;
} }

View File

@ -43,15 +43,24 @@
let kalkContext: Context; let kalkContext: Context;
let selectedLineOffset: number = 0; let selectedLineOffset: number = 0;
let calculatorElement: HTMLElement; let calculatorElement: HTMLElement;
let inputElement: HTMLInputElement; let inputElement: HTMLTextAreaElement;
let highlightedTextElement: HTMLElement;
let hasBeenInteractedWith = false;
afterUpdate(() => { function setText(text: string) {
// Scroll to bottom inputElement.value = text;
outputElement.children[ const highlighted = highlight(text);
outputElement.children.length - 1 setHtml(highlighted);
].scrollIntoView(false); }
calculatorElement.scrollIntoView();
}); function setHtml(html: string) {
highlightedTextElement.innerHTML = html;
inputElement.value = highlightedTextElement.textContent;
}
function getHtml(): string {
return highlightedTextElement.innerHTML;
}
function calculate( function calculate(
kalk: Kalk, kalk: Kalk,
@ -68,36 +77,44 @@
} }
function handleKeyDown(event: KeyboardEvent, kalk: Kalk) { function handleKeyDown(event: KeyboardEvent, kalk: Kalk) {
hasBeenInteractedWith = true;
if (event.key == "Enter") { if (event.key == "Enter") {
selectedLineOffset = 0; selectedLineOffset = 0;
const target = event.target as HTMLInputElement; const input = inputElement.value;
const input = target.textContent;
let output: string; let output: string;
if (input.trim() == "help") { if (input.trim() == "help") {
output = `<a style="color: ${linkcolor}" output = `<a style="color: ${linkcolor}"
href="https://kalk.netlify.app/#usage" href="https://kalk.strct.net/#usage"
target="blank">Link to usage guide</a>`; target="blank">Link to usage guide</a>`;
} else if (input.trim() == "clear") { } else if (input.trim() == "clear") {
outputLines = []; outputLines = [];
target.innerHTML = ""; setText("");
return; return;
} else { } else {
const [result, success] = calculate( const [result, success] = calculate(kalk, input);
kalk,
input.replace(/\s+/g, "") // Temporary fix, since it for some reason complains about spaces on chrome
);
output = success output = success
? highlight(result)[0] ? highlight(result)
: `<span style="color: ${errorcolor}">${result}</span>`; : `<span style="color: ${errorcolor}">${result}</span>`;
} }
outputLines = output outputLines = output
? [...outputLines, [target.innerHTML, true], [output, false]] ? [...outputLines, [getHtml(), true], [output, false]]
: [...outputLines, [target.innerHTML, true]]; : [...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, // 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. // 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") { if (event.key == "ArrowUp" || event.key == "ArrowDown") {
const target = event.target as HTMLInputElement;
const change = event.key == "ArrowUp" ? 1 : -1; const change = event.key == "ArrowUp" ? 1 : -1;
selectedLineOffset += change; selectedLineOffset += change;
if (selectedLineOffset < 0) { if (selectedLineOffset < 0) {
target.innerHTML = ""; setText("");
selectedLineOffset = 0; selectedLineOffset = 0;
return; return;
} }
@ -126,8 +142,7 @@
} }
if (line) { if (line) {
target.innerHTML = line[0]; setHtml(line[0]);
setCursorPosEnd(target);
} }
if (selectedLineOffset >= outputLines.length) { if (selectedLineOffset >= outputLines.length) {
@ -138,16 +153,27 @@
function handleInput(event: Event) { function handleInput(event: Event) {
const target = event.target as HTMLInputElement; const target = event.target as HTMLInputElement;
const cursorPos = getCursorPos(target); // Make sure it doesn't mess with the HTML.
const [highlighted, offset] = highlight(target.textContent); target.value = target.value
target.innerHTML = highlighted; .replaceAll("\n", "")
setCursorPos(target, cursorPos - offset); .replaceAll(" ", " ")
.replaceAll("&", "")
.replaceAll("<", "");
setText(target.value);
} }
function handleTouchLine(event: Event) { function handleTouchLine(event: Event) {
if (!inputElement.innerHTML) { if (!inputElement.value) {
const target = event.currentTarget as HTMLElement; 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) { function handleArrowClick(event: Event, left: boolean) {
const target = event.target as HTMLElement; const length = inputElement.value.length;
const cursorPos = getCursorPos(inputElement); const selection = inputElement.selectionEnd + (left ? -1 : 1);
target.blur(); inputElement.selectionEnd = Math.min(Math.max(selection, 0), length);
setCursorPos(inputElement, cursorPos + (left ? -1 : 1)); inputElement.selectionStart = inputElement.selectionEnd;
inputElement.focus({ preventScroll: true });
} }
function insertText(input: string) { function insertText(input: string) {
inputElement.focus({ preventScroll: true }); let offset = 0;
let cursorPos = getCursorPos(inputElement);
const textContent = inputElement.textContent;
let movementOffset = input.length;
if (input == "(") { if (input == "(") {
input += ")"; input += ")";
} else if (input == "=") { } else if (input == "=") {
input = " = "; input = " = ";
movementOffset = 3;
} else if (input == "Σ") { } else if (input == "Σ") {
input += "()"; input += "()";
movementOffset = 2; offset = -1;
} else if (input == "∫") { } else if (input == "∫") {
input += "()"; input += "()";
movementOffset = 2; offset = -1;
} else if (input == "⌊") { } else if (input == "⌊") {
input += "⌋"; input += "⌋";
offset = -1;
} else if (input == "⌈") { } else if (input == "⌈") {
input += "⌉"; input += "⌉";
offset = -1;
} else if (input == ",") { } else if (input == ",") {
input = ", "; input = ", ";
movementOffset = 2;
} }
const newString = inputElement.setRangeText(
textContent.slice(0, cursorPos) + input,
input + inputElement.selectionStart,
textContent.slice(cursorPos); inputElement.selectionEnd,
const [highlighted, offset] = highlight(newString); "end"
);
inputElement.innerHTML = highlighted; inputElement.selectionEnd += offset;
setText(inputElement.value);
inputElement.focus({ preventScroll: true }); 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(); if (autofocus) element.focus();
} }
function getCursorPos(element: HTMLInputElement): number { function highlight(input: string): string {
const shadowRoot = calculatorElement.getRootNode() as ShadowRoot; if (!input) return "";
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];
let result = input; let result = input;
let offset = 0;
result = result.replace( result = result.replace(
/(?<identifier>[^!-@\s_|^⌊⌋⌈⌉≈]+(_\d+)?)|(?<op>[+\-/*%^!≈])/g, /(?<identifier>[^!-@\s_|^⌊⌋⌈⌉≈]+(_\d+)?)|(?<op>[+\-/*%^!≈])/g,
(substring, identifier, _, op) => { (substring, identifier, _, op) => {
@ -320,8 +271,6 @@
} }
} }
offset += substring.length - newSubstring.length;
return `<span style="color: ${identifiercolor}">${newSubstring}</span>`; return `<span style="color: ${identifiercolor}">${newSubstring}</span>`;
} }
@ -333,9 +282,7 @@
} }
); );
if (result.endsWith(" ")) result = result.slice(0, -1) + "&nbsp"; return result;
return [result, offset];
} }
</script> </script>
@ -343,11 +290,11 @@
<section class="output" bind:this={outputElement}> <section class="output" bind:this={outputElement}>
<slot /> <slot />
{#each outputLines as line} {#each outputLines as line}
<console-line byuser={line[1]} on:touchstart={handleTouchLine}> <console-line byuser={line[1]}>
{#if line[1]} {#if line[1]}
<span style="color: {promptcolor}">&gt;&gt;</span> <span style="color: {promptcolor}">&gt;&gt;</span>
{/if} {/if}
<span class="value"> <span class="value" on:touchstart={handleTouchLine}>
{@html line[0]} {@html line[0]}
</span> </span>
</console-line> </console-line>
@ -358,22 +305,27 @@
{#await import("@paddim8/kalk")} {#await import("@paddim8/kalk")}
<span>Loading...</span> <span>Loading...</span>
{:then kalk} {:then kalk}
<div <div class="input-field-wrapper">
type="text" <div
contenteditable="true" class="highlighted-text"
class="input" aria-hidden
placeholder={hinttext} bind:this={highlightedTextElement}
autocomplete="off" />
autocorrect="off" <textarea
autocapitalize="off" class="input"
spellcheck="false" placeholder={hinttext}
use:focus autocomplete="off"
bind:this={inputElement} autocorrect="off"
on:keydown={(event) => handleKeyDown(event, kalk)} autocapitalize="off"
on:keyup={handleKeyUp} spellcheck="false"
on:input={handleInput} use:handleLoad
role="textbox" bind:this={inputElement}
/> on:keydown={(event) => handleKeyDown(event, kalk)}
on:keyup={handleKeyUp}
on:input={handleInput}
role="textbox"
/>
</div>
{:catch error} {:catch error}
<span style="color: {errorcolor}">{error}</span> <span style="color: {errorcolor}">{error}</span>
{/await} {/await}
@ -406,6 +358,7 @@
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
padding-bottom: 3.5em;
box-sizing: border-box; box-sizing: border-box;
background-color: inherit; background-color: inherit;
color: inherit; color: inherit;
@ -427,37 +380,55 @@
} }
.input-area { .input-area {
display: flex;
background-color: inherit; background-color: inherit;
display: flex; display: flex;
padding-left: 10px; padding-left: 10px;
font-size: 1.4em; font-size: 1.4em;
padding-bottom: 10px; padding-bottom: 10px;
box-sizing: border-box;
} }
.prompt, .prompt {
.input {
background-color: inherit; background-color: inherit;
} }
.input { .input-field-wrapper {
position: relative;
width: 100%;
}
.highlighted-text {
display: inline-block; display: inline-block;
width: 100%; width: 100%;
color: white; color: white;
word-wrap: anywhere; word-wrap: anywhere;
z-index: 1;
}
.input {
display: inline-block;
position: absolute;
top: 0;
left: 0;
width: 100%;
border: 0;
font-size: inherit;
font-family: inherit;
cursor: text; cursor: text;
color: transparent;
background: transparent;
caret-color: white;
resize: none;
z-index: 2;
word-wrap: anywhere;
&:focus { &:focus {
outline: none; outline: none;
} }
} }
[contenteditable][placeholder]:empty:before {
content: attr(placeholder);
position: absolute;
color: gray;
background-color: transparent;
}
.button-panel { .button-panel {
display: grid; display: grid;
grid-template-columns: repeat(10, auto); grid-template-columns: repeat(10, auto);