mirror of
https://github.com/PaddiM8/kalker.git
synced 2024-12-12 17:40:52 +01:00
kalk_web: Switched out contenteditable div to overlaid textarea
This commit is contained in:
parent
a68efe59e7
commit
3a7fab530a
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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) + " ";
|
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}">>></span>
|
<span style="color: {promptcolor}">>></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);
|
||||||
|
Loading…
Reference in New Issue
Block a user