当前位置:首页 > 科技  > 软件

图形编辑器开发:实现图形的复制粘贴

来源: 责编: 时间:2023-09-28 10:09:24 267观看
导读大家好,我是前端西瓜哥。今天这篇文字来讲解一下图形编辑器如何实现图形的复制粘贴。粘贴的范围首先需要确认一下粘贴的范围。如果只支持粘贴到当前编辑器下,方案很简单:只需要监听 Ctrl + C 键盘事件深拷贝一份选中图形

y9k28资讯网——每日最新资讯28at.com

大家好,我是前端西瓜哥。y9k28资讯网——每日最新资讯28at.com

今天这篇文字来讲解一下图形编辑器如何实现图形的复制粘贴。y9k28资讯网——每日最新资讯28at.com

粘贴的范围

首先需要确认一下粘贴的范围。y9k28资讯网——每日最新资讯28at.com

如果只支持粘贴到当前编辑器下,方案很简单:只需要监听 Ctrl + C 键盘事件深拷贝一份选中图形对象,然后再监听 Ctrl + V 事件,将拷贝出来的对象添加到图形树的末尾。y9k28资讯网——每日最新资讯28at.com

但通常我们希望可以跨 tab 页,跨图纸,跨浏览器,甚至从 Web 端复制到桌面端。y9k28资讯网——每日最新资讯28at.com

很明显,要实现这样的场景,我们需要操作系统级的支持:剪贴板y9k28资讯网——每日最新资讯28at.com

我们看看怎么实现通过剪贴板实现图形的复制粘贴。y9k28资讯网——每日最新资讯28at.com

复制逻辑

先是复制逻辑。y9k28资讯网——每日最新资讯28at.com

复制通常为两种方式:y9k28资讯网——每日最新资讯28at.com

  • 快捷键  Ctrl/Cmd + C 。
  • 在选中的元素上方右键出现菜单选项。选中 “复制” 选项。

如下图:y9k28资讯网——每日最新资讯28at.com

y9k28资讯网——每日最新资讯28at.com

当调用复制命令时,我们要将 选中的图形生成序列化快照。y9k28资讯网——每日最新资讯28at.com

所谓序列化,就是将内存中的对象转换为可以持久化的数据。最简单快捷的就是用 JSON.stringify() 序列化为 JSON 字符串。y9k28资讯网——每日最新资讯28at.com

除了图形对象 data,我们还要保存一些必要的元信息。y9k28资讯网——每日最新资讯28at.com

最后我们要保存的信息有:y9k28资讯网——每日最新资讯28at.com

  • data:选中图形的数组(只有属性值)。
  • appVersion:编辑器版本。随着编辑器的迭代,图纸存储结构可能会发生变化,我们需要版本号来做兼容处理。
  • paperId:图纸 id,用来判断是否跨图纸粘贴。跨还是不跨图纸,粘贴策略有所不同,后面会说明。
/** * 生成选中图形的快照,并保存到操作系统剪贴板中 */const getSelectedItemsSnapshot() => {  const selectedItems = selectSet.getItems();  if (selectedItems.length === 0) {    return null;  }  // 提取图形原始属性,丢掉多余属性(比如 id)  const copiedData = arrMap(selectedItems, (item) =>    lodash.omit(item.getAttrs(), 'id'),  );  // 序列化  return JSON.stringify({    appVersion: this.editor.appVersion,    paperId: this.editor.paperId,    data: JSON.stringify(copiedData),  });}

拿到快照信息后,我们会调用 navigator.clipboard.writeText() 方法,将数据保存到操作系统的剪贴板中。y9k28资讯网——每日最新资讯28at.com

/** * 绑定 Ctrl/Cmd + C 的事件响应函数 */const copyHandler = () => {  const snapshot = getSelectedItemsSnapshot();  if (!snapshot) {    return;  }  // 将序列化结果保存到剪贴板  navigator.clipboard.writeText(snapshot).then(() => {    // 这里可以考虑加一个 “复制成功” 弹窗提示    console.log('copied');  });};hotkeys('cmd+c, ctrl+c', copyHandler);

y9k28资讯网——每日最新资讯28at.com

粘贴

然后就是粘贴了。y9k28资讯网——每日最新资讯28at.com

粘贴分为右键粘贴和快捷键粘贴。y9k28资讯网——每日最新资讯28at.com

右键粘贴

y9k28资讯网——每日最新资讯28at.com

这里的右键粘贴使用了 clipboard.readText() 方法。因为该方法不是用户的主动动作,涉及到用户隐私问题,所以需要用户授权剪贴板权限才行。y9k28资讯网——每日最新资讯28at.com

另外,Firefox 浏览器直接报错,不会弹出剪贴板授权弹窗。y9k28资讯网——每日最新资讯28at.com

这不是个技术问题,因为可以手动修改 Firefox 浏览器设置启用剪贴板授权。它更是一个安全问题,Firefox 不认为用户能够正确地授权粘贴板操作,以及开发者不会滥用这个权限收集用户隐私。y9k28资讯网——每日最新资讯28at.com

右键粘贴因为提供了光标位置,所以我们可以将图形的位置对上这个位置。y9k28资讯网——每日最新资讯28at.com

快捷键粘贴

y9k28资讯网——每日最新资讯28at.com

前面我们因为主动获取剪贴板的内容,所以有权限问题。y9k28资讯网——每日最新资讯28at.com

但如果我们监听用户的 “粘贴” 操作,权限就宽松了很多,不需要授权。y9k28资讯网——每日最新资讯28at.com

因为这是用户的主动行为,用户从剪贴板取出了数据交给你,而不是你主动去访问剪贴板的数据。y9k28资讯网——每日最新资讯28at.com

const pasteHandler = (e: Event) => {  const event = e as ClipboardEvent;  const clipboardData = event.clipboardData;  if (!clipboardData) {    return;  }  // 拿到粘贴的文本内容  const pastedData = clipboardData.getData('Text');  // ...};// 监听 “粘贴” 事件window.addEventListener('paste', pasteHandler);

如果用户拒绝授权,我们可以考虑提示用户 “用 Ctrl + C 的方式粘贴”,或者用用户上次右键粘贴的内容凑数,虽然可能货不对版,但好歹有个东西。y9k28资讯网——每日最新资讯28at.com

相同图纸下右键粘贴

快捷键粘贴没有光标操作,所以粘贴图形的位置需要用另一种方式去处理。y9k28资讯网——每日最新资讯28at.com

我们需要考虑两种情况:相同图纸和跨图纸。y9k28资讯网——每日最新资讯28at.com

对于在同一个图纸下快捷键粘贴,图形复制时在哪里,粘贴也在哪里。y9k28资讯网——每日最新资讯28at.com

或者你可以给一个小的右下偏移,让用户感知到粘贴成功了。我个人不喜欢这个偏移,因为通常我复制,就是为了让图形做重复对齐排列的,我还得给它移动回去。y9k28资讯网——每日最新资讯28at.com

在另一张图纸下右键粘贴

如果是在另一张图纸下粘贴,我们就不能这么做了。y9k28资讯网——每日最新资讯28at.com

为什么呢?y9k28资讯网——每日最新资讯28at.com

举个例子,假设用户复制了图纸 A 中在 (10000, 10000) 坐标的图形。然后我打开图纸 B,图纸 B 此时视口的中心坐标在 (0, 0)。y9k28资讯网——每日最新资讯28at.com

用户一粘贴,然后说,诶,粘贴的图形哪去了?你说我可以让视口移动到粘贴图形的位置,那用户会说,诶,我在哪里,我的其他图形哪去了?y9k28资讯网——每日最新资讯28at.com

所以 对于跨图纸场景,最好的做法是将图形粘贴到画布正中心。y9k28资讯网——每日最新资讯28at.com

y9k28资讯网——每日最新资讯28at.com

代码实现

代码逻辑有点多,就不文字叙述了,看代码里面的注释吧。y9k28资讯网——每日最新资讯28at.com

class ClipboardManager {  private unbindEvents = noop;  constructor(private editor: Editor) {}  bindEvents() {    // Ctrl+C 键盘事件响应函数    const copyHandler = () => {      this.copy();    };    // 粘贴事件响应函数    const pasteHandler = (e: Event) => {      const event = e as ClipboardEvent;      const clipboardData = event.clipboardData;      if (!clipboardData) {        return;      }      const pastedData = clipboardData.getData('Text');      this.addGraphsFromClipboard(pastedData);    };    hotkeys('cmd+c, ctrl+c', copyHandler);    window.addEventListener('paste', pasteHandler);    this.unbindEvents = () => {      hotkeys.unbind('cmd+c, ctrl+c', copyHandler);      window.removeEventListener('paste', pasteHandler);    };  }  /**   * 将快照保存到剪贴板   */  copy() {    const snapshot = this.getSelectedItemsSnapshot();    if (!snapshot) {      return;    }    navigator.clipboard.writeText(snapshot).then(() => {      console.log('copied');    });  }  pasteAt(x: number, y: number) {    navigator.clipboard.readText().then((pastedData) => {      this.addGraphsFromClipboard(pastedData, x, y);    });  }  /**   * 生成选中图形的快照(序列化)   */  private getSelectedItemsSnapshot() {    const selectedItems = this.editor.selectedElements.getItems();    if (selectedItems.length === 0) {      return null;    }    const copiedData = arrMap(selectedItems, (item) =>      omit(item.getAttrs(), 'id'),    );    return JSON.stringify({      appVersion: this.editor.appVersion,      paperId: this.editor.paperId,      data: JSON.stringify(copiedData),    });  }  // 在指定坐标位置粘贴内容  private addGraphsFromClipboard(dataStr: string): void;  private addGraphsFromClipboard(dataStr: string, x: number, y: number): void;  private addGraphsFromClipboard(dataStr: string, x?: number, y?: number) {    let pastedData: IEditorPaperData | null = null;    try {      // 反序列化      pastedData = JSON.parse(dataStr);    } catch (e) {      return;    }    // 数据格式校验    if (      !(        pastedData &&        pastedData.appVersion.startsWith('suika-editor') &&        pastedData.data      )    ) {      return;    }    const editor = this.editor;    // 将数据解析并添加到图形树中    const pastedGraphs = editor.sceneGraph.addGraphsByStr(pastedData.data);    if (pastedGraphs.length === 0) {      return;    }    // 添加到历史记录(以实现撤销重做)    editor.commandManager.pushCommand(      new AddShapeCommand('pasted graphs', editor, pastedGraphs),    );    // 标记粘贴图形为选中状态    editor.selectedElements.setItems(pastedGraphs);    const bbox = editor.selectedElements.getBBox()!;    // 如果是右键粘贴(x 和 y 没有值)且跨图纸粘贴,计算粘贴图形要移动的目标位置    if (      (x === undefined || y === undefined) &&      pastedData.paperId !== editor.paperId    ) {      const vwCenter = this.editor.viewportManager.getCenter();      x = vwCenter.x - bbox.width / 2;      y = vwCenter.y - bbox.height / 2;    }    // 遍历粘贴图形,根据 x 和 y 进行位置修正    if (x !== undefined && y !== undefined) {      const dx = x - bbox.x;      const dy = y - bbox.y;      if (dx || dy) {        Graph.dMove(pastedGraphs, dx, dy);      }    }    // 渲染画布    editor.sceneGraph.render();  }  // 销毁时解绑事件监听  destroy() {    this.unbindEvents();  }}

一些优化点

这里补充一些可以优化的点。y9k28资讯网——每日最新资讯28at.com

前面的实现其实有个用户体验不好的地方,就是用户复制后,在图形编辑器外粘贴,会粘贴出一堆意义不明的字符串。y9k28资讯网——每日最新资讯28at.com

最好是用户粘贴不出任何东西,这个有办法解决。y9k28资讯网——每日最新资讯28at.com

之前我们用的是 clipboard.writeText() 方法,给数据指定的是 text/plain 的 MIME 类型。y9k28资讯网——每日最新资讯28at.com

实际上我们可以用另一个方法  clipboard.write(),该方法可以指定其他的文本相关 MIME 类型,然后将我们真正的数据放到到一些不会被其他软件解析的角落里。y9k28资讯网——每日最新资讯28at.com

我们来看看隔壁 Figma 是怎么做的?它将复制的数据设置为 text/html 类型。y9k28资讯网——每日最新资讯28at.com

我再看看它的 HTML 都是什么内容。y9k28资讯网——每日最新资讯28at.com

y9k28资讯网——每日最新资讯28at.com

可以看到数据主要保存在两个 span 元素上,它们都没有文本内容,所以在文本编辑器中进行标准的粘贴是粘贴不出任何内容的。y9k28资讯网——每日最新资讯28at.com

但这里 Figma 巧妙地用了一个自定义的 data-metadata 和 data-buffer 去保存真正的数据。这个数据看着像是序列化后的类似 base64 格式的内容。y9k28资讯网——每日最新资讯28at.com

这样就能巧妙地防止其他文本编辑器能够粘贴出内容,自己的编辑器却会在解析 html 结构时特意去读这个自定义属性拿到数据。y9k28资讯网——每日最新资讯28at.com

代码实现大概为:y9k28资讯网——每日最新资讯28at.com

const blob = new Blob(  [    `<meta charset="utf-8">    <span data-suika-meta="${这里是元数据}"></span>    <span data-suika-data="${这里是主体数据}"><span>`,  ],  { type: 'text/html' },);navigator.clipboard  .write([new ClipboardItem({ [blob.type]: blob })])  .then(() => {    console.log('copied');  });

Firefox 目前(2023.08.06)不支持 ClipboardItem,需要 document.execCommand('copy') 的旧方法来兼容。y9k28资讯网——每日最新资讯28at.com

如果要用 text/html 这种方式,还要做多几个工作:y9k28资讯网——每日最新资讯28at.com

  1. 序列化结果要能放到 html 的属性值中,需要做一个转义;
  2. 粘贴读取 HTML 内容时,额外需要一个 HTML 解析器去解析,千万不要直接用原生的 DOM 去处理它们,会有完全问题,比如可能会有 script 脚本。这个解析器也不只可以解析复制的图形内容,还可以用作普通的解析 html 对应生成文本图形对象。

然后就是粘贴文字、图形的情况,这时我们就不能用 clipboard.writeText(),要用 clipboard.write() 了。y9k28资讯网——每日最新资讯28at.com

结尾

总结一下图形编辑器的图形复制粘贴的逻辑。y9k28资讯网——每日最新资讯28at.com

在复制时,要将选中图形进行序列化保存到剪贴板。y9k28资讯网——每日最新资讯28at.com

粘贴的场景就比较多了。粘贴时需要反序列化解析数据,并创建对象添加到图形树上。y9k28资讯网——每日最新资讯28at.com

粘贴要注意权限问题,快捷键粘贴权限比较宽松,不需要用户授权;右键粘贴则因为是开发者的主动行为,所以需要授权,如果用户不授权,可以考虑提示用户用快捷键粘贴的方式,或粘贴上一次快捷键粘贴的内容。y9k28资讯网——每日最新资讯28at.com

右键粘贴时需要将图形粘贴到光标位置上。快捷键粘贴时则需要考虑是否跨图纸,如果是相同图纸,原地粘贴即可;如果是另一张图纸,则粘贴到视口正中心。y9k28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-11894-0.html图形编辑器开发:实现图形的复制粘贴

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: 好用!这些工具国庆一定要研究下

下一篇: c#委托用法详解,你了解吗?

标签:
  • 热门焦点
Top