Last active
May 9, 2020 02:28
-
-
Save ShanonJackson/eb8851910d673678615ff98698775a13 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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