All checks were successful
Deploy Development / deploy (push) Successful in 33s
Test Suite / pytest-backend (push) Successful in 24s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 27s
- Updated inline media markup to include a new data attribute for media size. - Enhanced the Rich Text Editor to support media size selection when inserting inline media. - Improved CSS styles for inline media display, accommodating different sizes (small, medium, full). - Bumped version to 0.8.62 and updated changelog to reflect these changes.
108 lines
3.9 KiB
JavaScript
108 lines
3.9 KiB
JavaScript
import React, { useMemo } from 'react'
|
|
import { sanitizeExerciseRichDisplayHtml } from '../utils/exerciseRichTextSanitize'
|
|
import ExerciseMediaEmbed from './ExerciseMediaEmbed'
|
|
|
|
function isTrashHidden(m) {
|
|
return String(m?.asset_lifecycle_state || 'active').toLowerCase() === 'trash_hidden'
|
|
}
|
|
|
|
function buildVisibleMediaMap(mediaList) {
|
|
const map = new Map()
|
|
for (const m of mediaList || []) {
|
|
if (!m || m.id == null || isTrashHidden(m)) continue
|
|
map.set(Number(m.id), m)
|
|
}
|
|
return map
|
|
}
|
|
|
|
function domToReactNodes(node, exerciseId, mediaById, path) {
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
const t = node.textContent
|
|
return t ? t : null
|
|
}
|
|
if (node.nodeType !== Node.ELEMENT_NODE) return null
|
|
|
|
const el = node
|
|
const tag = el.tagName.toLowerCase()
|
|
const key = path.join('.')
|
|
|
|
if (tag === 'span' && el.getAttribute('data-shinkan-exercise-media')) {
|
|
const raw = el.getAttribute('data-shinkan-exercise-media')
|
|
const mid = parseInt(raw, 10)
|
|
if (!Number.isFinite(mid) || mid < 1) {
|
|
return (
|
|
<span key={key} className="shinkan-inline-media-missing" style={{ color: 'var(--text3)', fontSize: '0.9em' }}>
|
|
[Ungültiger Medienverweis]
|
|
</span>
|
|
)
|
|
}
|
|
const media = mediaById.get(mid)
|
|
if (!media) {
|
|
return (
|
|
<span key={key} className="shinkan-inline-media-missing" style={{ color: 'var(--text3)', fontSize: '0.9em' }}>
|
|
[Medium nicht verfügbar]
|
|
</span>
|
|
)
|
|
}
|
|
const rawSize = (el.getAttribute('data-shinkan-exercise-media-size') || 'medium').toLowerCase().trim()
|
|
const layoutSize = rawSize === 'small' || rawSize === 'full' ? rawSize : 'medium'
|
|
const wrapClass =
|
|
layoutSize === 'small'
|
|
? 'shinkan-inline-media-wrap shinkan-inline-media-wrap--sm'
|
|
: layoutSize === 'full'
|
|
? 'shinkan-inline-media-wrap shinkan-inline-media-wrap--full'
|
|
: 'shinkan-inline-media-wrap shinkan-inline-media-wrap--md'
|
|
const lc = String(media.asset_lifecycle_state || 'active').toLowerCase()
|
|
return (
|
|
<span key={key} className={wrapClass} style={{ display: 'inline-block', verticalAlign: 'top' }}>
|
|
{lc === 'trash_soft' && (
|
|
<span style={{ fontSize: '0.75rem', color: 'var(--danger)', display: 'block', marginBottom: '4px' }}>
|
|
Dieses Medium ist im Papierkorb.
|
|
</span>
|
|
)}
|
|
<ExerciseMediaEmbed exerciseId={exerciseId} media={media} layoutSize={layoutSize} />
|
|
</span>
|
|
)
|
|
}
|
|
|
|
const children = []
|
|
const childNodes = Array.from(el.childNodes)
|
|
childNodes.forEach((ch, i) => {
|
|
const sub = domToReactNodes(ch, exerciseId, mediaById, [...path, String(i)])
|
|
if (sub != null && sub !== false) children.push(sub)
|
|
})
|
|
|
|
const props = { key }
|
|
if (tag === 'a' && el.getAttribute('href')) {
|
|
props.href = el.getAttribute('href')
|
|
props.target = '_blank'
|
|
props.rel = 'noreferrer'
|
|
}
|
|
return React.createElement(tag, props, children.length ? children : null)
|
|
}
|
|
|
|
/**
|
|
* Zentraler Anzeige-Pfad für Übungs-Rich-Text inkl. §11 Inline-Medien.
|
|
* @param {{ html?: string|null, exerciseId?: number|null, media?: object[]|null, className?: string }} props
|
|
*/
|
|
export default function ExerciseRichTextBlock({ html, exerciseId, media, className = '' }) {
|
|
const safe = useMemo(() => sanitizeExerciseRichDisplayHtml(html), [html])
|
|
const mediaById = useMemo(() => buildVisibleMediaMap(media), [media])
|
|
|
|
const body = useMemo(() => {
|
|
if (!safe.trim()) return null
|
|
const tpl = document.createElement('template')
|
|
tpl.innerHTML = safe
|
|
const nodes = []
|
|
Array.from(tpl.content.childNodes).forEach((ch, i) => {
|
|
const r = domToReactNodes(ch, exerciseId, mediaById, [String(i)])
|
|
if (r != null) nodes.push(r)
|
|
})
|
|
return nodes
|
|
}, [safe, exerciseId, mediaById])
|
|
|
|
if (!body || body.length === 0) return null
|
|
|
|
return <div className={`rich-text-content ${className}`.trim()}>{body}</div>
|
|
}
|