femto-webapp/src/components/FancyTextEditor.tsx
2025-05-06 12:53:32 +02:00

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>
)
}