Fabric.js 6.x: Die benutzerdefinierte Löschsteuerung funktioniert nicht für programmgesteuert hinzugefügte Textbox-ObjekJavaScript

Javascript-Forum
Anonymous
 Fabric.js 6.x: Die benutzerdefinierte Löschsteuerung funktioniert nicht für programmgesteuert hinzugefügte Textbox-Objek

Post by Anonymous »

Ich verwende Fabric.js 6.x mit Vue 3 und stoße auf ein Problem, bei dem benutzerdefinierte Löschsteuerelemente für anfänglich hinzugefügte Textobjekte einwandfrei funktionieren, für programmgesteuert hinzugefügte Textobjekte jedoch fehlschlagen. Wenn Sie bei einem neu hinzugefügten Textobjekt auf die Schaltfläche „Löschen“ klicken, wird die Auswahl des Objekts aufgehoben, anstatt gelöscht zu werden.
Problembeschreibung
Ich habe ein benutzerdefiniertes Löschsteuerelement, das oben rechts bei ausgewählten Textobjekten angezeigt wird. Das Steuerelement funktioniert perfekt für Textobjekte, die während der Canvas-Initialisierung hinzugefügt werden. Wenn ich jedoch programmgesteuert neue Textobjekte hinzufüge und versuche, sie zu löschen, führt das Klicken auf die Schaltfläche „Löschen“ dazu, dass die Auswahl des Objekts aufgehoben wird und die Schaltfläche verschwindet, anstatt das Objekt zu löschen.
Codebeispiel
Hier ist ein minimal reproduzierbares Beispiel:

Code: Select all

import { Canvas, Textbox, Control, type FabricObject } from 'fabric';

const canvasRef = ref(null);
const canvas = ref(null);

// Delete icon as data URL
const deleteIcon = 'data:image/svg+xml,...'; // SVG data URL

let deleteImg = null;

onMounted(() => {
deleteImg = document.createElement('img');
deleteImg.src = deleteIcon;

canvas.value = new Canvas(canvasRef.value, {
width: 800,
height: 800,
});

// Initial text - DELETE WORKS ✅
const initialText = new Textbox('Hello, World!', {
left: 200,
top: 200,
width: 250,
fontSize: 40,
editable: true,
hasControls: true,
hasBorders: true,
});

canvas.value.add(initialText);
initialText.controls = initialText.controls || {};
initialText.controls.deleteControl = createDeleteControl();
initialText.setCoords();

// Event listener for selection
canvas.value.on('selection:created', () => {
const activeObject = canvas.value.getActiveObject();
if (activeObject && activeObject.type === 'textbox') {
activeObject.controls = activeObject.controls || {};
activeObject.controls.deleteControl = createDeleteControl();
activeObject.setCoords();
canvas.value.renderAll();
}
});
});

// Create delete control
const createDeleteControl = () => {
return new Control({
x: 0.5,
y: -0.5,
offsetY: -16,
offsetX: 16,
cursorStyle: 'pointer',
mouseUpHandler: deleteObject,
render: renderDeleteIcon,
});
};

// Delete handler
const deleteObject = (_eventData, transform) => {
const canvas = transform.target.canvas;
if (canvas) {
canvas.remove(transform.target);
canvas.discardActiveObject();
canvas.requestRenderAll();
}
};

// Render delete icon
const renderDeleteIcon = (ctx, left, top, _styleOverride, fabricObject) => {
if (!deleteImg) return;
const size = 24;
ctx.save();
ctx.translate(left, top);
ctx.rotate((fabricObject.angle || 0) * Math.PI / 180);
ctx.drawImage(deleteImg, -size / 2, -size / 2, size, size);
ctx.restore();
};

// Add new text programmatically - DELETE DOESN'T WORK ❌
const handleAddText = () =>  {
const newText = new Textbox('New Text', {
left: 400,
top: 400,
width: 250,
fontSize: 40,
editable: true,
hasControls: true,
hasBorders: true,
});

// Add to canvas
canvas.value.add(newText);

// Select it
canvas.value.setActiveObject(newText);

// Add delete control
newText.controls = newText.controls || {};
newText.controls.deleteControl = createDeleteControl();
newText.setCoords();

canvas.value.renderAll();
};
Erwartetes Verhalten
  • Durch Klicken auf die Schaltfläche „Löschen“ auf einem Textobjekt (ursprünglich oder programmgesteuert hinzugefügt) sollte das Objekt gelöscht werden.
  • Das Löschsteuerelement sollte unabhängig davon, wann das Objekt hinzugefügt wurde, funktionsfähig bleiben.
Tatsächliches Verhalten
  • Die Schaltfläche „Löschen“ funktioniert zunächst hinzugefügte Textobjekte ✅
  • Die Schaltfläche „Löschen“ bei programmgesteuert hinzugefügten Textobjekten führt zur Aufhebung der Auswahl statt zum Löschen ❌
  • Der MouseUpHandler im Löschsteuerelement scheint für programmgesteuert hinzugefügte Objekte nicht ausgelöst zu werden
  • Keine Konsolenfehler oder Warnungen
Was ich versucht habe
  • Vorgänge zur Neuordnung: Objekt zur Leinwand hinzugefügt → ausgewählt → Steuerelement hinzugefügt → Koordinaten festlegen → gerendert
  • Ereignis-Listener: Handler „selection:created“ und „selection:updated“ hinzugefügt, um Steuerelemente erneut hinzuzufügen
  • Verzögertes Hinzufügen von Steuerelementen: SetTimeout verwendet, um Steuerelemente hinzuzufügen, nachdem Auswahlereignisse ausgelöst wurden
  • Mehrere Renderaufrufe: Nach dem Hinzufügen von Steuerelementen wurde renderAll() mehrmals aufgerufen
  • Steuerelementüberprüfung: Überprüft, ob Steuerelemente ordnungsgemäß angehängt sind (sie befinden sich im Steuerelementobjekt)
  • Maus-Ereignishandler: MouseDownHandler wurde ausprobiert, um true zurückzugeben, um die standardmäßige Aufhebung der Auswahl zu verhindern
  • Ereignis auf Canvas-Ebene Abfangen: Canvas-Mouse:down-Handler hinzugefügt, um Klicks auf den Löschkontrollbereich zu erkennen
Umgebung
  • Fabric.js: 6.9.0
  • Vue: 3.x
  • Nuxt: 3.x
Zusätzliche Beobachtungen
  • Das Löschsteuerelement wird sowohl auf anfänglichen als auch auf neuen Textobjekten visuell korrekt gerendert
  • Das Steuerelement erscheint im Steuerobjekt des Objekts
  • Konsolenprotokolle zeigen, dass das Steuerelement mit den richtigen Handlern erstellt wurde
  • Das Problem scheint darin zu liegen, dass das Klickereignis auf dem Löschsteuerelement abgefangen wird oder der Handler nicht aufgerufen
  • Möglicherweise liegt ein Problem mit dem Z-Index oder der Ereignisweitergabe vor, aber ich bin mir nicht sicher, wie ich das in Fabric.js debuggen soll
Frage
Gibt es ein bekanntes Problem mit benutzerdefinierten Steuerelementen für programmgesteuert hinzugefügte Objekte in Fabric.js 6.x? Wie kann sichergestellt werden, dass benutzerdefinierte Steuerelemente für dynamisch hinzugefügte Objekte funktionieren? Gibt es Timing-Probleme oder fehlende Initialisierungsschritte?
Hier ist der vollständige Code:

Code: Select all





Canvas Item Controller





Add New Text



v-if="selectedTextObject"
class="space-y-4"
>

Selected Text





Font Color






Font Size: {{ selectedFontSize }}px



12px
72px





v-else
class="text-center text-gray-500 py-8"
>
Select a text object to edit





Debug Info



[h4]
Selected:
[/h4]
{{ selectedObjectInfo }}

[h4]
All Objects:
[/h4]
{{ allObjectsInfo }}
















import { Canvas, Textbox, FabricImage, Control, type FabricObject } from 'fabric';

const canvasRef = ref(null);
const canvas = ref(null);
const selectedObjectInfo = ref('None');
const allObjectsInfo = ref('[]');
const selectedTextObject = ref(null);
const selectedTextColor = ref('#000000');
const selectedFontSize = ref(40);

// Delete icon SVG
const deleteIcon = 'data:image/svg+xml,%3C%3Fxml version=\'1.0\' encoding=\'utf-8\'%3F%3E%3C!DOCTYPE svg PUBLIC \'-//W3C//DTD SVG 1.1//EN\' \'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\'%3E%3Csvg version=\'1.1\' id=\'Ebene_1\' xmlns=\'http://www.w3.org/2000/svg\' xmlns:xlink=\'http://www.w3.org/1999/xlink\' x=\'0px\' y=\'0px\' width=\'595.275px\' height=\'595.275px\' viewBox=\'200 215 230 470\' xml:space=\'preserve\'%3E%3Ccircle style=\'fill:%23F44336;\' cx=\'299.76\' cy=\'439.067\' r=\'218.516\'/%3E%3Cg%3E%3Crect x=\'267.162\' y=\'307.978\' transform=\'matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)\' style=\'fill:white;\' width=\'65.545\' height=\'262.18\'/%3E%3Crect x=\'266.988\' y=\'308.153\' transform=\'matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)\' style=\'fill:white;\' width=\'65.544\' height=\'262.179\'/%3E%3C/g%3E%3C/svg%3E';

let deleteImg: HTMLImageElement | null = null;

// Load delete icon image
onMounted(() => {
deleteImg = document.createElement('img');
deleteImg.src = deleteIcon;
});

// Delete object function
const deleteObject = (_eventData: unknown, transform: { target: FabricObject & { canvas?: Canvas } }) => {
const canvas = transform.target.canvas;
if (canvas) {
canvas.remove(transform.target);
canvas.discardActiveObject();
canvas.requestRenderAll();
updateDebugInfo();
updateSelectedText();
}
};

// Render delete icon
const renderDeleteIcon = (ctx: CanvasRenderingContext2D, left: number, top: number, _styleOverride: unknown, fabricObject: FabricObject) => {
if (!deleteImg) return;
const size = 24;
ctx.save();
ctx.translate(left, top);
ctx.rotate((fabricObject.angle || 0) * Math.PI / 180);
ctx.drawImage(deleteImg, -size / 2, -size / 2, size, size);
ctx.restore();
};

// Create delete control
const createDeleteControl = () => {
return new Control({
x: 0.5,
y: -0.5,
offsetY: -16,
offsetX: 16,
cursorStyle: 'pointer',
mouseUpHandler: deleteObject,
render: renderDeleteIcon,
});
};

const initCanvas = () => {
if (!canvasRef.value) return;

const fabricCanvas = new Canvas(canvasRef.value, {
width: 800,
height: 800,
backgroundColor: '#ffffff',
});

// Add text element using Textbox - width-based resizing, fontSize stays constant
const textObj = new Textbox('Hello, World!', {
left: 200,
top: 200,
width: 250,
fontSize: 40,
fontFamily: 'Arial',
fill: '#000000',
selectable: true,
editable: true, // Explicitly enable inline editing
hasControls: true,
hasBorders: true,
lockScalingX: false, // Allow horizontal scaling (width change)
lockScalingY: true, // Prevent vertical scaling - only width changes
lockRotation: false,
splitByGrapheme: true, // Ensures text wraps correctly
});

fabricCanvas.add(textObj);

// Add delete control after adding to canvas (preserve existing controls)
textObj.controls = textObj.controls || {};
textObj.controls.deleteControl = createDeleteControl();
textObj.setCoords();

// Add image element
FabricImage.fromURL('/test-img.png', { crossOrigin: 'anonymous' })
.then((img: FabricImage) =>  {
img.set({
left: 400,
top: 400,
scaleX: 0.3,
scaleY: 0.3,
selectable: true,
hasControls: true,
hasBorders: true,
lockScalingX: false,
lockScalingY: false,
lockRotation: false,
});
fabricCanvas.add(img);
fabricCanvas.renderAll();
updateDebugInfo();
})
.catch((error) => {
console.error('Failed to load image:', error);
});

// Track object changes
fabricCanvas.on('object:modified', () => {
updateDebugInfo();
});

fabricCanvas.on('object:moving', () => {
updateDebugInfo();
});

fabricCanvas.on('object:scaling', () => {
updateDebugInfo();
});

fabricCanvas.on('object:rotating', () => {
updateDebugInfo();
});

fabricCanvas.on('selection:created', () => {
// Add delete control to selected text objects
const activeObject = fabricCanvas.getActiveObject();
if (activeObject && activeObject.type === 'textbox') {
activeObject.controls = activeObject.controls || {};
activeObject.controls.deleteControl = createDeleteControl();
activeObject.setCoords();
fabricCanvas.renderAll();
}
updateSelectedText();
updateDebugInfo();
});

fabricCanvas.on('selection:updated', () => {
// Add delete control to selected text objects
const activeObject = fabricCanvas.getActiveObject();
if (activeObject && activeObject.type === 'textbox') {
activeObject.controls = activeObject.controls || {};
activeObject.controls.deleteControl = createDeleteControl();
activeObject.setCoords();
fabricCanvas.renderAll();
}
updateSelectedText();
updateDebugInfo();
});

fabricCanvas.on('selection:cleared', () => {
selectedTextObject.value = null;
updateDebugInfo();
});

canvas.value = fabricCanvas;
updateDebugInfo();
};

const handleAddText = () => {
if (!canvas.value) return;

const centerX = (canvas.value.width || 800) / 2;
const centerY = (canvas.value.height || 800) / 2;

const textObj = new Textbox('New Text', {
left: centerX,
top: centerY,
width: 250,
fontSize: 40,
fontFamily: 'Arial',
fill: '#000000',
selectable: true,
editable: true, // Explicitly enable inline editing
hasControls: true,
hasBorders: true,
lockScalingX: false, // Allow horizontal scaling (width change)
lockScalingY: true, // Prevent vertical scaling - only width changes
lockRotation: false,
splitByGrapheme: true, // Ensures text wraps correctly
});

// 1. Add to canvas first (Fabric.js initializes default controls)
canvas.value.add(textObj);

// 2. Then select it (ensures selection state is set)
canvas.value.setActiveObject(textObj);

// 3. Then add delete control (preserve existing controls)
textObj.controls = textObj.controls || {};
textObj.controls.deleteControl = createDeleteControl();

// 4.  Update coordinates after adding control
textObj.setCoords();

canvas.value.renderAll();
updateDebugInfo();
updateSelectedText();
};

const handleColorChange = (color: string) => {
if (!selectedTextObject.value) return;

selectedTextColor.value = color;
selectedTextObject.value.set('fill', color);
canvas.value?.renderAll();
updateDebugInfo();
};

const handleFontSizeChange = (size: number) => {
if (!selectedTextObject.value) return;

selectedFontSize.value = size;
selectedTextObject.value.set('fontSize', size);
canvas.value?.renderAll();
updateDebugInfo();
};

const updateSelectedText = () => {
if (!canvas.value) {
selectedTextObject.value = null;
return;
}

const activeObject = canvas.value.getActiveObject();
if (activeObject && (activeObject.type === 'textbox' || activeObject.type === 'i-text' || activeObject.type === 'text')) {
selectedTextObject.value = activeObject as Textbox;
selectedTextColor.value = selectedTextObject.value.fill as string || '#000000';
selectedFontSize.value = selectedTextObject.value.fontSize || 40;
}
else {
selectedTextObject.value = null;
}
};

const updateDebugInfo = () => {
if (!canvas.value) return;

const activeObject = canvas.value.getActiveObject();
const objects = canvas.value.getObjects();

// Update selected object info
if (activeObject) {
const obj = activeObject as FabricObject;
const isText = obj.type === 'textbox' || obj.type === 'i-text' || obj.type === 'text';
const textObj = isText ? obj as Textbox : null;

selectedObjectInfo.value = JSON.stringify({
type: obj.type,
left: obj.left,
top: obj.top,
width: 'width' in obj ? (obj as { width?: number }).width : undefined,
height: 'height' in obj ? (obj as { height?: number }).height : undefined,
scaleX: obj.scaleX,
scaleY: obj.scaleY,
angle: obj.angle,
...(textObj
? {
text: textObj.text,
fontSize: textObj.fontSize,
fontFamily: textObj.fontFamily,
fill: textObj.fill,
}
: {}),
}, null, 2);
}
else {
selectedObjectInfo.value = 'None';
}

// Update all objects info
allObjectsInfo.value = JSON.stringify(
objects.map((obj) => {
const fabricObj = obj as FabricObject;
const isText = fabricObj.type === 'textbox' || fabricObj.type === 'i-text' || fabricObj.type === 'text';
const textObj = isText ? fabricObj as Textbox : null;

return {
type: fabricObj.type,
left: fabricObj.left,
top: fabricObj.top,
width: 'width' in fabricObj ? (fabricObj as { width?: number }).width : undefined,
height: 'height' in fabricObj ? (fabricObj as { height?: number }).height : undefined,
scaleX: fabricObj.scaleX,
scaleY: fabricObj.scaleY,
angle: fabricObj.angle,
...(textObj
? {
text: textObj.text,
fontSize: textObj.fontSize,
}
: {}),
};
}),
null,
2,
);
};

onMounted(() => {
nextTick(() => {
initCanvas();
});
});

onUnmounted(() => {
if (canvas.value) {
canvas.value.dispose();
}
});

Quick Reply

Change Text Case: 
   
  • Similar Topics
    Replies
    Views
    Last post