Whiteboard: Text Live-Vorschau beim Tippen auf Canvas; Linkliste: doppelter Kanban-Eintrag entfernt
This commit is contained in:
@@ -283,6 +283,7 @@ export default function Whiteboard({ toast, mobile }) {
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [shareModal,setShareModal]= useState(false);
|
||||
const [textInput, setTextInput] = useState(null);
|
||||
const [textPreview, setTextPreview] = useState(null); // Live-Vorschau während Tippen
|
||||
const [selectedId,setSelectedId]= useState(null);
|
||||
|
||||
// Kollaboration
|
||||
@@ -303,6 +304,7 @@ export default function Whiteboard({ toast, mobile }) {
|
||||
const vpRef = useRef({x:0,y:0,zoom:1});
|
||||
const elementsRef = useRef([]);
|
||||
const selectedRef = useRef(null);
|
||||
const textPreviewRef = useRef(null);
|
||||
|
||||
// ── Hilfsfunktionen ──────────────────────────────────────────────────────
|
||||
const syncVp = (vp) => { vpRef.current=vp; setViewport({...vp}); };
|
||||
@@ -463,7 +465,25 @@ export default function Whiteboard({ toast, mobile }) {
|
||||
ctx.save(); ctx.fillStyle=c.color; ctx.font='10px monospace';
|
||||
ctx.fillText('▶ '+c.username, sx+6, sy-4); ctx.restore();
|
||||
}
|
||||
}, [collab]);
|
||||
// Text-Vorschau während Tippen
|
||||
if (textPreviewRef.current) {
|
||||
const { text, x, y, fontSize, color } = textPreviewRef.current;
|
||||
const vp = vpRef.current;
|
||||
const sx = x * vp.zoom + vp.x;
|
||||
const sy = y * vp.zoom + vp.y;
|
||||
ctx.save();
|
||||
ctx.font = `${fontSize * vp.zoom}px monospace`;
|
||||
ctx.fillStyle = color;
|
||||
ctx.globalAlpha = 0.85;
|
||||
ctx.fillText(text || '|', sx, sy);
|
||||
// Cursor-Blinken simulieren: kleiner Strich hinter dem Text
|
||||
if (text) {
|
||||
const tw = ctx.measureText(text).width;
|
||||
ctx.fillRect(sx + tw + 1, sy - fontSize * vp.zoom * 0.8, 2, fontSize * vp.zoom);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
}, [collab, textPreview]);
|
||||
|
||||
useEffect(()=>{ render(); }, [elements, viewport, selectedId, collab, render]);
|
||||
|
||||
@@ -507,6 +527,8 @@ export default function Whiteboard({ toast, mobile }) {
|
||||
|
||||
// Text
|
||||
if (tool==='text') {
|
||||
textPreviewRef.current = { text:'', x, y, fontSize:FONT_SIZES[fontSize], color };
|
||||
render();
|
||||
setTextInput({ sx, sy, x, y }); return;
|
||||
}
|
||||
|
||||
@@ -717,7 +739,6 @@ export default function Whiteboard({ toast, mobile }) {
|
||||
|
||||
{textInput&&(
|
||||
mobile ? (
|
||||
// Mobile: festes Panel unten, kein Keyboard-Verschiebe-Problem
|
||||
<div style={{position:'fixed',bottom:0,left:0,right:0,zIndex:500,
|
||||
background:'#1a1a1e',borderTop:'1px solid rgba(255,255,255,0.15)',
|
||||
padding:'12px 16px',paddingBottom:'calc(12px + env(safe-area-inset-bottom,0px))'}}>
|
||||
@@ -726,37 +747,44 @@ export default function Whiteboard({ toast, mobile }) {
|
||||
</div>
|
||||
<div style={{display:'flex',gap:8}}>
|
||||
<input autoFocus
|
||||
onChange={e=>{
|
||||
textPreviewRef.current={text:e.target.value,x:textInput.x,y:textInput.y,fontSize:FONT_SIZES[fontSize],color};
|
||||
render();
|
||||
}}
|
||||
onKeyDown={e=>{
|
||||
if(e.key==='Enter'){
|
||||
const v=e.target.value; setTextInput(null);
|
||||
const v=e.target.value; textPreviewRef.current=null; setTextInput(null);
|
||||
if(v?.trim()) pushElement({id:getId(),type:'text',color,fontSize:FONT_SIZES[fontSize],text:v,x:textInput.x,y:textInput.y});
|
||||
}
|
||||
if(e.key==='Escape') setTextInput(null);
|
||||
if(e.key==='Escape'){ textPreviewRef.current=null; setTextInput(null); render(); }
|
||||
}}
|
||||
placeholder="Text eingeben…"
|
||||
style={{...S.inp,flex:1,fontSize:14,color}}/>
|
||||
<button onMouseDown={e=>{
|
||||
e.preventDefault(); // Verhindert blur auf Input
|
||||
e.preventDefault();
|
||||
const inp=e.currentTarget.previousSibling;
|
||||
const v=inp?.value||''; setTextInput(null);
|
||||
const v=inp?.value||''; textPreviewRef.current=null; setTextInput(null);
|
||||
if(v?.trim()) pushElement({id:getId(),type:'text',color,fontSize:FONT_SIZES[fontSize],text:v,x:textInput.x,y:textInput.y});
|
||||
}} style={S.btn('#4ECDC4',true)}>✓</button>
|
||||
<button onMouseDown={e=>{e.preventDefault();setTextInput(null);}} style={S.btn('#888888',true)}>✕</button>
|
||||
<button onMouseDown={e=>{e.preventDefault();textPreviewRef.current=null;setTextInput(null);render();}} style={S.btn('#888888',true)}>✕</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// Desktop: direkt am Klick-Punkt, Canvas-relative Position
|
||||
<div style={{position:'absolute',left:textInput.sx,top:textInput.sy,zIndex:100,pointerEvents:'auto'}}>
|
||||
<input autoFocus
|
||||
onChange={e=>{
|
||||
textPreviewRef.current={text:e.target.value,x:textInput.x,y:textInput.y,fontSize:FONT_SIZES[fontSize],color};
|
||||
render();
|
||||
}}
|
||||
onKeyDown={e=>{
|
||||
if(e.key==='Enter'){
|
||||
const v=e.target.value; setTextInput(null);
|
||||
const v=e.target.value; textPreviewRef.current=null; setTextInput(null);
|
||||
if(v?.trim()) pushElement({id:getId(),type:'text',color,fontSize:FONT_SIZES[fontSize],text:v,x:textInput.x,y:textInput.y});
|
||||
}
|
||||
if(e.key==='Escape') setTextInput(null);
|
||||
if(e.key==='Escape'){ textPreviewRef.current=null; setTextInput(null); render(); }
|
||||
}}
|
||||
onBlur={e=>{
|
||||
const v=e.target.value; setTextInput(null);
|
||||
const v=e.target.value; textPreviewRef.current=null; setTextInput(null);
|
||||
if(v?.trim()) pushElement({id:getId(),type:'text',color,fontSize:FONT_SIZES[fontSize],text:v,x:textInput.x,y:textInput.y});
|
||||
}}
|
||||
style={{background:'rgba(0,0,0,0.7)',border:'none',
|
||||
|
||||
Reference in New Issue
Block a user