Inline Medien #24
|
|
@ -1,6 +1,6 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.60"
|
APP_VERSION = "0.8.61"
|
||||||
BUILD_DATE = "2026-05-08"
|
BUILD_DATE = "2026-05-08"
|
||||||
DB_SCHEMA_VERSION = "20260508049"
|
DB_SCHEMA_VERSION = "20260508049"
|
||||||
|
|
||||||
|
|
@ -29,6 +29,13 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.8.61",
|
||||||
|
"date": "2026-05-08",
|
||||||
|
"changes": [
|
||||||
|
"RTE „Bild/Video im Text“: eingebaute Hilfe (Caret + insertHTML/Fallback); sichtbarer 📎-Chip im Editor; Hinweis bei fehlgeschlagener Einfügung/Prompt-ID",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.8.60",
|
"version": "0.8.60",
|
||||||
"date": "2026-05-08",
|
"date": "2026-05-08",
|
||||||
|
|
|
||||||
|
|
@ -3895,6 +3895,26 @@ a.analysis-split__nav-item {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* §11 Inline-Marker: im Editor sichtbar (DOM ist nur leeres span + ZWJ — sonst „passiert nichts“-Effekt) */
|
||||||
|
.rich-text-editor span.shinkan-inline-media {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: baseline;
|
||||||
|
margin: 2px 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px dashed var(--accent);
|
||||||
|
background: var(--surface2);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent-dark);
|
||||||
|
line-height: 1.35;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.rich-text-editor span.shinkan-inline-media::before {
|
||||||
|
content: '📎 #' attr(data-shinkan-exercise-media);
|
||||||
|
}
|
||||||
|
|
||||||
/* Listen im Editor (nicht nur in .rich-text-content) – sonst „unsichtbare“ Bullets */
|
/* Listen im Editor (nicht nur in .rich-text-content) – sonst „unsichtbare“ Bullets */
|
||||||
.rich-text-editor ul,
|
.rich-text-editor ul,
|
||||||
.rich-text-editor ol {
|
.rich-text-editor ol {
|
||||||
|
|
|
||||||
|
|
@ -46,26 +46,58 @@ function normalText() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertExerciseMediaPlaceholder(editorEl, mediaId) {
|
function insertExerciseMediaPlaceholder(editorEl, mediaId) {
|
||||||
if (!editorEl || mediaId == null) return
|
if (!editorEl || mediaId == null) return false
|
||||||
const sid = parseInt(String(mediaId), 10)
|
const sid = parseInt(String(mediaId), 10)
|
||||||
if (!Number.isFinite(sid) || sid < 1) return
|
if (!Number.isFinite(sid) || sid < 1) return false
|
||||||
|
|
||||||
editorEl.focus()
|
editorEl.focus()
|
||||||
const sel = window.getSelection()
|
const sel = window.getSelection()
|
||||||
if (!sel) return
|
if (!sel) return false
|
||||||
let range = null
|
|
||||||
if (sel?.rangeCount) {
|
let caretInside = false
|
||||||
|
if (sel.rangeCount > 0) {
|
||||||
try {
|
try {
|
||||||
const r0 = sel.getRangeAt(0)
|
const r0 = sel.getRangeAt(0)
|
||||||
if (editorEl.contains(r0.commonAncestorContainer)) range = r0
|
caretInside = editorEl.contains(r0.commonAncestorContainer)
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
caretInside = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!range) {
|
if (!caretInside) {
|
||||||
|
const anchor = document.createRange()
|
||||||
|
try {
|
||||||
|
anchor.selectNodeContents(editorEl)
|
||||||
|
anchor.collapse(false)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sel.removeAllRanges()
|
||||||
|
sel.addRange(anchor)
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = `<span data-shinkan-exercise-media="${sid}" class="shinkan-inline-media">\u2060</span>`
|
||||||
|
let inserted = false
|
||||||
|
try {
|
||||||
|
inserted = document.execCommand('insertHTML', false, html)
|
||||||
|
} catch {
|
||||||
|
inserted = false
|
||||||
|
}
|
||||||
|
if (inserted) return true
|
||||||
|
|
||||||
|
let range = null
|
||||||
|
try {
|
||||||
|
range = sel.rangeCount ? sel.getRangeAt(0).cloneRange() : null
|
||||||
|
} catch {
|
||||||
|
range = null
|
||||||
|
}
|
||||||
|
if (!range || !editorEl.contains(range.commonAncestorContainer)) {
|
||||||
range = document.createRange()
|
range = document.createRange()
|
||||||
range.selectNodeContents(editorEl)
|
range.selectNodeContents(editorEl)
|
||||||
range.collapse(false)
|
range.collapse(false)
|
||||||
|
sel.removeAllRanges()
|
||||||
|
sel.addRange(range)
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
const span = document.createElement('span')
|
const span = document.createElement('span')
|
||||||
span.setAttribute('data-shinkan-exercise-media', String(sid))
|
span.setAttribute('data-shinkan-exercise-media', String(sid))
|
||||||
span.className = 'shinkan-inline-media'
|
span.className = 'shinkan-inline-media'
|
||||||
|
|
@ -76,6 +108,10 @@ function insertExerciseMediaPlaceholder(editorEl, mediaId) {
|
||||||
range.collapse(true)
|
range.collapse(true)
|
||||||
sel.removeAllRanges()
|
sel.removeAllRanges()
|
||||||
sel.addRange(range)
|
sel.addRange(range)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -144,30 +180,50 @@ export default function RichTextEditor({
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const el = ref.current
|
const el = ref.current
|
||||||
if (!el || !insertExerciseMediaSlots?.length) return
|
const slots = insertExerciseMediaSlots
|
||||||
|
if (!el || !slots?.length) return
|
||||||
let choice = ''
|
let choice = ''
|
||||||
if (insertExerciseMediaSlots.length === 1) {
|
if (slots.length === 1) {
|
||||||
choice = String(insertExerciseMediaSlots[0].id)
|
choice = String(slots[0].id)
|
||||||
} else {
|
} else {
|
||||||
choice = window.prompt(
|
choice =
|
||||||
`Medium-ID eingeben oder aus Liste:\n${insertExerciseMediaSlots
|
window.prompt(
|
||||||
|
`Medium-ID eingeben oder aus Liste:\n${slots
|
||||||
.slice(0, 30)
|
.slice(0, 30)
|
||||||
.map((s) => `${s.id}: ${s.label}`)
|
.map((s) => `${s.id}: ${s.label}`)
|
||||||
.join('\n')}`,
|
.join('\n')}`,
|
||||||
'',
|
String(slots[0].id),
|
||||||
)
|
) ?? ''
|
||||||
}
|
}
|
||||||
const idParsed = parseInt(String(choice).trim(), 10)
|
const idParsed = parseInt(String(choice).trim(), 10)
|
||||||
if (!Number.isFinite(idParsed)) return
|
if (!Number.isFinite(idParsed)) {
|
||||||
if (!insertExerciseMediaSlots.some((s) => Number(s.id) === idParsed)) {
|
if (slots.length > 1) {
|
||||||
alert('Diese Übungs-ID ist nicht in der Medienliste.')
|
alert('Keine gültige Medium-ID angegeben.')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!slots.some((s) => Number(s.id) === idParsed)) {
|
||||||
|
alert('Diese ID ist nicht in der Medienliste dieser Übung.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedRange = saveSelectionInside(el)
|
||||||
|
|
||||||
|
queueMicrotask(() => {
|
||||||
|
const shell = ref.current
|
||||||
|
if (!shell) return
|
||||||
|
shell.focus()
|
||||||
|
restoreSelection(savedRange)
|
||||||
|
const ok = insertExerciseMediaPlaceholder(shell, idParsed)
|
||||||
|
if (!ok) {
|
||||||
|
alert(
|
||||||
|
'Einfügen ist fehlgeschlagen — bitte einmal ins Textfeld klicken (Cursor setzen), dann „Bild/Video im Text“ erneut.',
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const saved = saveSelectionInside(el)
|
|
||||||
el.focus()
|
|
||||||
restoreSelection(saved)
|
|
||||||
insertExerciseMediaPlaceholder(el, idParsed)
|
|
||||||
sync()
|
sync()
|
||||||
|
shell.focus()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user