一、 产品视角下复制粘贴需要解决的问题
- 复制粘贴时,需要静默复制(剪切板内不会看到复制的具体内容, 同miro)
- 统一自定义鼠标复制粘贴和键盘复制粘贴内容
- 实现外部内容也可以粘贴到内部
二、复制粘贴实现方式及分析
复制粘贴代码实现大概分为以下三种方式
- Document.execCommand 方法
- document.execCommand('copy') 复制命令
- document.execCommand('paste') 粘贴命令
- 缺点:
- safari 不支持
- 无法自定义头, 无法控制其写入剪切板是否可见
- 同步操作,大量复制可能出现一些卡顿问题
- 优点:
- 可以触发浏览器的 "copy事件"
- 异步的 Clipboard API navigator.clipboard
- navigator.clipboard.write 写入剪切板
- navigator.clipboard.read 读取剪切板
- 缺点:
- 只有安全模式可以使用,http模式无法支持
- 无法自定义头, 无法控制其写入剪切板是否可见
- 优点:
- 可以异步读写
- 设计更合理,使用方便
- 浏览器默认复制粘贴的copy、paste 事件。 e.clipboardData
- 事件对象的
clipboardData
属性包含了剪贴板数据。它是一个对象,有以下属性和方法。 - Event.clipboardData.setData(type, data) 修改剪贴板数据,需要指定数据类型
- Event.clipboardData.getData(type) 获取剪贴板数据,需要指定数据类型
- Event.clipboardData.clearData([type]) 清除剪贴板数据,可以指定数据类型
- 优点:
- 可以自定义数据类型, 自定义的数据类型只在当前应用内粘贴可见,可以有效保护复制内容
- 缺点: 只有原生浏览器事件才可以调用 e.clipboardData, 自定义的鼠标复制粘贴事件无法触发该事件, 但是可以通过 调用document.execCommand('copy') 间接的触发浏览器的onCopy事件,从而间接拿到e.clipboardData 对其进行自定义类型, 局限是safari无法触发, document.execCommand('paste') 无法触发
技术选择
由于第一种和第二种复制粘贴的方式无法自定义复制粘贴的数据类型, 无法解决上述产品视角的问题:
A. 无法设置自定义数据类型,剪切板内会泄漏元素信息
B. 不设置自定义数据类型, 用户复制我们系统中再去其他地方粘贴,会有大量匪夷所思的文本, 用户角度为乱码, 对用户造成理解困扰
三、 键盘Ctrl+C\Ctrl+V 实现思路
1、 监听document上的copy事件、paste事件。 在e.clipboardData 上获取剪切板内的信息, 分别处理外部粘贴,和内部元素粘贴, 将内部复制的元素设置自定义数据类型,粘贴时通过自定义数据类型获取 细节详见代码 onCopy、onPaste 方法
2、阻止默认事件
四、鼠标复制粘贴 实现
1、由于鼠标的复制粘贴为自定义的菜单。 无法拿到系统默认的event.clipboardData 无法设置自定义数据类型, 之前方案是鼠标单独维护了一套复制粘贴逻辑, 问题是无法统一鼠标和键盘的复制粘贴是两套单独的逻辑和结果。
2、其间为了解决鼠标的复制粘贴和键盘的复制粘贴两套的逻辑,分别尝试了,在鼠标复制阶段, 将代码写入浏览器缓存, 写入内存, 等多种方式, 遇到的最大问题是, 无法解决外部复制的内容, 与鼠标复制内部元素的时间先后顺序问题, 产生异常bug
3、幸运的是,通过调研,查看文档, 发现在鼠标调用document.execCommand('copy') 时, 会触发document的copy回调事件, 从而可以成功拿到event.clipboardData设置自定义数据类型, 从而统一鼠标和键盘的复制, 不幸的是, 该方法存在兼容性问题, 在safari 浏览器该事件还是无法直接使用。 而且直接脚本执行 document.execCommand('paste') 各浏览器也无法支持。 所以当前鼠标复制粘贴的方式是, 禁用鼠标右键粘贴, 在safari浏览器中, 禁用鼠标复制粘贴。
4、竞品 miro 中,同样遇到了此类问题,在safari浏览器中同样禁用了鼠标复制粘贴, 在其他浏览器中,禁用了鼠标粘贴
五、画板内复制粘贴实现细节
复制
- 1、 判断条件当前聚焦在画板内,并且target不是input
- 2、通过 canvas.getActiveObjects() 拿到选择的元素、判断要复制的元素不是箭头、文本框
- 3、遍历选中的元素 分别处理 连线 画板 细节 obj.toJSON() 序列化处理元素
- 4、写入剪切板,自定义写入类型 e.clipboardData.setData('teamind-copy-data', JSON.stringify(jsonArr))
- 5、组织onCopy默认事件 e.preventDefault()
粘贴
- 1、判断条件, 鼠标聚焦在当前画板上、非只读模式、非AreaSelection模式、target 不是多文本、input、noPasteUnput
- 2、处理 event.clipboardData.items 对象, 通过遍历 分别处理外部剪贴板内容 file 和 text、处理内部 teamind-copy-data 类型数据
- 3、处理文本: 创建一个Textbox canvas添加该元素双击选中、添加到撤销回退内、发送socket消息
- 4、处理file: 判断图片大小,文件名大小, 调用上传图片方法 uploadImage(file, mouseX, mouseY)
- 5、判断内部元素 调用方法 pasteObjArr(objArr, new fabric.Point(mouseX, mouseY))
六、补充
/** * @param {*} linkText 要复制的文本内容 * @param {*} callback 复制成功的回调方法 * 兼容问题 * navigator.clipboard 在不安全情况(非https)下无法使用,以及用户关闭浏览器权限 * document.execCommand('copy') 兜底方案 在safari一些版本无法正常使用 */ export function copyToClipboard(text: string): Promise<string> { return new Promise((resolve, reject) => { // 是否降级使用 const isFallback = !navigator.clipboard if (!isFallback) { navigator.clipboard .writeText(text) .then( () => { resolve(text) }, () => { copy(text) resolve(text) } ) .catch(() => { reject(text) }) } else { copy(text) resolve(text) } }) } // 降级复制 function copy(text: string) { const textarea = document.createElement('textarea') document.body.appendChild(textarea) // 隐藏此输入框 textarea.style.display = 'none' // 赋值 textarea.value = text // 选中 textarea.select() // 复制 document.execCommand('copy', true) // 移除输入框 document.body.removeChild(textarea) }