Created
August 10, 2019 13:09
-
-
Save zaydek-old/a2458331475feb5d6c3a584b015e5703 to your computer and use it in GitHub Desktop.
This file contains hidden or 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 React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react" | |
import PropTypes from "prop-types" | |
import useMethods from "use-methods" | |
import { findNode, findPos, readRoot } from "./traverseDOM" | |
import { Title } from "./Title" | |
const contentEditable = { | |
contentEditable: true, | |
suppressContentEditableWarning: true, | |
spellCheck: false | |
} | |
const initialState = { | |
value: "", | |
pos1: 0, | |
pos2: 0, | |
selection: "" | |
} | |
const methods = state => ({ | |
reset(newValue) { | |
let { value, pos1, pos2, selection } = state | |
value = newValue | |
pos1 = state.value.length | |
pos2 = state.pos1 | |
selection = "" | |
return { value, pos1, pos2, selection } | |
}, | |
insert(newValue) { | |
let { value, pos1, pos2, selection } = state | |
value = value.slice(0, pos1) + newValue + value.slice(pos2) | |
pos1 = pos1 + newValue.length | |
pos2 = pos1 | |
selection = "" | |
return { value, pos1, pos2, selection } | |
}, | |
trimLeft(n) { | |
let { value, pos1, pos2, selection } = state | |
value = value.slice(0, pos1 + n) + value.slice(pos2) | |
pos1 = pos1 + n | |
pos2 = pos1 | |
selection = "" | |
return { value, pos1, pos2, selection } | |
}, | |
setPos(newPos1, newPos2 = newPos1) { | |
let { value, pos1, pos2, selection } = state | |
pos1 = Math.min(newPos1, newPos2) // Get the minimum of `pos1` and `pos2`. | |
pos2 = Math.max(newPos1, newPos2) // Get the maximum of `pos1` and `pos2`. | |
selection = value.slice(pos1, pos2) | |
return { value, pos1, pos2, selection } | |
} | |
}) | |
// Based on stackoverflow.com/a/53837442. | |
function useForceUpdate() { | |
const [update, setUpdate] = useState(false) | |
const forceUpdate = () => { | |
setUpdate(!update) | |
} | |
return [update, forceUpdate] | |
} | |
const DEBUG = true | |
function NewNote(props) { | |
const ref = useRef(null) | |
const [update, forceUpdate] = useForceUpdate() | |
const [ | |
{ value, pos1, pos2, selection }, | |
{ reset, insert, trimLeft, setPos } | |
] = useMethods(methods, initialState) | |
useEffect(() => { | |
const h = e => { | |
if (document.activeElement !== ref.current) { | |
return | |
} | |
const selection = document.getSelection() | |
const pos1 = findPos(ref.current, selection.anchorNode, selection.anchorOffset) | |
const pos2 = findPos(ref.current, selection.focusNode , selection.focusOffset ) | |
setPos(pos1, pos2) | |
} | |
document.addEventListener("selectionchange", h) | |
return () => { | |
document.removeEventListener("selectionchange", h) | |
} | |
}, []) | |
useLayoutEffect(() => { | |
DEBUG && console.log("useLayoutEffect") | |
const selection = document.getSelection() | |
const node1 = findNode(ref.current, pos1) | |
const node2 = findNode(ref.current, pos2) | |
const range = document.createRange() | |
range.setStart(node1.node, node1.offset) | |
range.setEnd (node2.node, node2.offset) | |
selection.removeAllRanges() | |
selection.addRange(range) | |
if (pos1 === pos2 && pos1 === value.length) { | |
window.scrollTo(0, ref.current.scrollHeight) | |
} | |
}, [value, update]) | |
const handleKeyDown = e => { | |
switch (e.key) { | |
case "Enter": | |
e.preventDefault() | |
insert("\n") | |
return | |
case "Backspace": | |
if (!value) { | |
e.preventDefault() | |
reset("") | |
// `forceUpdate` is needed because `reset("")` is | |
// idempotent. | |
forceUpdate() | |
} else if (value[pos1 - 1] === "\n") { | |
e.preventDefault() | |
trimLeft(-1) | |
} else if (selection.length) { | |
e.preventDefault() | |
insert("") | |
} | |
return | |
default: | |
return | |
} | |
} | |
const handleInput = e => { | |
if (e.nativeEvent.inputType === "insertCompositionText") { | |
return | |
} | |
const newValue = readRoot(ref.current) | |
reset(newValue) | |
const selection = document.getSelection() | |
const pos1 = findPos(ref.current, selection.anchorNode, selection.anchorOffset) | |
const pos2 = findPos(ref.current, selection.focusNode , selection.focusOffset ) | |
setPos(pos1, pos2) | |
} | |
const handleCut = e => { | |
e.preventDefault() | |
e.clipboardData.setData("text/plain", selection) | |
insert("") | |
} | |
const handleCopy = e => { | |
e.preventDefault() | |
e.clipboardData.setData("text/plain", selection) | |
} | |
const handlePaste = e => { | |
e.preventDefault() | |
const data = e.clipboardData.getData("text/plain") | |
insert(data) | |
// `forceUpdate` is needed because pasting a selection | |
// can be idempotent. | |
forceUpdate() | |
} | |
return ( | |
<Title title="Editing …"> | |
<div className="h:4"/> | |
<div className="flex -r -x:center"> | |
<div className="w:40"> | |
<p style={{fontFamily: "monospace"}}> | |
^{value.split("\n").join("\\n")}$<br /> | |
{pos1}, {pos2} | |
</p> | |
<div className="h:1" /> | |
<div ref={ref} {...contentEditable} onKeyDown={handleKeyDown} onInput={handleInput} onCut={handleCut} onCopy={handleCopy} onPaste={handlePaste}> | |
{value.split("\n").map((block, index) => ( | |
<p key={index} className="fs:1.2 ls:-0.0.2 lh:1.3"> | |
{!block ? <span children={<br />} /> : block} | |
</p> | |
))} | |
</div> | |
{/* <div className="h:1" /> */} | |
{/* <textarea className="w:max h:8 fs:1.2 ls:-0.0.2 lh:1.3" /> */} | |
</div> | |
</div> | |
<div className="h:8" /> | |
</Title> | |
) | |
} | |
export { NewNote } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment