Text-Editor: V-Align + Mask-Type/Color + Case-Transform
Erweitert den Editor um die fehlenden Rhino-Text-Optionen: - Vertical Alignment (Top/Middle/Bottom) als 3 Pill-Buttons in Row 2. Backend: text_create._apply_valign mit TextVerticalAlignment-Enum. - Mask-Type Dropdown (None/Viewport/Solid). Bei Solid erscheint ein Mask-Color-Picker. MaskMargin disabled wenn Type=none. Backend: schaltet te.MaskEnabled + te.MaskUsesViewportColor + te.MaskColor entsprechend. - Case-Dropdown in Row 2: upper/lower/capitalize/invert. Wirkt nur auf die Selektion via execCommand insertText. open_for_edit liest valign + maskType/Color/Margin aus bestehendem TextEntity zurueck, damit beim Re-Open der Zustand erhalten bleibt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+84
-5
@@ -247,6 +247,9 @@ export default function TextEditorApp() {
|
||||
const [frame, setFrame] = useState('none') // none | rect | capsule
|
||||
const [horizontalToView, setHorizontalToView] = useState(false)
|
||||
const [rotation, setRotation] = useState(0)
|
||||
const [valign, setVAlign] = useState('top') // top | middle | bottom
|
||||
const [maskType, setMaskType] = useState('none') // none | viewport | solid
|
||||
const [maskColor, setMaskColor] = useState([255, 255, 255])
|
||||
const [maskMargin, setMaskMargin] = useState(0)
|
||||
const [symbolsOpen, setSymbolsOpen] = useState(false)
|
||||
const [styles, setStyles] = useState([])
|
||||
@@ -324,6 +327,9 @@ export default function TextEditorApp() {
|
||||
if (s.italic != null) setItalic(!!s.italic)
|
||||
if (s.underline != null) setUnderline(!!s.underline)
|
||||
if (s.align) setAlign(s.align)
|
||||
if (s.valign) setVAlign(s.valign)
|
||||
if (s.maskType) setMaskType(s.maskType)
|
||||
if (Array.isArray(s.maskColor)) setMaskColor(s.maskColor)
|
||||
// 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.
|
||||
@@ -420,6 +426,34 @@ export default function TextEditorApp() {
|
||||
}
|
||||
const clearColor = () => { setColor(null); exec('foreColor', '#000000') }
|
||||
|
||||
const onMaskColorPick = (hex) => {
|
||||
const m = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i)
|
||||
if (m) setMaskColor([parseInt(m[1],16), parseInt(m[2],16), parseInt(m[3],16)])
|
||||
if (maskType === 'none') setMaskType('solid')
|
||||
}
|
||||
|
||||
// Case-Transformationen auf die aktuelle Selektion. Wenn nichts
|
||||
// markiert ist, no-op (kein All-Text-Modus, sonst zu invasiv).
|
||||
const transformCase = (mode) => {
|
||||
restoreSelection()
|
||||
const sel = window.getSelection()
|
||||
if (!sel || sel.rangeCount === 0) return
|
||||
const range = sel.getRangeAt(0)
|
||||
if (range.collapsed) return
|
||||
const src = range.toString()
|
||||
let out = src
|
||||
if (mode === 'upper') out = src.toUpperCase()
|
||||
else if (mode === 'lower') out = src.toLowerCase()
|
||||
else if (mode === 'capitalize') out = src.replace(/\w\S*/g, w =>
|
||||
w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
||||
else if (mode === 'invert') out = src.split('').map(ch =>
|
||||
ch === ch.toUpperCase() ? ch.toLowerCase() : ch.toUpperCase()).join('')
|
||||
document.execCommand('insertText', false, out)
|
||||
editorRef.current?.focus()
|
||||
}
|
||||
const rgbToHex = ([r, g, b]) => '#' +
|
||||
[r, g, b].map(n => Math.max(0, Math.min(255, n|0)).toString(16).padStart(2, '0')).join('')
|
||||
|
||||
const onCommit = () => {
|
||||
const el = editorRef.current
|
||||
if (!el) return
|
||||
@@ -432,8 +466,9 @@ export default function TextEditorApp() {
|
||||
text,
|
||||
runs,
|
||||
settings: {
|
||||
font, size, bold, italic, underline, align, color,
|
||||
frame, horizontalToView, rotation, maskMargin,
|
||||
font, size, bold, italic, underline, align, valign, color,
|
||||
frame, horizontalToView, rotation,
|
||||
maskType, maskColor, maskMargin,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -528,24 +563,68 @@ export default function TextEditorApp() {
|
||||
<Icon name="format_align_right" size={13} />
|
||||
</Pill>
|
||||
<div style={{ width: 6 }} />
|
||||
<Pill active={valign === 'top'} onMouseDown={onBtnMouseDown(() => setVAlign('top'))} title="Oben">
|
||||
<Icon name="vertical_align_top" size={13} />
|
||||
</Pill>
|
||||
<Pill active={valign === 'middle'} onMouseDown={onBtnMouseDown(() => setVAlign('middle'))} title="Mittig">
|
||||
<Icon name="vertical_align_center" size={13} />
|
||||
</Pill>
|
||||
<Pill active={valign === 'bottom'} onMouseDown={onBtnMouseDown(() => setVAlign('bottom'))} title="Unten">
|
||||
<Icon name="vertical_align_bottom" size={13} />
|
||||
</Pill>
|
||||
<div style={{ width: 6 }} />
|
||||
<Pill onMouseDown={onBtnMouseDown(doSup)} title="Hochstellen (x²)" style={{ fontFamily: 'serif' }}>x²</Pill>
|
||||
<Pill onMouseDown={onBtnMouseDown(doSub)} title="Tiefstellen (x₂)" style={{ fontFamily: 'serif' }}>x₂</Pill>
|
||||
<div style={{ width: 6 }} />
|
||||
<Dropdown value=""
|
||||
onChange={(v) => { if (v) transformCase(v) }}
|
||||
width={130}
|
||||
title="Selektion in Gross-/Kleinschrift wandeln"
|
||||
options={[
|
||||
<option key="" value="">— Aa —</option>,
|
||||
<option key="upper" value="upper">GROSSBUCHSTABEN</option>,
|
||||
<option key="lower" value="lower">kleinbuchstaben</option>,
|
||||
<option key="capitalize" value="capitalize">Erste Buchstaben</option>,
|
||||
<option key="invert" value="invert">Umkehren</option>,
|
||||
]} />
|
||||
</div>
|
||||
|
||||
{/* Toolbar Row 3: Frame / Rotation / Mask / Horizontal-to-view / Symbole */}
|
||||
{/* Toolbar Row 3: Frame / Mask (Type/Color/Margin) / Rotation / Camera / Symbole */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
||||
<Dropdown value={frame} onChange={setFrame} width={130}
|
||||
title="Rahmen um den Text"
|
||||
options={FRAME_OPTIONS.map(o => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))} />
|
||||
<Dropdown value={maskType} onChange={setMaskType} width={120}
|
||||
title="Maske hinter dem Text (verdeckt Hintergrund)"
|
||||
options={[
|
||||
<option key="none" value="none">Keine Maske</option>,
|
||||
<option key="viewport" value="viewport">Viewport-Farbe</option>,
|
||||
<option key="solid" value="solid">Solide Farbe</option>,
|
||||
]} />
|
||||
{maskType === 'solid' && (
|
||||
<label style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
height: BAR_H + 2, padding: '0 8px',
|
||||
background: 'var(--bg-input)', border: '1px solid var(--border)',
|
||||
borderRadius: 999, cursor: 'pointer', flexShrink: 0,
|
||||
boxSizing: 'border-box',
|
||||
}} title="Mask-Farbe">
|
||||
<input type="color" value={rgbToHex(maskColor)}
|
||||
style={{ width: 16, height: 16, border: 'none', padding: 0, background: 'transparent' }}
|
||||
onChange={(e) => onMaskColorPick(e.target.value)} />
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>Farbe</span>
|
||||
</label>
|
||||
)}
|
||||
<div style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 4,
|
||||
height: BAR_H + 2, padding: '0 10px', boxSizing: 'border-box',
|
||||
background: 'var(--bg-input)', border: '1px solid var(--border)',
|
||||
borderRadius: 999, flexShrink: 0,
|
||||
}} title="Mask-Rand (m) — weisser Hintergrund hinter Text">
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>Mask</span>
|
||||
opacity: maskType === 'none' ? 0.5 : 1,
|
||||
}} title="Rand um den Text-Inhalt der mit-maskiert wird (m)">
|
||||
<span style={{ fontSize: 10, color: 'var(--text-muted)' }}>Rand</span>
|
||||
<input type="number" step="0.05" min="0"
|
||||
value={maskMargin}
|
||||
onChange={(e) => setMaskMargin(parseFloat(e.target.value) || 0)}
|
||||
|
||||
Reference in New Issue
Block a user