Skip to content

Instantly share code, notes, and snippets.

@ShanonJackson
Last active May 9, 2020 02:27
Show Gist options
  • Save ShanonJackson/b5b71fb17912fb043f40f0e461510bcd to your computer and use it in GitHub Desktop.
Save ShanonJackson/b5b71fb17912fb043f40f0e461510bcd 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={{ outline: "none", border: "1px solid black", minWidth: "70px" }}
suppressContentEditableWarning={true}
>
{value}
</div>
);
};
export const FirstAttempt = () => {
const [value, setValue] = useState("");
return <ResizingInput value={value} onChange={setValue} />;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment