Text-Editor: Style-Picker focus-restore + Runs-Round-Trip beim Reopen
User-Bugs:
1. Stil-Picker setzt Toolbar-State aber Editor zeigt alle Texte gleich
2. Beim Reopen (Doppelklick auf DOSSIER-Text) ist Editor leer/unformatiert
Fix 1 — applyStyle focus-restore:
Style-Picker stiehlt Focus → savedRange ist da, aber execCommand
ohne Focus auf editable schreibt nichts. Fix: editorRef.focus() +
restoreSelection() VOR execCommand. Plus Size jetzt via inline-span
(execCommand fontSize geht nur 1-7 Scale, kein Pixel).
Fix 2 — Runs persistieren als UserString fuer Round-Trip:
- _commit: runs als JSON in attrs.SetUserString("dossier_text_runs")
abgelegt zusammen mit dem dossier_text-Tag
- open_for_edit: liest UserString, parsed JSON → initial_runs
- Bridge INIT: sendet initialRuns mit
- Frontend onMessage INIT: wenn initialRuns vorhanden → runsToHtml()
baut HTML mit spans (font-family, font-size, color) + Tags (b/i/u/
sup/sub) — Editor zeigt jetzt beim Reopen das tatsaechliche reiche
Format statt PlainText im Default-Helvetica
runsToHtml: pro run text/style-Splittung per \n → <br>. Escaping fuer
&<> damit kein XSS / Parse-Fehler.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+22
-2
@@ -84,7 +84,7 @@ def _on_idle_check_pending_edit(sender, e):
|
|||||||
|
|
||||||
class TextEditorBridge(panel_base.BaseBridge):
|
class TextEditorBridge(panel_base.BaseBridge):
|
||||||
def __init__(self, frame_data, settings, fonts,
|
def __init__(self, frame_data, settings, fonts,
|
||||||
edit_obj_id=None, initial_text=""):
|
edit_obj_id=None, initial_text="", initial_runs=None):
|
||||||
panel_base.BaseBridge.__init__(self, "text_editor")
|
panel_base.BaseBridge.__init__(self, "text_editor")
|
||||||
self._frame = frame_data # (origin, width, height, p1, p2)
|
self._frame = frame_data # (origin, width, height, p1, p2)
|
||||||
self._initial_settings = settings
|
self._initial_settings = settings
|
||||||
@@ -92,6 +92,7 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
self._form_ref = None
|
self._form_ref = None
|
||||||
self._edit_obj_id = edit_obj_id # bei Doppelklick-Edit gesetzt
|
self._edit_obj_id = edit_obj_id # bei Doppelklick-Edit gesetzt
|
||||||
self._initial_text = initial_text
|
self._initial_text = initial_text
|
||||||
|
self._initial_runs = initial_runs # rich-format-Runs falls vorhanden
|
||||||
|
|
||||||
def set_form(self, form):
|
def set_form(self, form):
|
||||||
self._form_ref = form
|
self._form_ref = form
|
||||||
@@ -106,6 +107,7 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
"fonts": self._fonts,
|
"fonts": self._fonts,
|
||||||
"styles": styles,
|
"styles": styles,
|
||||||
"initialText": self._initial_text,
|
"initialText": self._initial_text,
|
||||||
|
"initialRuns": self._initial_runs,
|
||||||
"editMode": bool(self._edit_obj_id),
|
"editMode": bool(self._edit_obj_id),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -219,6 +221,15 @@ class TextEditorBridge(panel_base.BaseBridge):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[TEXT-EDITOR] color:", ex)
|
print("[TEXT-EDITOR] color:", ex)
|
||||||
attrs.SetUserString("dossier_text", "1")
|
attrs.SetUserString("dossier_text", "1")
|
||||||
|
# Runs als JSON persistieren — beim Re-Open kann der Editor
|
||||||
|
# die ganze Struktur (Fonts/Sizes/Styles pro Segment) wieder
|
||||||
|
# herstellen statt nur PlainText zu zeigen.
|
||||||
|
if runs:
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
attrs.SetUserString("dossier_text_runs", json.dumps(runs))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[TEXT-EDITOR] runs persist:", ex)
|
||||||
|
|
||||||
# Edit-Mode: bestehenden TextEntity ersetzen statt neu hinzu
|
# Edit-Mode: bestehenden TextEntity ersetzen statt neu hinzu
|
||||||
if self._edit_obj_id is not None:
|
if self._edit_obj_id is not None:
|
||||||
@@ -439,6 +450,14 @@ def open_for_edit(obj):
|
|||||||
initial_text = ""
|
initial_text = ""
|
||||||
try: initial_text = te.PlainText or ""
|
try: initial_text = te.PlainText or ""
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
# Runs aus UserString lesen (gespeichert beim letzten Save)
|
||||||
|
initial_runs = None
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
rj = obj.Attributes.GetUserString("dossier_text_runs")
|
||||||
|
if rj: initial_runs = json.loads(rj)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[TEXT-EDITOR] read runs:", ex)
|
||||||
|
|
||||||
# Frame aus dem Text-BBox ableiten (fuer Dialog-Positionierung)
|
# Frame aus dem Text-BBox ableiten (fuer Dialog-Positionierung)
|
||||||
p1 = te.Plane.Origin
|
p1 = te.Plane.Origin
|
||||||
@@ -461,7 +480,8 @@ def open_for_edit(obj):
|
|||||||
(origin, width, height, p1, p2),
|
(origin, width, height, p1, p2),
|
||||||
settings, fonts,
|
settings, fonts,
|
||||||
edit_obj_id=obj.Id,
|
edit_obj_id=obj.Id,
|
||||||
initial_text=initial_text)
|
initial_text=initial_text,
|
||||||
|
initial_runs=initial_runs)
|
||||||
sc.sticky["text_editor_bridge"] = bridge
|
sc.sticky["text_editor_bridge"] = bridge
|
||||||
|
|
||||||
form = panel_base.open_satellite_window(
|
form = panel_base.open_satellite_window(
|
||||||
|
|||||||
+55
-8
@@ -146,6 +146,40 @@ function htmlToRuns(rootEl) {
|
|||||||
return runs
|
return runs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Runs zurueck in HTML konvertieren — fuer das Re-Open eines bestehenden
|
||||||
|
// DOSSIER-Texts. Spiegelt die persistierten Format-Runs als <span> mit
|
||||||
|
// inline-style + Tags (b/i/u/sup/sub). Newlines werden zu <br>.
|
||||||
|
function runsToHtml(runs) {
|
||||||
|
if (!Array.isArray(runs)) return ''
|
||||||
|
const escape = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
const out = []
|
||||||
|
for (const r of runs) {
|
||||||
|
const raw = r.text || ''
|
||||||
|
if (!raw) continue
|
||||||
|
// Per Newline splitten — jede non-leere Section bekommt eigene span
|
||||||
|
const segs = raw.split('\n')
|
||||||
|
for (let i = 0; i < segs.length; i++) {
|
||||||
|
if (i > 0) out.push('<br>')
|
||||||
|
const seg = segs[i]
|
||||||
|
if (!seg) continue
|
||||||
|
const styles = []
|
||||||
|
if (r.font) styles.push(`font-family: ${r.font}`)
|
||||||
|
if (r.fontSizePx) styles.push(`font-size: ${r.fontSizePx}px`)
|
||||||
|
if (r.color) styles.push(`color: ${r.color}`)
|
||||||
|
let opens = '', closes = ''
|
||||||
|
if (r.bold) { opens += '<b>'; closes = '</b>' + closes }
|
||||||
|
if (r.italic) { opens += '<i>'; closes = '</i>' + closes }
|
||||||
|
if (r.underline) { opens += '<u>'; closes = '</u>' + closes }
|
||||||
|
if (r.sup) { opens += '<sup>'; closes = '</sup>' + closes }
|
||||||
|
else if (r.sub) { opens += '<sub>'; closes = '</sub>' + closes }
|
||||||
|
const inner = opens + escape(seg) + closes
|
||||||
|
if (styles.length) out.push(`<span style="${styles.join('; ')}">${inner}</span>`)
|
||||||
|
else out.push(inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out.join('')
|
||||||
|
}
|
||||||
|
|
||||||
function SymbolPopover({ open, onClose, onPick }) {
|
function SymbolPopover({ open, onClose, onPick }) {
|
||||||
if (!open) return null
|
if (!open) return null
|
||||||
return (
|
return (
|
||||||
@@ -290,11 +324,16 @@ export default function TextEditorApp() {
|
|||||||
if (s.italic != null) setItalic(!!s.italic)
|
if (s.italic != null) setItalic(!!s.italic)
|
||||||
if (s.underline != null) setUnderline(!!s.underline)
|
if (s.underline != null) setUnderline(!!s.underline)
|
||||||
if (s.align) setAlign(s.align)
|
if (s.align) setAlign(s.align)
|
||||||
// Bei Edit-Mode: bestehenden Text in den Editor laden
|
// Bei Edit-Mode: bestehenden Text in den Editor laden. Wenn Runs
|
||||||
|
// persistiert sind (= reicher Format-Stand vom letzten Save),
|
||||||
|
// diese als HTML laden — sonst PlainText fallback.
|
||||||
const initialText = data.initialText || ''
|
const initialText = data.initialText || ''
|
||||||
|
const initialRuns = data.initialRuns
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
if (initialText) {
|
if (initialRuns && initialRuns.length > 0) {
|
||||||
|
editorRef.current.innerHTML = runsToHtml(initialRuns)
|
||||||
|
} else if (initialText) {
|
||||||
editorRef.current.innerText = initialText
|
editorRef.current.innerText = initialText
|
||||||
}
|
}
|
||||||
editorRef.current.focus()
|
editorRef.current.focus()
|
||||||
@@ -335,8 +374,9 @@ export default function TextEditorApp() {
|
|||||||
exec(a === 'center' ? 'justifyCenter' : a === 'right' ? 'justifyRight' : 'justifyLeft') }
|
exec(a === 'center' ? 'justifyCenter' : a === 'right' ? 'justifyRight' : 'justifyLeft') }
|
||||||
const doSup = () => exec('superscript')
|
const doSup = () => exec('superscript')
|
||||||
const doSub = () => exec('subscript')
|
const doSub = () => exec('subscript')
|
||||||
// Stil anwenden: Toolbar-State setzen + (wenn Auswahl im Editor) via
|
// Stil anwenden: Toolbar-State setzen + Selection restoren + via
|
||||||
// execCommand auf die Selektion applizieren.
|
// execCommand applizieren (auf Selektion oder als Default fuer's
|
||||||
|
// kommende Tippen).
|
||||||
const applyStyle = (style) => {
|
const applyStyle = (style) => {
|
||||||
if (!style) return
|
if (!style) return
|
||||||
// Toolbar-State synchronisieren
|
// Toolbar-State synchronisieren
|
||||||
@@ -346,13 +386,20 @@ export default function TextEditorApp() {
|
|||||||
setItalic(!!style.italic)
|
setItalic(!!style.italic)
|
||||||
setUnderline(!!style.underline)
|
setUnderline(!!style.underline)
|
||||||
if (style.align) setAlign(style.align)
|
if (style.align) setAlign(style.align)
|
||||||
// Auf Selektion applizieren (oder zumindest fuer kommendes Tippen)
|
// Selection wiederherstellen + Editor-Focus damit execCommand greift.
|
||||||
|
editorRef.current?.focus()
|
||||||
|
restoreSelection()
|
||||||
try {
|
try {
|
||||||
const sel = window.getSelection()
|
|
||||||
const hasSel = sel && sel.rangeCount > 0 && sel.toString().length > 0
|
|
||||||
document.execCommand('styleWithCSS', false, true)
|
document.execCommand('styleWithCSS', false, true)
|
||||||
if (style.font) document.execCommand('fontName', false, style.font)
|
if (style.font) document.execCommand('fontName', false, style.font)
|
||||||
// Bold/Italic/Underline togglen, falls Selektion da ist oder nicht
|
// Size auf Selektion via Inline-Span (execCommand fontSize macht
|
||||||
|
// nur 1-7 Scale, kein px)
|
||||||
|
const sel = window.getSelection()
|
||||||
|
const hasSel = sel && sel.rangeCount > 0 && !sel.getRangeAt(0).collapsed
|
||||||
|
if (style.size != null && hasSel) {
|
||||||
|
applyInlineStyleToSelection('font-size', sizeToPx(style.size) + 'px')
|
||||||
|
}
|
||||||
|
// Bold/Italic/Underline togglen wenn nicht schon im Wunsch-State
|
||||||
const wantBold = !!style.bold
|
const wantBold = !!style.bold
|
||||||
const wantItal = !!style.italic
|
const wantItal = !!style.italic
|
||||||
const wantUnd = !!style.underline
|
const wantUnd = !!style.underline
|
||||||
|
|||||||
Reference in New Issue
Block a user