Whiteboard: PNG-Export Fix, Text-Input Mobile, Suche + Linkliste ergänzt; Kanban in Suche ergänzt
This commit is contained in:
@@ -1584,6 +1584,8 @@ const SEARCH_TOOL_INDEX = [
|
||||
{type:'tool',label:'Linkliste',icon:'🔗',sub:'Werkzeuge',id:'linkliste',keywords:['link','url','lesezeichen','bookmark','webseite']},
|
||||
{type:'tool',label:'Code-Schnipsel',icon:'📄',sub:'Werkzeuge',id:'codeschnipsel',keywords:['code','snippet','skript','programmierung','source']},
|
||||
{type:'tool',label:'CAD-Skizzen',icon:'📐',sub:'Werkzeuge',id:'skizze',keywords:['cad','skizze','zeichnung','sketch']},
|
||||
{type:'tool',label:'Kanban',icon:'🗂',sub:'Werkzeuge',id:'kanban',keywords:['kanban','board','aufgaben','task','todo','karte','spalte','planung']},
|
||||
{type:'tool',label:'Whiteboard',icon:'🖊',sub:'Werkzeuge',id:'whiteboard',keywords:['whiteboard','zeichnen','canvas','board','draw','skizze','malen','zeichen']},
|
||||
{type:'tool',label:'Dev-Tools',icon:'🔧',sub:'Werkzeuge',id:'devtools',keywords:['entwickler','developer','tools','werkzeuge']},
|
||||
{type:'tool',label:'Paywall-Killer',icon:'🔓',sub:'Werkzeuge',id:'paywallkiller',keywords:['paywall','archiv','archive','artikel','zeitung','bild','waz','heise','spiegel','zeit','bypass','umgehen','lesen','gesperrt','bezahlschranke']},
|
||||
{type:'tool',label:'Media',icon:'🎬',sub:'Freizeit',id:'media',keywords:['kino','film','movie','streaming','demnächst','favoriten','cinema']},
|
||||
|
||||
@@ -10,6 +10,8 @@ const TOOL_ENTRIES = [
|
||||
{ url:'tool://bestellungen', label:'📦 Bestellungen', 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://whiteboard', label:'🖊 Whiteboard', group:'Werkzeuge' },
|
||||
{ url:'tool://dateien', label:'📁 Dateien', group:'Werkzeuge' },
|
||||
{ url:'tool://nachrichten', label:'💬 Nachrichten', group:'Werkzeuge' },
|
||||
{ url:'tool://linkliste', label:'🔗 Linkliste', group:'Werkzeuge' },
|
||||
|
||||
@@ -178,7 +178,7 @@ function renderElements(ctx, elements, vp, selectedId, forExport) {
|
||||
|
||||
// Offscreen-Export (ohne Auswahl-Rahmen, original Viewport)
|
||||
function exportToPng(elements, title) {
|
||||
// Bounding-Box aller Elemente berechnen
|
||||
if (!elements?.length) return; // Nichts zu exportieren
|
||||
let minX=Infinity,minY=Infinity,maxX=-Infinity,maxY=-Infinity;
|
||||
for (const el of elements) {
|
||||
const b = getBounds(el); if (!b) continue;
|
||||
@@ -194,7 +194,9 @@ function exportToPng(elements, title) {
|
||||
const a = document.createElement('a');
|
||||
a.href = offscreen.toDataURL('image/png');
|
||||
a.download = (title||'whiteboard').replace(/[^\w\s-]/g,'').trim()+'.png';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
||||
|
||||
// ── Share-Modal ───────────────────────────────────────────────────────────────
|
||||
@@ -681,8 +683,7 @@ export default function Whiteboard({ toast, mobile }) {
|
||||
</div>
|
||||
</div>
|
||||
<div style={{display:'flex',gap:4,flexShrink:0}}>
|
||||
<button onClick={e=>{e.stopPropagation(); exportToPng([], b.title);
|
||||
// Daten erst laden dann exportieren
|
||||
<button onClick={e=>{e.stopPropagation();
|
||||
api(`/tools/whiteboard/${b.id}/data`).then(d=>{
|
||||
const els=JSON.parse(typeof d.elements==='string'?d.elements:JSON.stringify(d.elements||[]));
|
||||
exportToPng(els,b.title);
|
||||
@@ -715,24 +716,54 @@ export default function Whiteboard({ toast, mobile }) {
|
||||
)}
|
||||
|
||||
{textInput&&(
|
||||
<div style={{position:'absolute',left:textInput.sx,top:textInput.sy,zIndex:100}}>
|
||||
<input autoFocus
|
||||
onKeyDown={e=>{
|
||||
if(e.key==='Enter'){
|
||||
const v=e.target.value;
|
||||
setTextInput(null);
|
||||
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))'}}>
|
||||
<div style={{fontSize:10,fontFamily:'monospace',color:'rgba(255,255,255,0.35)',marginBottom:6}}>
|
||||
TEXT EINGEBEN · Enter = bestätigen · Esc = abbrechen
|
||||
</div>
|
||||
<div style={{display:'flex',gap:8}}>
|
||||
<input autoFocus
|
||||
onKeyDown={e=>{
|
||||
if(e.key==='Enter'){
|
||||
const v=e.target.value; 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);
|
||||
}}
|
||||
placeholder="Text eingeben…"
|
||||
style={{...S.inp,flex:1,fontSize:14,color}}/>
|
||||
<button onMouseDown={e=>{
|
||||
e.preventDefault(); // Verhindert blur auf Input
|
||||
const inp=e.currentTarget.previousSibling;
|
||||
const v=inp?.value||''; 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);
|
||||
}}
|
||||
onBlur={e=>{
|
||||
const v=e.target.value;
|
||||
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:'transparent',border:'none',outline:'1px dashed rgba(255,255,255,0.4)',
|
||||
color,fontSize:FONT_SIZES[fontSize],fontFamily:'monospace',minWidth:100}}/>
|
||||
</div>
|
||||
}} style={S.btn('#4ECDC4',true)}>✓</button>
|
||||
<button onMouseDown={e=>{e.preventDefault();setTextInput(null);}} 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
|
||||
onKeyDown={e=>{
|
||||
if(e.key==='Enter'){
|
||||
const v=e.target.value; 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);
|
||||
}}
|
||||
onBlur={e=>{
|
||||
const v=e.target.value; 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',
|
||||
outline:`1px dashed ${color}`,borderRadius:4,padding:'2px 6px',
|
||||
color,fontSize:FONT_SIZES[fontSize],fontFamily:'monospace',minWidth:120}}/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* ── Topbar ───────────────────────────────────────────────────────── */}
|
||||
|
||||
Reference in New Issue
Block a user