Fabric.js 6.x: настраиваемый элемент управления удалением не работает для программно добавленных объектов Textbox — объеJavascript

Форум по Javascript
Ответить
Anonymous
 Fabric.js 6.x: настраиваемый элемент управления удалением не работает для программно добавленных объектов Textbox — объе

Сообщение Anonymous »

Я использую Fabric.js 6.x с Vue 3 и столкнулся с проблемой, когда пользовательские элементы управления удалением работают нормально для первоначально добавленных текстовых объектов, но не работают для текстовых объектов, добавленных программным путем. При нажатии кнопки удаления на вновь добавленном текстовом объекте выбор объекта снимается, а не удаляется.
Описание проблемы
У меня есть настраиваемый элемент управления удалением, который появляется в правом верхнем углу выбранных текстовых объектов. Этот элемент управления отлично работает для текстовых объектов, добавленных во время инициализации холста, но когда я программно добавляю новые текстовые объекты и пытаюсь их удалить, нажатие кнопки удаления приводит к отмене выбора объекта и исчезновению объекта, а не к его удалению.
Пример кода
Вот минимальный воспроизводимый пример:

Код: Выделить всё

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();
};
Ожидаемое поведение
  • Нажатие кнопки удаления на любом текстовом объекте (исходном или добавленном программно) должно удалить объект.
  • Элемент удаления должен оставаться работоспособным независимо от того, когда объект был добавлен.
Фактическое поведение
  • Кнопка «Удалить» работает для изначально добавленных текстовых объектов ✅
  • Кнопка «Удалить» на добавленных программно текстовых объектах вызывает отмену выбора вместо удаления ❌
  • MouseUpHandler в элементе управления удалением, похоже, не срабатывает для добавленных программно объектов
  • Нет ошибок или предупреждений консоли
Что я пробовал
  • Операции изменения порядка: добавлен объект на холст → выбран его → добавлен элемент управления → заданы координаты → отображен.
  • Прослушиватели событий: добавлены обработчики select:created и select:updated для повторного добавления элементов управления.
  • Отложенное добавление элемента управления: используется setTimeout для добавления элементов управления после срабатывания событий выбора.
  • Несколько вызовов рендеринга: вызывается renderAll() несколько раз после добавления элементов управления.
  • Проверка элементов управления: проверяется правильность прикрепления элементов управления (они находятся в объекте элементов управления).
  • Обработчики событий мыши: попробовал mouseDownHandler, возвращающий true, чтобы предотвратить отмену выбора по умолчанию.
  • Событие уровня холста. перехват: добавлен обработчик холста mouse:down для обнаружения кликов в области управления удалением.
Среда
  • Fabric.js: 6.9.0
  • Vue: 3.x
  • Nuxt: 3.x
Дополнительные наблюдения
  • Элемент управления удалением визуально отображается правильно как на исходном, так и на новом текстовых объектах.
  • Элемент управления появляется в элементах управления объекта. объект.
  • Журналы консоли показывают, что элемент управления создан с соответствующими обработчиками.
  • Похоже, проблема заключается в том, что событие щелчка элемента управления удалением перехватывается или обработчик не создается. называется
  • Возможна проблема с z-индексом или распространением событий, но я не знаю, как ее отладить в Fabric.js.
Вопрос
Существует ли известная проблема с настраиваемыми элементами управления для программно добавленных объектов в Fabric.js 6.x? Как правильно обеспечить работу пользовательских элементов управления для динамически добавляемых объектов? Есть ли какие-либо проблемы со временем или шаги инициализации, которые мне не хватает?
Вот полный код:

Код: Выделить всё





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();
}
});



Подробнее здесь: https://stackoverflow.com/questions/798 ... -added-tex
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Javascript»