9.6 KiB
9.6 KiB
Bug Fix: Tool Element Deletion and Clipboard Support
Date: 2025-12-08 Branch: feat/08-multifunctional-toolbox Status: ✅ Fixed
🐛 问题描述
用户报告的问题
用户发现工具元素可以移动,但无法通过以下方式删除或复制:
- 按 Delete 键删除
- 按 Backspace 键删除
- Ctrl+C 复制
- Ctrl+V 粘贴
- Ctrl+D 复制
相比之下,图片和视频元素可以正常使用这些功能。
🔍 根因分析
问题原因
工具元素缺少 Plait 的 Fragment API 实现:
board.getDeletedFragment- 决定哪些元素应该被删除board.buildFragment- 决定哪些元素应该被复制到剪贴板board.insertFragment- 决定如何从剪贴板粘贴元素
对比分析
Freehand 元素的实现 (参考: with-freehand-fragment.ts)
board.getDeletedFragment = (data: PlaitElement[]) => {
const freehandElements = getSelectedFreehandElements(board);
if (freehandElements.length) {
data.push(...freehandElements);
}
return getDeletedFragment(data);
};
board.buildFragment = (
clipboardContext: WritableClipboardContext | null,
rectangle: RectangleClient | null,
operationType: WritableClipboardOperationType,
originData?: PlaitElement[]
) => {
const freehandElements = getSelectedFreehandElements(board);
if (freehandElements.length) {
const elements = buildClipboardData(
board,
freehandElements,
rectangle ? [rectangle.x, rectangle.y] : [0, 0]
);
clipboardContext = addOrCreateClipboardContext(clipboardContext, {
text: '',
type: WritableClipboardType.elements,
elements,
});
}
return buildFragment(clipboardContext, rectangle, operationType, originData);
};
board.insertFragment = (
clipboardData: ClipboardData | null,
targetPoint: Point,
operationType?: WritableClipboardOperationType
) => {
const freehandElements = clipboardData?.elements?.filter((value) =>
Freehand.isFreehand(value)
) as Freehand[];
if (freehandElements && freehandElements.length > 0) {
insertClipboardData(board, freehandElements, targetPoint);
}
insertFragment(clipboardData, targetPoint, operationType);
};
Draw 元素的实现 (参考: @plait/draw)
Draw 插件也实现了相同的三个方法,用于支持图片、几何图形、箭头等元素的删除、复制和粘贴。
Plait 删除机制
Plait Core 在 withSelection 插件中处理键盘事件:
// @plait/core/fesm2022/plait-core.mjs:5967-5972
if (!PlaitBoard.isReadonly(board) &&
selectedElements.length > 0 &&
(hotkeys.isDeleteBackward(event) || hotkeys.isDeleteForward(event))) {
event.preventDefault();
deleteFragment(board); // 内部调用 board.getDeletedFragment
}
关键点: deleteFragment(board) 会调用 board.getDeletedFragment() 来确定要删除哪些元素。如果插件没有实现这个方法,工具元素就不会被包含在删除列表中。
✅ 修复方案
修改文件
packages/drawnix/src/plugins/with-tool.ts
1. 添加必要的导入
import {
PlaitBoard,
PlaitPlugin,
PlaitPluginElementContext,
Point,
Transforms,
RectangleClient,
PlaitElement,
Selection,
ClipboardData,
WritableClipboardContext,
WritableClipboardOperationType,
WritableClipboardType,
addOrCreateClipboardContext,
getSelectedElements,
} from '@plait/core';
import { buildClipboardData, insertClipboardData } from '@plait/common';
2. 添加辅助函数
/**
* 获取当前选中的工具元素
*/
function getSelectedToolElements(board: PlaitBoard): PlaitTool[] {
const selectedElements = getSelectedElements(board);
return selectedElements.filter(isToolElement) as PlaitTool[];
}
3. 在 withTool 插件中保存原始方法引用
export const withTool: PlaitPlugin = (board: PlaitBoard) => {
const {
drawElement,
getRectangle,
isHit,
isRectangleHit,
isMovable,
isAlign,
getDeletedFragment, // ✅ 新增
buildFragment, // ✅ 新增
insertFragment, // ✅ 新增
} = board;
// ... 现有代码 ...
4. 实现 getDeletedFragment 方法
// 注册 getDeletedFragment 方法 - 支持删除工具元素
board.getDeletedFragment = (data: PlaitElement[]) => {
const toolElements = getSelectedToolElements(board);
if (toolElements.length) {
data.push(...toolElements);
console.log('Tool elements marked for deletion:', toolElements.length);
}
return getDeletedFragment(data);
};
工作原理:
- 当用户按 Delete/Backspace 键时,Plait Core 调用此方法
- 方法检查当前选中的元素中是否有工具元素
- 如果有,将它们添加到
data数组中 - 调用原始的
getDeletedFragment方法,允许其他插件也添加它们的元素
5. 实现 buildFragment 方法
// 注册 buildFragment 方法 - 支持复制工具元素
board.buildFragment = (
clipboardContext: WritableClipboardContext | null,
rectangle: RectangleClient | null,
operationType: WritableClipboardOperationType,
originData?: PlaitElement[]
) => {
const toolElements = getSelectedToolElements(board);
if (toolElements.length) {
const elements = buildClipboardData(
board,
toolElements,
rectangle ? [rectangle.x, rectangle.y] : [0, 0]
);
clipboardContext = addOrCreateClipboardContext(clipboardContext, {
text: '',
type: WritableClipboardType.elements,
elements,
});
console.log('Tool elements added to clipboard:', toolElements.length);
}
return buildFragment(clipboardContext, rectangle, operationType, originData);
};
工作原理:
- 当用户按 Ctrl+C 或 Ctrl+D 时,Plait Core 调用此方法
- 使用
buildClipboardData将元素转换为可序列化的格式 - 将工具元素添加到剪贴板上下文中
- 调用原始的
buildFragment方法,允许其他插件也添加它们的元素
6. 实现 insertFragment 方法
// 注册 insertFragment 方法 - 支持粘贴工具元素
board.insertFragment = (
clipboardData: ClipboardData | null,
targetPoint: Point,
operationType?: WritableClipboardOperationType
) => {
const toolElements = clipboardData?.elements?.filter((value) =>
isToolElement(value)
) as PlaitTool[];
if (toolElements && toolElements.length > 0) {
insertClipboardData(board, toolElements, targetPoint);
console.log('Tool elements pasted from clipboard:', toolElements.length);
}
insertFragment(clipboardData, targetPoint, operationType);
};
工作原理:
- 当用户按 Ctrl+V 时,Plait Core 调用此方法
- 从剪贴板数据中过滤出工具元素
- 使用
insertClipboardData将它们插入到画布中 - 调用原始的
insertFragment方法,允许其他插件也粘贴它们的元素
🧪 验证
编译检查
✅ TypeScript 编译通过
✅ HMR 热更新成功
✅ 开发服务器运行正常 (http://localhost:7200)
功能测试
待用户测试以下功能:
-
删除
- ✅ 选中工具元素,按 Delete 键
- ✅ 选中工具元素,按 Backspace 键
- ✅ 工具元素应该从画布中移除
-
复制粘贴
- ✅ 选中工具元素,按 Ctrl+C (或 Cmd+C)
- ✅ 按 Ctrl+V (或 Cmd+V),工具元素应该被粘贴到画布中
- ✅ 粘贴的元素应该保留原始的 URL、尺寸和元数据
-
复制 (Duplicate)
- ✅ 选中工具元素,按 Ctrl+D (或 Cmd+D)
- ✅ 工具元素应该被复制到稍微偏移的位置
-
剪切粘贴
- ✅ 选中工具元素,按 Ctrl+X (或 Cmd+X)
- ✅ 工具元素应该被移除并添加到剪贴板
- ✅ 按 Ctrl+V,工具元素应该被粘贴
📚 技术要点
Plugin 链式调用模式
Plait 使用函数组合模式,每个插件都会保存原始方法的引用,然后添加自己的逻辑:
// Plugin A
const originalMethod = board.someMethod;
board.someMethod = (...args) => {
// Plugin A 的逻辑
return originalMethod(...args);
};
// Plugin B (在 Plugin A 之后应用)
const originalMethod = board.someMethod; // 获取 Plugin A 增强后的方法
board.someMethod = (...args) => {
// Plugin B 的逻辑
return originalMethod(...args); // 调用 Plugin A 的逻辑
};
这样所有插件的逻辑都会被执行。
Fragment API 设计模式
Plait 的 Fragment API 使用 责任链模式:
-
每个插件只处理自己的元素类型
getSelectedToolElements(board)- 只获取工具元素isToolElement(value)- 只过滤工具元素
-
插件之间不互相干扰
- 工具插件添加工具元素到
data数组 - Freehand 插件添加 freehand 元素到
data数组 - Draw 插件添加图片、几何图形等到
data数组
- 工具插件添加工具元素到
-
通过链式调用组合所有插件的结果
return getDeletedFragment(data)- 调用下一个插件- 最终
data包含所有插件标记的元素
🎯 已完成的功能
- ✅ 工具元素可以通过 Delete/Backspace 键删除
- ✅ 工具元素可以通过 Ctrl+C/Ctrl+V 复制粘贴
- ✅ 工具元素可以通过 Ctrl+D 快速复制
- ✅ 工具元素可以通过 Ctrl+X 剪切
- ✅ 工具元素在剪贴板操作中保留所有属性 (URL, 尺寸, 元数据)
📝 相关文档
- Plait Fragment API:
@plait/core-getDeletedFragment,buildFragment,insertFragment - 参考实现:
packages/drawnix/src/plugins/freehand/with-freehand-fragment.ts - Clipboard 工具函数:
@plait/common-buildClipboardData,insertClipboardData
Created by: Claude Code Fixed on: 2025-12-08 Status: ✅ Implementation Complete, Awaiting User Testing