Skip to content

Instantly share code, notes, and snippets.

@ShanonJackson
Last active May 9, 2020 02:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ShanonJackson/eb8851910d673678615ff98698775a13 to your computer and use it in GitHub Desktop.
Save ShanonJackson/eb8851910d673678615ff98698775a13 to your computer and use it in GitHub Desktop.
import * as React from "react";
import { useEffect, useLayoutEffect, useState } from "react";
const getCursorPos = (node: Node | null, offset: number, text: string) => {
if (node?.nodeType === Node.TEXT_NODE) return offset;
return offset === 0 ? 0 : text.length;
};
const getSelection = (text: string) => {
const domSelection = window.getSelection();
if (!domSelection) return { start: 0, end: 0 };
const focusPos = getCursorPos(domSelection.focusNode, domSelection.focusOffset, text);
const anchorPos = getCursorPos(domSelection.anchorNode, domSelection.anchorOffset, text);
const start = Math.min(focusPos, anchorPos);
const end = Math.max(focusPos, anchorPos);
return { start, end };
};
const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max);
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
const ResizingInput = ({ value, onChange }: { value: string; onChange: (newStr: string) => void }) => {
const [cursorPosition, setCursorPosition] = useState<number | undefined>(undefined);
const insertString = (data: string, { start, end }: { start: number; end: number } = getSelection(value)) => {
onChange(value.slice(0, start) + data + value.slice(end));
setCursorPosition(start + data.length);
};
const onBeforeInput = (e: React.FormEvent<HTMLDivElement> & { data: string }) => {
e.preventDefault();
insertString(e.data);
};
useIsomorphicLayoutEffect(() => {
if (cursorPosition === undefined) return;
const sel = window.getSelection();
if (!sel?.focusNode) return;
const range = document.createRange();
// Use firstChild if available because on first click focusNode will be the div rather than textNode on empty element
range.setStart(sel.focusNode.firstChild || sel.focusNode, clamp(cursorPosition, 0, value.length));
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
});
const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (e) => {
const Keycodes = { BACKSPACE: 8, ESCAPE: 27, INSERT: 45, DELETE: 46 };
if (e.keyCode === Keycodes.BACKSPACE || e.keyCode === Keycodes.DELETE) {
e.preventDefault();
const { start, end } = getSelection(value);
if (start !== end) return insertString("");
if (e.keyCode === Keycodes.BACKSPACE) {
if (start === 0) return;
return insertString("", { start: start - 1, end: start });
}
return insertString("", { start: start, end: start + 1 });
}
/* Don't handle these for purposes of demo */
if ([Keycodes.ESCAPE, Keycodes.INSERT].includes(e.keyCode)) e.preventDefault();
};
return (
<div
contentEditable={true}
onBeforeInput={onBeforeInput}
onKeyDown={onKeyDown}
style={{ display: "inline-block", outline: "none", border: "1px solid black", minWidth: "70px", whiteSpace: "pre-wrap" }}
suppressContentEditableWarning={true}
>
{value}
</div>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment