shinkan-jinkendo/frontend/src/utils/exerciseInlineMediaRefs.js
Lars 337f29401b
All checks were successful
Deploy Development / deploy (push) Successful in 34s
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 30s
feat(exercises): update inline media functionality and version bump to 0.8.63
- Incremented application version to 0.8.63 and updated changelog with new features.
- Enhanced inline media handling in the Rich Text Editor, including support for captions.
- Introduced new CSS styles for improved media display and layout in the editor.
- Replaced `ExerciseMediaEmbed` with `ExerciseAttachmentMediaStrip` for better media management in exercise content.
2026-05-08 12:20:24 +02:00

86 lines
2.5 KiB
JavaScript

/**
* §11 Inline-Medien: aus HTML / Übungsobjekt referenzierte exercise_media-IDs sammeln.
*/
const DATA_ATTR_RE = /data-shinkan-exercise-media\s*=\s*["']?(\d+)/gi
export const SHINKAN_EXERCISE_MEDIA_DRAG_MIME = 'application/x-shinkan-exercise-media'
/**
* @param {string|null|undefined} html
* @returns {Set<number>}
*/
export function collectInlineExerciseMediaIdsFromHtml(html) {
const ids = new Set()
if (!html || typeof html !== 'string') return ids
let m
const re = new RegExp(DATA_ATTR_RE.source, 'gi')
while ((m = re.exec(html)) !== null) {
const n = parseInt(m[1], 10)
if (Number.isFinite(n) && n > 0) ids.add(n)
}
return ids
}
const EXERCISE_RTF_FIELDS = ['summary', 'goal', 'execution', 'preparation', 'trainer_notes']
/**
* HTML-Schnipsel aus Übung + Varianten-Fließtext für Inline-Scan.
* @param {object|null|undefined} exercise
* @returns {string[]}
*/
export function gatherExerciseHtmlSlicesForInlineScan(exercise) {
if (!exercise || typeof exercise !== 'object') return []
const slices = []
for (const f of EXERCISE_RTF_FIELDS) {
const html = exercise[f]
if (typeof html === 'string' && html.trim()) slices.push(html)
}
for (const v of exercise.variants || []) {
const ec = v?.execution_changes
if (typeof ec === 'string' && ec.trim()) slices.push(ec)
}
return slices
}
/**
* Alle im Fließtext eingebetteten exercise_media-IDs (Übung + Varianten).
* @param {object|null|undefined} exercise
* @returns {Set<number>}
*/
export function collectInlineExerciseMediaIdsFromExercise(exercise) {
const ids = new Set()
for (const html of gatherExerciseHtmlSlicesForInlineScan(exercise)) {
collectInlineExerciseMediaIdsFromHtml(html).forEach((id) => ids.add(id))
}
return ids
}
/**
* @param {number} exerciseMediaId
* @param {string} [caption]
*/
export function buildExerciseMediaDragPayload(exerciseMediaId, caption = '') {
return JSON.stringify({
exerciseMediaId: Number(exerciseMediaId),
caption: typeof caption === 'string' ? caption : '',
})
}
/**
* @param {string} raw
* @returns {{ exerciseMediaId: number, caption: string }|null}
*/
export function parseExerciseMediaDragPayload(raw) {
if (!raw || typeof raw !== 'string') return null
try {
const o = JSON.parse(raw)
const id = Number(o.exerciseMediaId)
if (!Number.isFinite(id) || id < 1) return null
const caption = typeof o.caption === 'string' ? o.caption : ''
return { exerciseMediaId: id, caption }
} catch {
return null
}
}