Skip to content

Instantly share code, notes, and snippets.

@bigmistqke
Created September 29, 2024 10:19
Show Gist options
  • Select an option

  • Save bigmistqke/74bc18cc3853aa1355ed43df0526ed20 to your computer and use it in GitHub Desktop.

Select an option

Save bigmistqke/74bc18cc3853aa1355ed43df0526ed20 to your computer and use it in GitHub Desktop.
import { render } from "solid-js/web";
function format(source: string) {
if (source.endsWith("\n")) {
return source + "\n";
}
return source;
}
function unformat(source: string) {
if (source.endsWith("\n")) {
return source.slice(0, -1);
}
return source;
}
function escape(str: string) {
return str.replace(/[&<>"']/g, (char) => {
switch (char) {
case "&":
return "&amp;";
case "<":
return "&lt;";
case ">":
return "&gt;";
case '"':
return "&quot;";
case "'":
return "&#039;";
default:
return char;
}
});
}
function select(node: ChildNode, start: number, end?: number) {
if (!(node instanceof Node)) {
console.error("node is not an instance of Node", node);
return;
}
const selection = document.getSelection()!;
const range = document.createRange();
selection.removeAllRanges();
selection.addRange(range);
range.setStart(node, start);
if (end) {
range.setEnd(node, end);
}else{
range.setEnd(node, start);
}
}
function getSelectionOffsets(element: HTMLElement) {
const selection = document.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
// Create a range that spans from the start of the contenteditable to the selection start
const preSelectionRange = document.createRange();
preSelectionRange.selectNodeContents(element);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
// The length of the preSelectionRange gives the start offset relative to the whole content
const start = preSelectionRange.toString().length;
const end = start + range.toString().length;
return [start, end];
}
return [0, 0];
}
function createPatch(e: InputEvent & {currentTarget: HTMLElement}) {
const [start, end] = getSelectionOffsets(e.currentTarget)
switch (e.inputType) {
case "insertText": {
return [[start, end], e.data || ""] as const;
}
case "insertParagraph": {
return [[start, end], "\n"] as const;
}
case "deleteContentBackward": {
const offset = start === end ? Math.max(0, start - 1) : start;
return [[offset, end], ""] as const;
}
default:
throw `Unsupported inputType: ${e.inputType}`;
}
}
function ContentEditable(props: { value: string }) {
return (
<pre
contenteditable
onBeforeInput={(e) => {
e.preventDefault();
console.log(e.inputType);
const element = e.currentTarget;
const text = unformat(e.currentTarget.innerText);
const [[start, end], data] = createPatch(e);
element.innerHTML = escape(
format(`${text.slice(0, start)}${data}${text.slice(end)}`),
);
select(element.childNodes[0], start + data.length);
}}
>
{props.value}
</pre>
);
}
render(() => <ContentEditable value="hallo" />, document.getElementById("app")!);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment