Whiteboard: Text Live-Vorschau beim Tippen auf Canvas; Linkliste: doppelter Kanban-Eintrag entfernt
This commit is contained in:
@@ -10,7 +10,6 @@ const TOOL_ENTRIES = [
|
|||||||
{ url:'tool://bestellungen', label:'📦 Bestellungen', group:'3D-Druck' },
|
{ url:'tool://bestellungen', label:'📦 Bestellungen', group:'3D-Druck' },
|
||||||
{ url:'tool://kalkulator3d', label:'🧮 Kostenrechner', group:'3D-Druck' },
|
{ url:'tool://kalkulator3d', label:'🧮 Kostenrechner', group:'3D-Druck' },
|
||||||
{ url:'tool://kanban', label:'🗂 Kanban', group:'Werkzeuge' },
|
{ url:'tool://kanban', label:'🗂 Kanban', group:'Werkzeuge' },
|
||||||
{ url:'tool://kanban', label:'🗂 Kanban', group:'Werkzeuge' },
|
|
||||||
{ url:'tool://whiteboard', label:'🖊 Whiteboard', group:'Werkzeuge' },
|
{ url:'tool://whiteboard', label:'🖊 Whiteboard', group:'Werkzeuge' },
|
||||||
{ url:'tool://dateien', label:'📁 Dateien', group:'Werkzeuge' },
|
{ url:'tool://dateien', label:'📁 Dateien', group:'Werkzeuge' },
|
||||||
{ url:'tool://nachrichten', label:'💬 Nachrichten', group:'Werkzeuge' },
|
{ url:'tool://nachrichten', label:'💬 Nachrichten', group:'Werkzeuge' },
|
||||||
|
|||||||
@@ -283,6 +283,7 @@ export default function Whiteboard({ toast, mobile }) {
|
|||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [shareModal,setShareModal]= useState(false);
|
const [shareModal,setShareModal]= useState(false);
|
||||||
const [textInput, setTextInput] = useState(null);
|
const [textInput, setTextInput] = useState(null);
|
||||||
|
const [textPreview, setTextPreview] = useState(null); // Live-Vorschau während Tippen
|
||||||
const [selectedId,setSelectedId]= useState(null);
|
const [selectedId,setSelectedId]= useState(null);
|
||||||
|
|
||||||
// Kollaboration
|
// Kollaboration
|
||||||
@@ -303,6 +304,7 @@ export default function Whiteboard({ toast, mobile }) {
|
|||||||
const vpRef = useRef({x:0,y:0,zoom:1});
|
const vpRef = useRef({x:0,y:0,zoom:1});
|
||||||
const elementsRef = useRef([]);
|
const elementsRef = useRef([]);
|
||||||
const selectedRef = useRef(null);
|
const selectedRef = useRef(null);
|
||||||
|
const textPreviewRef = useRef(null);
|
||||||
|
|
||||||
// ── Hilfsfunktionen ──────────────────────────────────────────────────────
|
// ── Hilfsfunktionen ──────────────────────────────────────────────────────
|
||||||
const syncVp = (vp) => { vpRef.current=vp; setViewport({...vp}); };
|
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.save(); ctx.fillStyle=c.color; ctx.font='10px monospace';
|
||||||
ctx.fillText('▶ '+c.username, sx+6, sy-4); ctx.restore();
|
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]);
|
useEffect(()=>{ render(); }, [elements, viewport, selectedId, collab, render]);
|
||||||
|
|
||||||
@@ -507,6 +527,8 @@ export default function Whiteboard({ toast, mobile }) {
|
|||||||
|
|
||||||
// Text
|
// Text
|
||||||
if (tool==='text') {
|
if (tool==='text') {
|
||||||
|
textPreviewRef.current = { text:'', x, y, fontSize:FONT_SIZES[fontSize], color };
|
||||||
|
render();
|
||||||
setTextInput({ sx, sy, x, y }); return;
|
setTextInput({ sx, sy, x, y }); return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,7 +739,6 @@ export default function Whiteboard({ toast, mobile }) {
|
|||||||
|
|
||||||
{textInput&&(
|
{textInput&&(
|
||||||
mobile ? (
|
mobile ? (
|
||||||
// Mobile: festes Panel unten, kein Keyboard-Verschiebe-Problem
|
|
||||||
<div style={{position:'fixed',bottom:0,left:0,right:0,zIndex:500,
|
<div style={{position:'fixed',bottom:0,left:0,right:0,zIndex:500,
|
||||||
background:'#1a1a1e',borderTop:'1px solid rgba(255,255,255,0.15)',
|
background:'#1a1a1e',borderTop:'1px solid rgba(255,255,255,0.15)',
|
||||||
padding:'12px 16px',paddingBottom:'calc(12px + env(safe-area-inset-bottom,0px))'}}>
|
padding:'12px 16px',paddingBottom:'calc(12px + env(safe-area-inset-bottom,0px))'}}>
|
||||||
@@ -726,37 +747,44 @@ export default function Whiteboard({ toast, mobile }) {
|
|||||||
</div>
|
</div>
|
||||||
<div style={{display:'flex',gap:8}}>
|
<div style={{display:'flex',gap:8}}>
|
||||||
<input autoFocus
|
<input autoFocus
|
||||||
|
onChange={e=>{
|
||||||
|
textPreviewRef.current={text:e.target.value,x:textInput.x,y:textInput.y,fontSize:FONT_SIZES[fontSize],color};
|
||||||
|
render();
|
||||||
|
}}
|
||||||
onKeyDown={e=>{
|
onKeyDown={e=>{
|
||||||
if(e.key==='Enter'){
|
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(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…"
|
placeholder="Text eingeben…"
|
||||||
style={{...S.inp,flex:1,fontSize:14,color}}/>
|
style={{...S.inp,flex:1,fontSize:14,color}}/>
|
||||||
<button onMouseDown={e=>{
|
<button onMouseDown={e=>{
|
||||||
e.preventDefault(); // Verhindert blur auf Input
|
e.preventDefault();
|
||||||
const inp=e.currentTarget.previousSibling;
|
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});
|
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>
|
}} 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>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Desktop: direkt am Klick-Punkt, Canvas-relative Position
|
|
||||||
<div style={{position:'absolute',left:textInput.sx,top:textInput.sy,zIndex:100,pointerEvents:'auto'}}>
|
<div style={{position:'absolute',left:textInput.sx,top:textInput.sy,zIndex:100,pointerEvents:'auto'}}>
|
||||||
<input autoFocus
|
<input autoFocus
|
||||||
|
onChange={e=>{
|
||||||
|
textPreviewRef.current={text:e.target.value,x:textInput.x,y:textInput.y,fontSize:FONT_SIZES[fontSize],color};
|
||||||
|
render();
|
||||||
|
}}
|
||||||
onKeyDown={e=>{
|
onKeyDown={e=>{
|
||||||
if(e.key==='Enter'){
|
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(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=>{
|
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});
|
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',
|
style={{background:'rgba(0,0,0,0.7)',border:'none',
|
||||||
|
|||||||
Reference in New Issue
Block a user