shinkan-jinkendo/frontend/src/components/RichTextEditor.jsx

121 lines
3.4 KiB
React
Raw Normal View History

import React, { useRef, useEffect, useState, useCallback } from 'react'
function exec(cmd, value = null) {
try {
return document.execCommand(cmd, false, value)
} catch (_) {
return false
}
}
/** Browser: formatBlock erwartet oft Tag in Großschreibung. */
function formatBlock(tag) {
const t = String(tag).toUpperCase()
if (!exec('formatBlock', t)) {
exec('formatBlock', tag.toLowerCase())
}
}
function normalText() {
exec('removeFormat')
formatBlock('p')
}
/**
* Leichter WYSIWYG (contenteditable). Wert kommt von außen zuverlässig ins DOM (Edit-Modus).
*/
export default function RichTextEditor({ value, onChange, placeholder, minHeight = '140px' }) {
const ref = useRef(null)
const [focused, setFocused] = useState(false)
useEffect(() => {
const el = ref.current
if (!el || focused) return
const next = value ?? ''
if (el.innerHTML !== next) {
el.innerHTML = next
}
}, [value, focused])
const sync = useCallback(() => {
if (!ref.current) return
onChange(ref.current.innerHTML)
}, [onChange])
const run = (fn) => (e) => {
e.preventDefault()
ref.current?.focus()
fn()
sync()
}
const onLink = (e) => {
e.preventDefault()
ref.current?.focus()
const url = window.prompt('Link-URL (https://…)')
if (url) {
exec('createLink', url)
sync()
}
}
return (
<div className="rich-text-editor-wrap">
<div className="rich-text-toolbar" role="toolbar" aria-label="Formatierung">
<button type="button" className="rte-btn" title="Fett" onMouseDown={run(() => exec('bold'))}>
<strong>B</strong>
</button>
<button type="button" className="rte-btn" title="Kursiv" onMouseDown={run(() => exec('italic'))}>
<em>I</em>
</button>
<button type="button" className="rte-btn" title="Unterstrichen" onMouseDown={run(() => exec('underline'))}>
U
</button>
<span className="rte-sep" />
<button type="button" className="rte-btn" title="Normaler Absatz" onMouseDown={run(normalText)}>
Normal
</button>
<button type="button" className="rte-btn" title="Zwischenüberschrift" onMouseDown={run(() => formatBlock('h3'))}>
Ü3
</button>
<span className="rte-sep" />
<button type="button" className="rte-btn" title="Aufzählung" onMouseDown={run(() => exec('insertUnorderedList'))}>
• Liste
</button>
<button type="button" className="rte-btn" title="Nummerierte Liste" onMouseDown={run(() => exec('insertOrderedList'))}>
1. Liste
</button>
<span className="rte-sep" />
<button type="button" className="rte-btn" title="Link einfügen" onMouseDown={onLink}>
Link
</button>
<button
type="button"
className="rte-btn"
title="Zeichenformatierung am Cursor entfernen"
onMouseDown={run(() => {
exec('removeFormat')
exec('unlink')
})}
>
␡
</button>
</div>
<div
ref={ref}
className="rich-text-editor"
contentEditable
suppressContentEditableWarning
data-placeholder={placeholder || ''}
style={{ minHeight }}
onFocus={() => setFocused(true)}
onBlur={() => {
setFocused(false)
sync()
}}
onInput={sync}
/>
</div>
)
}