From 91b3fec5ccde01805da1229a310344d9e84adae3 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 28 Apr 2026 13:09:42 +0200 Subject: [PATCH] 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. --- frontend/src/app.css | 59 +++ frontend/src/components/RichTextEditor.jsx | 50 +- frontend/src/pages/ExerciseFormPage.jsx | 577 ++++++++++----------- 3 files changed, 393 insertions(+), 293 deletions(-) diff --git a/frontend/src/app.css b/frontend/src/app.css index 4e8a194..2b122d0 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -2406,6 +2406,26 @@ a.analysis-split__nav-item { overflow-y: auto; resize: vertical; } +/* Listen im Editor (nicht nur in .rich-text-content) – sonst „unsichtbare“ Bullets */ +.rich-text-editor ul, +.rich-text-editor ol { + margin: 0.35rem 0; + padding-left: 1.35rem; + list-style-position: outside; +} +.rich-text-editor ul { + list-style-type: disc; +} +.rich-text-editor ol { + list-style-type: decimal; +} +.rich-text-editor li { + margin: 0.15rem 0; + display: list-item; +} +.rich-text-editor p { + margin: 0.35rem 0; +} .rich-text-editor:empty:before { content: attr(data-placeholder); color: var(--text3); @@ -2441,6 +2461,45 @@ a.analysis-split__nav-item { .exercise-card__body { flex: 1 1 auto; } +.exercise-variants-details summary { + list-style: none; +} +.exercise-variants-details summary::-webkit-details-marker { + display: none; +} +.exercise-variants-summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 16px; + cursor: pointer; + user-select: none; +} +.exercise-variants-summary__title { + font-size: 1.1rem; + font-weight: 600; +} +.exercise-variants-summary__badge { + font-size: 12px; + font-weight: 600; + color: var(--text2); + background: var(--surface2); + padding: 4px 10px; + border-radius: 999px; + border: 1px solid var(--border); +} +.exercise-variants-details__body { + padding: 0 16px 16px; + border-top: 1px solid var(--border); +} +.exercise-variants-hint { + font-size: 13px; + color: var(--text2); + margin: 12px 0; + line-height: 1.45; +} + .exercise-card__actions { flex-shrink: 0; display: flex; diff --git a/frontend/src/components/RichTextEditor.jsx b/frontend/src/components/RichTextEditor.jsx index f5254ea..5a83d93 100644 --- a/frontend/src/components/RichTextEditor.jsx +++ b/frontend/src/components/RichTextEditor.jsx @@ -8,6 +8,30 @@ function exec(cmd, value = null) { } } +/** 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() @@ -44,14 +68,29 @@ export default function RichTextEditor({ value, onChange, placeholder, minHeight const run = (fn) => (e) => { e.preventDefault() - ref.current?.focus() + 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() - ref.current?.focus() + 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) @@ -79,7 +118,12 @@ export default function RichTextEditor({ value, onChange, placeholder, minHeight Ü3 -