shinkan-jinkendo/frontend/src/components/RichTextEditor.jsx
Lars 91b3fec5cc
Some checks failed
Deploy Development / deploy (push) Successful in 33s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 1m55s
feat: enhance RichTextEditor and exercise variant management
- 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.
2026-04-28 13:09:42 +02:00

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