95 lines
2.4 KiB
TypeScript
95 lines
2.4 KiB
TypeScript
import { useRef, useEffect, KeyboardEvent, useState } from 'react'
|
|
|
|
interface TextInputProps {
|
|
value: string
|
|
onInput: (value: string) => void
|
|
onKeyDown: (e: TextInputKeyDownEvent) => void
|
|
className?: string
|
|
placeholder?: string
|
|
}
|
|
|
|
export interface TextInputKeyDownEvent {
|
|
key: string
|
|
ctrlKey: boolean
|
|
preventDefault: () => void
|
|
}
|
|
|
|
export default function FancyTextEditor({
|
|
value: _value,
|
|
onInput,
|
|
onKeyDown,
|
|
className: extraClasses = '',
|
|
placeholder = '',
|
|
}: TextInputProps) {
|
|
const divRef = useRef<HTMLDivElement>(null)
|
|
const [hasFocus, setHasFocus] = useState(false)
|
|
|
|
// the contenteditable likes to slip in newlines at the bottom of our innerText
|
|
// which makes it bad to check for empty string because it might be "\n"
|
|
// so we just trim it upfront and then fogeddaboudit
|
|
const value = _value.trim()
|
|
|
|
// The funky mechanics here are to stop the cursor from jumping back the start.
|
|
// It probably will have the cursor jump to the start if anything changes programmatically,
|
|
// which is probably unnecessary anyway
|
|
useEffect(() => {
|
|
const div = divRef.current
|
|
if (div == null) {
|
|
return
|
|
}
|
|
|
|
if (!value && !hasFocus) {
|
|
div.innerText = placeholder
|
|
} else if (div.innerText !== value) {
|
|
div.innerText = value
|
|
}
|
|
}, [hasFocus, placeholder, value])
|
|
|
|
useEffect(() => {
|
|
const div = divRef.current!
|
|
if (div == null) {
|
|
return
|
|
}
|
|
|
|
const inputListener = () => {
|
|
onInput(div.innerText)
|
|
}
|
|
|
|
const blurListener = () => {
|
|
setHasFocus(false)
|
|
if (!value) {
|
|
div.innerText = placeholder
|
|
}
|
|
}
|
|
|
|
const focusListener = () => {
|
|
setHasFocus(true)
|
|
div.innerText = value
|
|
}
|
|
|
|
div.addEventListener('input', inputListener)
|
|
div.addEventListener('blur', blurListener)
|
|
div.addEventListener('focus', focusListener)
|
|
|
|
return () => {
|
|
div.removeEventListener('focus', focusListener)
|
|
div.removeEventListener('blur', blurListener)
|
|
div.removeEventListener('input', inputListener)
|
|
}
|
|
}, [onInput, placeholder, value])
|
|
|
|
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
onKeyDown(e)
|
|
}
|
|
|
|
const textColour = value ? 'text-gray-900' : 'text-gray-500'
|
|
return (
|
|
<div
|
|
ref={divRef}
|
|
className={`text-input w-full min-h-30 ${textColour} ${extraClasses}`}
|
|
contentEditable
|
|
onKeyDown={handleKeyDown}
|
|
suppressContentEditableWarning
|
|
></div>
|
|
)
|
|
}
|