- Added support for saving and restoring text selection in the RichTextEditor, improving user experience during formatting. - Updated CSS styles for the RichTextEditor to enhance list formatting and overall appearance. - Introduced new ExerciseVariantFields component to streamline the editing of exercise variants, including fields for variant name, description, execution changes, and more. - Enhanced ExerciseFormPage to manage exercise variants more effectively, including improved state handling and UI updates for variant selection.
165 lines
4.5 KiB
JavaScript
165 lines
4.5 KiB
JavaScript
import React, { useRef, useEffect, useState, useCallback } from 'react'
|
|
|
|
function exec(cmd, value = null) {
|
|
try {
|
|
return document.execCommand(cmd, false, value)
|
|
} catch (_) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
/** Selection speichern, bevor Toolbar-Klicks sie zerstört (mousedown + preventDefault allein reicht nicht überall). */
|
|
function saveSelectionInside(editorEl) {
|
|
const sel = window.getSelection()
|
|
if (!sel || sel.rangeCount === 0 || !editorEl) return null
|
|
try {
|
|
const range = sel.getRangeAt(0)
|
|
if (!editorEl.contains(range.commonAncestorContainer)) return null
|
|
return range.cloneRange()
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
function restoreSelection(range) {
|
|
if (!range) return
|
|
try {
|
|
const sel = window.getSelection()
|
|
sel.removeAllRanges()
|
|
sel.addRange(range)
|
|
} catch {
|
|
/* noop */
|
|
}
|
|
}
|
|
|
|
/** 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()
|
|
e.stopPropagation()
|
|
const el = ref.current
|
|
if (!el) return
|
|
const saved = saveSelectionInside(el)
|
|
el.focus()
|
|
restoreSelection(saved)
|
|
try {
|
|
document.execCommand('styleWithCSS', false, false)
|
|
} catch {
|
|
/* optional */
|
|
}
|
|
fn()
|
|
sync()
|
|
}
|
|
|
|
const onLink = (e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
const el = ref.current
|
|
if (!el) return
|
|
const saved = saveSelectionInside(el)
|
|
el.focus()
|
|
restoreSelection(saved)
|
|
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 (Mehrzeilen-Markierung möglich)"
|
|
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>
|
|
)
|
|
}
|