Skip to content

Instantly share code, notes, and snippets.

@zaydek-old
Created August 12, 2019 17:53
Show Gist options
  • Save zaydek-old/2bcb2429e9d06f60ebfa2a4c281a348b to your computer and use it in GitHub Desktop.
Save zaydek-old/2bcb2429e9d06f60ebfa2a4c281a348b to your computer and use it in GitHub Desktop.
import React from "react"
// import PropTypes from "prop-types"
// import useMethods from "use-methods"
import * as traverseDOM from "./traverseDOM"
import { Title } from "./Title"
import { useForceUpdate } from "./useForceUpdate"
function ContentEditable(props) {
const ref = React.useRef(null)
const [forcedUpdate, forceUpdate] = useForceUpdate()
// `insert` is a convenience function for inserting and
// trimming text.
const insert = (insertText, trimL = 0, trimR = 0) => {
let { value, pos1, pos2 } = props
value = value.slice(0, pos1 - trimL) + insertText + value.slice(pos2 + trimR)
pos1 = pos1 - trimL + insertText.length
pos2 = pos1
return { value, pos1, pos2 }
}
React.useLayoutEffect(() => {
const selection = document.getSelection()
const node1 = traverseDOM.findNode(ref.current, props.pos1)
const node2 = traverseDOM.findNode(ref.current, props.pos2)
const range = document.createRange()
range.setStart(node1.node, node1.offset) // NOTE: `(...node1)` doesn’t work.
range.setEnd (node2.node, node2.offset)
selection.removeAllRanges()
selection.addRange(range)
if (props.pos1 === props.value.length && props.pos1 === props.pos2) {
window.scrollTo(0, ref.current.scrollHeight)
}
}, [props.value, props.pos1, props.pos2, forcedUpdate])
const handleSelect = e => {
const selection = document.getSelection()
const pos1 = traverseDOM.findPos(ref.current, selection.anchorNode, selection.anchorOffset)
const pos2 = traverseDOM.findPos(ref.current, selection.focusNode , selection.focusOffset )
props.onChange({ ...props.editorState, pos1, pos2 })
}
const handleKeyDown = e => {
if (e.key !== "Enter" && e.key !== "Backspace" && (!e.ctrlKey && e.key !== "d")) {
return
}
switch (true) {
case e.key === "Enter":
e.preventDefault()
const { value, pos1, pos2 } = insert("\n")
props.onChange({ ...props.editorState, value, pos1, pos2 })
break
case e.key === "Backspace" || (e.ctrlKey && e.key === "d"):
if (!props.value.length) {
e.preventDefault()
// We don’t need to re-render (most editors don’t)
// but we do to communicate user feedback.
forceUpdate()
} else if (props.pos1 === props.pos2 && ((props.pos1 - 1 >= 0 && props.value[props.pos1 - 1] === "\n") || props.value[props.pos1] === "\n")) {
e.preventDefault()
const dir = Number(e.key === "Backspace")
const { value, pos1, pos2 } = insert("", dir, !dir)
props.onChange({ ...props.editorState, value, pos1, pos2 })
} else if (props.pos1 !== props.pos2) {
e.preventDefault()
const { value, pos1, pos2 } = insert("")
props.onChange({ ...props.editorState, value, pos1, pos2 })
}
break
default:
break
}
}
// Fix for `compositionend` events.
//
// See blog.evanyou.me/2014/01/03/composition-event for reference.
const handleCompositionEnd = e => {
const value = traverseDOM.readRoot(ref.current)
const selection = document.getSelection()
const pos1 = traverseDOM.findPos(ref.current, selection.anchorNode, selection.anchorOffset)
const pos2 = traverseDOM.findPos(ref.current, selection.focusNode , selection.focusOffset )
props.onChange({ ...props.editorState, value, pos1, pos2 })
}
const handleInput = e => {
if (e.nativeEvent.isComposing) {
// See `handleCompositionEnd` for fix.
return
}
const value = traverseDOM.readRoot(ref.current)
const selection = document.getSelection()
const pos1 = traverseDOM.findPos(ref.current, selection.anchorNode, selection.anchorOffset)
const pos2 = traverseDOM.findPos(ref.current, selection.focusNode , selection.focusOffset )
props.onChange({ ...props.editorState, value, pos1, pos2 })
}
return (
<>
<div>
<p className="fs:1.1 lh:1.3" style={{fontFamily: "monospace"}}>
value: ^{props.value.split("\n").join("\\n")}$
</p>
<p className="fs:1.1 lh:1.3" style={{fontFamily: "monospace"}}>
pos1: &nbsp;{props.pos1}
</p>
<p className="fs:1.1 lh:1.3" style={{fontFamily: "monospace"}}>
pos2: &nbsp;{props.pos2}
</p>
</div>
<div className="h:1.1" />
{React.createElement(
"div",
{
ref,
contentEditable: true,
suppressContentEditableWarning: true,
spellCheck: false,
onSelect: handleSelect,
onKeyDown: handleKeyDown,
onCompositionEnd: handleCompositionEnd,
onInput: handleInput
},
props.value.split("\n").map((item, index) => (
<p key={index} className="fs:1.2 ls:-0.0.2 lh:1.3">
{!item ? <span><br /></span> : item}
</p>
))
)}
</>
)
}
function createEditorState(value = "", pos1 = 0, pos2 = 0) {
const stack = [] // The state stack.
const index = -1 // The state stack’s index.
return { value, pos1, pos2, stack, index }
}
function NewNote(props) {
const [editorState, setEditorState] = React.useState(createEditorState("Hello, world!", 13, 13))
return (
<Title title="Editing …">
<div className="h:4"/>
<div className="flex -r -x:center">
<div className="w:40">
<ContentEditable {...editorState} editorState={editorState} onChange={setEditorState} />
</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