chore(release): bump version to 0.8.61 and update changelog
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 23s
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 23s
- Updated application version to 0.8.61.
- Added changelog entry for version 0.8.61, detailing the new inline help feature for the "Bild/Video im Text" functionality, including a visible 📎-chip in the editor and prompts for failed insertions.
This commit is contained in:
parent
cc51b0f08f
commit
979e328cef
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.8.60"
|
||||
APP_VERSION = "0.8.61"
|
||||
BUILD_DATE = "2026-05-08"
|
||||
DB_SCHEMA_VERSION = "20260508049"
|
||||
|
||||
|
|
@ -29,6 +29,13 @@ MODULE_VERSIONS = {
|
|||
}
|
||||
|
||||
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",
|
||||
"date": "2026-05-08",
|
||||
|
|
|
|||
|
|
@ -3895,6 +3895,26 @@ a.analysis-split__nav-item {
|
|||
overflow-y: auto;
|
||||
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 */
|
||||
.rich-text-editor ul,
|
||||
.rich-text-editor ol {
|
||||
|
|
|
|||
|
|
@ -46,36 +46,72 @@ function normalText() {
|
|||
}
|
||||
|
||||
function insertExerciseMediaPlaceholder(editorEl, mediaId) {
|
||||
if (!editorEl || mediaId == null) return
|
||||
if (!editorEl || mediaId == null) return false
|
||||
const sid = parseInt(String(mediaId), 10)
|
||||
if (!Number.isFinite(sid) || sid < 1) return
|
||||
if (!Number.isFinite(sid) || sid < 1) return false
|
||||
|
||||
editorEl.focus()
|
||||
const sel = window.getSelection()
|
||||
if (!sel) return
|
||||
let range = null
|
||||
if (sel?.rangeCount) {
|
||||
if (!sel) return false
|
||||
|
||||
let caretInside = false
|
||||
if (sel.rangeCount > 0) {
|
||||
try {
|
||||
const r0 = sel.getRangeAt(0)
|
||||
if (editorEl.contains(r0.commonAncestorContainer)) range = r0
|
||||
caretInside = editorEl.contains(r0.commonAncestorContainer)
|
||||
} 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.selectNodeContents(editorEl)
|
||||
range.collapse(false)
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
}
|
||||
try {
|
||||
const span = document.createElement('span')
|
||||
span.setAttribute('data-shinkan-exercise-media', String(sid))
|
||||
span.className = 'shinkan-inline-media'
|
||||
span.appendChild(document.createTextNode('\u2060'))
|
||||
range.deleteContents()
|
||||
range.insertNode(span)
|
||||
range.setStartAfter(span)
|
||||
range.collapse(true)
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
const span = document.createElement('span')
|
||||
span.setAttribute('data-shinkan-exercise-media', String(sid))
|
||||
span.className = 'shinkan-inline-media'
|
||||
span.appendChild(document.createTextNode('\u2060'))
|
||||
range.deleteContents()
|
||||
range.insertNode(span)
|
||||
range.setStartAfter(span)
|
||||
range.collapse(true)
|
||||
sel.removeAllRanges()
|
||||
sel.addRange(range)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -144,30 +180,50 @@ export default function RichTextEditor({
|
|||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const el = ref.current
|
||||
if (!el || !insertExerciseMediaSlots?.length) return
|
||||
const slots = insertExerciseMediaSlots
|
||||
if (!el || !slots?.length) return
|
||||
let choice = ''
|
||||
if (insertExerciseMediaSlots.length === 1) {
|
||||
choice = String(insertExerciseMediaSlots[0].id)
|
||||
if (slots.length === 1) {
|
||||
choice = String(slots[0].id)
|
||||
} else {
|
||||
choice = window.prompt(
|
||||
`Medium-ID eingeben oder aus Liste:\n${insertExerciseMediaSlots
|
||||
.slice(0, 30)
|
||||
.map((s) => `${s.id}: ${s.label}`)
|
||||
.join('\n')}`,
|
||||
'',
|
||||
)
|
||||
choice =
|
||||
window.prompt(
|
||||
`Medium-ID eingeben oder aus Liste:\n${slots
|
||||
.slice(0, 30)
|
||||
.map((s) => `${s.id}: ${s.label}`)
|
||||
.join('\n')}`,
|
||||
String(slots[0].id),
|
||||
) ?? ''
|
||||
}
|
||||
const idParsed = parseInt(String(choice).trim(), 10)
|
||||
if (!Number.isFinite(idParsed)) return
|
||||
if (!insertExerciseMediaSlots.some((s) => Number(s.id) === idParsed)) {
|
||||
alert('Diese Übungs-ID ist nicht in der Medienliste.')
|
||||
if (!Number.isFinite(idParsed)) {
|
||||
if (slots.length > 1) {
|
||||
alert('Keine gültige Medium-ID angegeben.')
|
||||
}
|
||||
return
|
||||
}
|
||||
const saved = saveSelectionInside(el)
|
||||
el.focus()
|
||||
restoreSelection(saved)
|
||||
insertExerciseMediaPlaceholder(el, idParsed)
|
||||
sync()
|
||||
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
|
||||
}
|
||||
sync()
|
||||
shell.focus()
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user