You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Fabric.js+React组合文本框编辑异常:位置偏移与报错修复需求

问题解决:Fabric.js + React 组内文本双击编辑与重组方案

问题根源分析

  1. 位置偏移:子元素使用originX: 'center'/originY: 'center'与Group默认的left/top原点坐标系冲突,导致组定位计算混乱。
  2. 错误抛出:
    • getRetinaScaling未定义:操作未绑定到Canvas的对象,或Canvas引用为空时执行了编辑/重组逻辑。
    • uniScaleKey未定义:组解散后子元素的缩放属性未正确继承,重组时未复用原始组的缩放配置。
  3. 编辑逻辑无效:双击时直接调用enterEditing()后立即exitEditing(),用户无法真正编辑文本。

分步解决方案

1. 修正子元素坐标系,解决位置偏移

将所有子元素的originX/originY统一改为left/top,与Group默认坐标系对齐,确保组定位准确:

const rectangle1 = new fabric.Rect({
  top: 0,
  left: 0,
  width: 40,
  height: 15,
  fill: 'black',
  originX: 'left', // 改为left
  originY: 'top', // 改为top
  stroke: 'white',
  strokeWidth: 2,
});

const rectangle2 = new fabric.Rect({
  top: 15,
  left: 0,
  width: 40,
  height: 15,
  fill: 'black',
  originX: 'left',
  originY: 'top',
  stroke: 'white',
  strokeWidth: 2,
});

const sensorName = new fabric.IText('Enter...', {
  fontSize: 10,
  fontWeight: 900,
  fill: '#ffffff',
  originX: 'left',
  originY: 'top',
  left: 10, // 手动调整到矩形中心
  top: 2.5,
  fontFamily: 'FuturaBT-Book',
  editable: true,
});

const sensorValue = new fabric.IText('Enter...', {
  fontSize: 10,
  fontWeight: 900,
  fill: '#ffffff',
  originX: 'left',
  originY: 'top',
  left: 10,
  top: 17.5, // 对应第二个矩形的中心
  fontFamily: 'FuturaBT-Book',
  editable: true,
});

2. 重构双击编辑逻辑

双击组时保存原始组属性,解散组并将子元素添加到Canvas,标记元素归属以便后续重组:

const doubleClickThreshold = 300;
let lastClickTime = 0;

group.on('mousedown', (options: fabric.IEvent) => {
  const currentTime = new Date().getTime();
  const isDoubleClick = currentTime - lastClickTime < doubleClickThreshold;
  lastClickTime = currentTime;

  if (isDoubleClick && group && group.type === 'group' && canvas) {
    // 保存组的核心属性
    const groupProps = {
      left: group.left,
      top: group.top,
      scaleX: group.scaleX,
      scaleY: group.scaleY,
      uuid: group[UUID],
    };

    // 给所有子元素标记组归属
    group.getObjects().forEach(item => {
      item.set('groupUUID', groupProps.uuid);
      item.set('originalGroupProps', groupProps);
    });

    // 解散组:移除组,添加子元素到画布
    canvas.remove(group);
    group.getObjects().forEach(item => canvas.add(item));

    // 自动聚焦第一个文本框(可选)
    const firstText = group.getObjects().find(item => item.type === 'i-text') as fabric.IText;
    if (firstText) {
      setTimeout(() => {
        firstText.enterEditing();
        firstText.hiddenTextarea?.focus();
      }, 0);
    }
  }
});

3. 实现编辑完成后的安全重组

编写handleTextEditingExited函数,当文本编辑完成后,自动找到同组元素并重组:

const handleTextEditingExited = (textObj: fabric.IText, canvas?: fabric.Canvas) => {
  if (!canvas) return;

  const groupUUID = textObj.get('groupUUID');
  const originalProps = textObj.get('originalGroupProps');
  if (!groupUUID || !originalProps) return;

  // 筛选同组所有元素
  const groupItems = canvas.getObjects().filter(item => item.get('groupUUID') === groupUUID);
  if (groupItems.length !== 4) return; // 确保是原始的4个元素

  // 移除画布上的单个元素
  groupItems.forEach(item => canvas.remove(item));

  // 创建新组并复用原始属性
  const newGroup = new fabric.Group(groupItems, {
    left: originalProps.left,
    top: originalProps.top,
    scaleX: originalProps.scaleX,
    scaleY: originalProps.scaleY,
    [UUID]: originalProps.uuid,
  } as IGroupOptionsWithUUID);

  // 重新绑定双击事件
  let groupLastClickTime = 0;
  newGroup.on('mousedown', (options: fabric.IEvent) => {
    const currentTime = new Date().getTime();
    const isDoubleClick = currentTime - groupLastClickTime < doubleClickThreshold;
    groupLastClickTime = currentTime;

    if (isDoubleClick && newGroup && newGroup.type === 'group' && canvas) {
      const newGroupProps = {
        left: newGroup.left,
        top: newGroup.top,
        scaleX: newGroup.scaleX,
        scaleY: newGroup.scaleY,
        uuid: newGroup[UUID],
      };

      newGroup.getObjects().forEach(item => {
        item.set('groupUUID', newGroupProps.uuid);
        item.set('originalGroupProps', newGroupProps);
      });

      canvas.remove(newGroup);
      newGroup.getObjects().forEach(item => canvas.add(item));

      const firstText = newGroup.getObjects().find(item => item.type === 'i-text') as fabric.IText;
      if (firstText) {
        setTimeout(() => {
          firstText.enterEditing();
          firstText.hiddenTextarea?.focus();
        }, 0);
      }
    }
  });

  // 给新组内的文本绑定编辑退出事件
  newGroup.getObjects().forEach(item => {
    if (item.type === 'i-text') {
      const textItem = item as fabric.IText;
      textItem.off('editing:exited'); // 清除旧事件
      textItem.on('editing:exited', () => handleTextEditingExited(textItem, canvas));
    }
  });

  // 添加新组到画布并刷新
  canvas.add(newGroup);
  canvas.renderAll();
};

4. 修正事件绑定逻辑

移除原始代码中直接绑定到sensorName/sensorValue的事件,改为在组创建后统一绑定:

// 替换原来的两个editing:exited绑定
group.getObjects().forEach(item => {
  if (item.type === 'i-text') {
    const textItem = item as fabric.IText;
    textItem.on('editing:exited', () => handleTextEditingExited(textItem, canvas));
  }
});

关键注意事项

  • 确保所有操作都在canvas存在的前提下执行,避免空引用错误。
  • 重组时必须移除画布上的单个元素,再添加新组,避免元素重复。
  • 使用setTimeout延迟调用enterEditing(),确保元素已被添加到Canvas后再触发编辑状态。

内容的提问来源于stack exchange,提问作者Geonhee Lee

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 11:37:27