94 lines
3.1 KiB
React
94 lines
3.1 KiB
React
|
|
import React, { useRef, useEffect, useState, useCallback } from 'react'
|
||
|
|
|
||
|
|
function exec(cmd, value = null) {
|
||
|
|
try {
|
||
|
|
document.execCommand(cmd, false, value)
|
||
|
|
} catch (_) {
|
||
|
|
/* ignore */
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Leichter WYSIWYG-Editor (contenteditable) — ohne zusätzliche npm-Pakete.
|
||
|
|
* Speichert HTML; Anzeige über sanitizeTrainerHtml + dangerouslySetInnerHTML.
|
||
|
|
*/
|
||
|
|
export default function RichTextEditor({ value, onChange, placeholder, minHeight = '140px' }) {
|
||
|
|
const ref = useRef(null)
|
||
|
|
const [focused, setFocused] = useState(false)
|
||
|
|
const lastExternal = useRef(value)
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (!ref.current) return
|
||
|
|
if (focused) return
|
||
|
|
if (value !== lastExternal.current) {
|
||
|
|
lastExternal.current = value
|
||
|
|
if (ref.current.innerHTML !== (value || '')) {
|
||
|
|
ref.current.innerHTML = value || ''
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, [value, focused])
|
||
|
|
|
||
|
|
const sync = useCallback(() => {
|
||
|
|
if (!ref.current) return
|
||
|
|
const html = ref.current.innerHTML
|
||
|
|
lastExternal.current = html
|
||
|
|
onChange(html)
|
||
|
|
}, [onChange])
|
||
|
|
|
||
|
|
const onLink = () => {
|
||
|
|
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" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('bold')}>
|
||
|
|
<strong>B</strong>
|
||
|
|
</button>
|
||
|
|
<button type="button" className="rte-btn" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('italic')}>
|
||
|
|
<em>I</em>
|
||
|
|
</button>
|
||
|
|
<button type="button" className="rte-btn" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('underline')}>
|
||
|
|
U
|
||
|
|
</button>
|
||
|
|
<span className="rte-sep" />
|
||
|
|
<button type="button" className="rte-btn" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('formatBlock', 'h3')}>
|
||
|
|
H3
|
||
|
|
</button>
|
||
|
|
<button type="button" className="rte-btn" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('insertUnorderedList')}>
|
||
|
|
•
|
||
|
|
</button>
|
||
|
|
<button type="button" className="rte-btn" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('insertOrderedList')}>
|
||
|
|
1.
|
||
|
|
</button>
|
||
|
|
<button type="button" className="rte-btn" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('formatBlock', 'p')}>
|
||
|
|
¶
|
||
|
|
</button>
|
||
|
|
<span className="rte-sep" />
|
||
|
|
<button type="button" className="rte-btn" onMouseDown={(e) => e.preventDefault()} onClick={onLink}>
|
||
|
|
Link
|
||
|
|
</button>
|
||
|
|
<button type="button" className="rte-btn" onMouseDown={(e) => e.preventDefault()} onClick={() => exec('removeFormat')}>
|
||
|
|
⌫
|
||
|
|
</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>
|
||
|
|
)
|
||
|
|
}
|