Skip to content

Instantly share code, notes, and snippets.

@zaydek-old
Created August 10, 2019 13:09
Show Gist options
  • Save zaydek-old/b65180a1b964f7feea1c0c480b3cfbba to your computer and use it in GitHub Desktop.
Save zaydek-old/b65180a1b964f7feea1c0c480b3cfbba to your computer and use it in GitHub Desktop.
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