Canvas简历编辑器-我的剪贴板里究竟有什么数据

Canvas简历编辑器-我的剪贴板里究竟有什么数据

在这里我们先来聊聊我们究竟应该如何操作剪贴板,也就是我们在浏览器的复制粘贴事件,并且在此基础上聊聊我们在Canvas图形编辑器中应该如何控制焦点以及如何实现复制粘贴行为。

  • 在线编辑: https://windrunnermax.github.io/CanvasEditor
  • 开源地址: https://github.com/WindrunnerMax/CanvasEditor

关于Canvas简历编辑器项目的相关文章:

  • 社区老给我推Canvas,我也学习Canvas做了个简历编辑器
  • Canvas图形编辑器-数据结构与History(undo/redo)
  • Canvas图形编辑器-我的剪贴板里究竟有什么数据
  • Canvas简历编辑器-图形绘制与状态管理(轻量级DOM)
  • Canvas简历编辑器-Monorepo+Rspack工程实践

剪贴板

我们在平时使用一些在线文档编辑器的时候,可能会好奇一个问题,为什么我能够直接把格式复制出来,而不仅仅是纯文本,甚至于说从浏览器中复制内容到Office Word都可以保留格式,看起来是不是一件很神奇的事情,不过当我们了解到剪贴板的基本操作之后,就可以了解这其中的底层实现了。

说到剪贴板,我们可能以为我们复制的就是纯文本,当然显然光靠复制纯文本我们是做不到这一点的,所以实际上剪贴板是可以存储复杂内容的,那么在这里我们以Word为例,当我们从Word中复制文本时,其实际上是会在剪贴板中写入这么几个key值:

text/plain
text/html
text/rtf
image/png

看着text/plain是不是很眼熟,这明显就是我们常见的Content-Type或者称作MIME-Type,所以说我们是不是可以认为剪贴板是一个Record<string, string>的类型,但是别忽略了我们还有一个image/png类型,因为我们的剪贴板是可以复制文件的,所以我们常用的剪贴板类型就是Record<string, string | File>例如此时复制这段文字在剪贴板中就是如下内容

text/plain
例如此时复制这段文字在剪贴板中就是如下内容text/html
<meta charset='utf-8'><strong style="...">例如此时复制这段文字</strong><em style="...">在剪贴板中就是如下内容</em>

那么我们粘贴的时候就很明显了,我们只需要从剪贴板里读取内容就可以了,例如我们从语雀复制内容到飞书中,我们在语雀复制的时候会将text/plain以及text/html写入剪贴板,在粘贴到飞书的时候就可以首先检查是否有text/htmlkey,如果有的话就可以读取出来,并且将其解析成为飞书自己的私有格式,就可以通过剪贴板来保持内容格式粘贴到飞书了,如果没有text/html的话,就直接将text/plain的内容写到私有的JSON数据即可。

此外,我们还可以考虑到一个问题,在上边的例子中实际上我们是复制时需要将JSON转到HTML字符串,在粘贴时需要将HTML字符串转换为JSON,这都是需要进行序列化与反序列化的,是需要有性能消耗以及内容损失的,所以是不是能减少这部分消耗,那么当然是可以的,通常来说如果是在应用内直接直接粘贴的话,可以直接通过剪贴板的数据直接compose到当前的JSON即可,这样就可以更完整地保持内容以及减少对于HTML解析的消耗。例如在飞书中,会有docx/text的独立Clipboard Key以及data-lark-record-data作为独立JSON数据源。

那么至此我们已经了解到剪贴板的工作原理,紧接着我们就来聊一聊如何进行复制操作,说到复制我们可能通常会想到clipboard.js,如果需要兼容性比较高的话可以考虑,但是如果需要在现在浏览器中使用的话,则可以直接考虑使用HTML5规范的API完成,在浏览器中关于复制的API常用的有两种,分别是document.execCommand("copy")以及navigator.clipboard.write

对于document.execCommand("copy")来说,我们可以直接借助textarea + execCommand来执行写剪贴板的操作,在这里需要注意的是如果这个事件必须要是isTrusted的事件,也就是说这个事件必须要是用户触发的,例如点击事件、键盘事件等等,如果我们在打开页面后直接执行这段代码的话,则实际上是不会触发的。此外,如果在控制台执行这段代码的话,写入剪贴板是可行的,因为我们通常会用回车这个操作来执行代码,所以这个事件是isTrusted的。

const TEXT_PLAIN = "text/plain";const data = {"text/plain": "1", "text/html":"<div>1</div>"};
const textarea = document.createElement("textarea");
textarea.addEventListener("copy",event => {for (const [key, value] of Object.entries(data)) {event.clipboardData && event.clipboardData.setData(key, value);}event.stopPropagation();event.preventDefault();},true
);
textarea.style.position = "fixed";
textarea.style.left = "-999px";
textarea.style.top = "-999px";
textarea.value = data[TEXT_PLAIN];
document.body.appendChild(textarea);
textarea.select();
document.execCommand("copy");
document.body.removeChild(textarea);

对于navigator.clipboard来说,如果我们只写入纯文本的话是比较简单的,直接调用write方法即可,只不过需要注意Document is focused,也就是焦点需要在当前页面内。如果需要在剪贴板中写入其他的值,则需要ClipboardItem对象来写入Blob,在这里需要注意的是,FireFox只有Nightly中有定义,所以在这里需要判断下,如果不存在这个对象的话就需要走降级的复制,可以使用上述的document.execCommand API

const data = {"text/plain": "1", "text/html":"<div>1</div>"};
if (navigator.clipboard && window.ClipboardItem) {const dataItems = {};for (const [key, value] of Object.entries(data)) {const blob = new Blob([value], { type: key });dataItems[key] = blob;}navigator.clipboard.write([new ClipboardItem(dataItems)]);
}

紧接着我们可以聊下粘贴行为,在这里我们可以用onPaste事件以及navigator.clipboard.read方法,对于navigator.clipboard.read方法来说,我们可以直接读取并且打印即可,在这里需要注意的是需要Document is focused,所以这里我们需要在控制台延时几秒,然后将鼠标点击到页面上才可以正常打印,此外还有一个问题是打印的types并不完整,可能是必须要规范内的MIME Type才直接支持,自定义的key不支持。

navigator.clipboard.read().then(res => {for (const item of res) {const types = item.types;for (const type of types) {item.getType(type).then(data => {const reader = new FileReader();reader.readAsText(data, "utf-8");reader.onload = () => {console.info(type, reader.result);};});}}
});

针对onPaste事件,我们可以通过clipboardData获取更加完整的相关数据,我们可以获取比较完整的数据以及构造File数据,这里可以使用下面的代码直接在控制台执行,并且可以将内容粘贴到其中,这样就可以打印出当前剪贴板的内容了。

const input = document.createElement("input");
input.style.position = "fixed";
input.style.top = "100px";
input.style.right = "10px";
input.style.zIndex = "999999";
input.style.width = "200px";
input.placeholder = "Read Clipboard On Paste";
input.addEventListener("paste", event => {const clipboardData = event.clipboardData || window.clipboardData;for (const item of clipboardData.items) {console.log(`%c${item.type}`, "background-color: #165DFF; color: #fff; padding: 3px 5px;");console.log(item.kind === "file" ? item.getAsFile() : clipboardData.getData(item.type));}
});
document.body.appendChild(input);

Clipboard模块

上边我们已经了解到如何操作我们的剪贴板了,那么下面我们就需要将其应用在编辑器当中了,不过我们首先需要关注焦点问题,因为在编辑器中我们不能保证所有的焦点都是在编辑器Canvas上的,比如我弹出一个输入框输入画布大小的时候,也是可能会使用粘贴行为的,而如果此时进行粘贴是会触发document上的onPaste事件的,那么此时就有可能错误的将不应该粘贴的内容插入到剪贴板当中了,所以我们需要处理焦点,也就是说我们需要确定当前操作是在编辑器上的时候才触发Copy/Paste行为。

平时我做富文本相关的功能比较多,所以在实现画板的时候总想按照富文本的设计思路来实现,同样的因为之前也说过我们需要实现History以及在编辑面板富文本的能力,所以焦点就很重要,如果焦点不在画板上的时候如果按下Undo/Redo键画板是不应该响应的,所以现在就需要有一个状态来控制当前焦点是否在Canvas上,经过调研发现了两个方案,方案一是使用document.activeElement,但是Canvas是不会有焦点的,所以需要将tabIndex="-1"属性赋予Canvas元素,这样就可以通过activeElement拿到焦点状态了,方案二是在Canvas上方再覆盖一层div,通过pointerEvents: none来防止事件的鼠标指针事件,但是此时通过window.getSelection是可以拿到焦点元素的,此时只需要再判断焦点元素是不是设置的这个元素就可以了。

当焦点的问题解决之后,我们就可以直接进行剪贴板的读写了,这部分实现就比较简单了,在复制的时候需要注意到将内容序列化为JSON字符串,并且还要写入一个text/plain的占位符,这样可以让用户在其他地方粘贴的时候是有感知的,对于我们的编辑器自身而言是不需要感知的。

public static KEY = "SKETCHING_CLIPBOARD_KEY";
private copyFromCanvas = (e: ClipboardEvent, isCut = false) => {const clipboardData = e.clipboardData;if (clipboardData) {const ids = this.editor.selection.getActiveDeltaIds();if (ids.size === 0) return void 0;const data: Record<string, DeltaLike> = {};for (const id of ids) {const delta = this.editor.deltaSet.get(id);if (!delta) return void 0;data[id] = delta.toJSON();if (isCut) {const parentId = this.editor.state.getDeltaStateParentId(id);this.editor.state.apply(new Op(OP_TYPE.DELETE, { id, parentId }));}}const str = TSON.stringify(data);str && clipboardData.setData(Clipboard.KEY, str);clipboardData.setData("text/plain", "请在编辑器中粘贴");isCut && this.editor.canvas.mask.clearWithOp();e.stopPropagation();e.preventDefault();}
};

粘贴的这部分需要处理一个交互问题,用户肯定是希望在多选时也可以直接粘贴多个图形的,所以在此处我们需要处理好粘贴的位置,在这里我用的方法是取的所有选中图形的中点,在用户触发粘贴行为时将中点对齐到此时鼠标所在的位置,并且计算好偏移量应用到反序列化的图形上,这样就可以做到跟随用户的鼠标进行粘贴了,这里还有一点是需要替换掉粘贴图形的id,这是新的图形当然就需要有新的唯一标识符。

public static KEY = "SKETCHING_CLIPBOARD_KEY";
private onPaste = (e: ClipboardEvent) => {if (!this.editor.canvas.isActive()) return void 0;const clipboardData = e.clipboardData;if (clipboardData) {const str = clipboardData.getData(Clipboard.KEY);const data = str && TSON.parse<Record<string, DeltaLike>>(str);if (data) {let range: Range | null = null;Object.values(data).forEach(deltaLike => {const { x, y, width, height } = deltaLike;const current = Range.fromRect(x, y, width, height);range = range ? range.compose(current) : current;});const compose = range as unknown as Range;if (compose) {const center = compose.center();const cursor = this.editor.canvas.root.cursor;const { x, y } = center.diff(cursor);Object.values(data).forEach(deltaLike => {const id = getUniqueId();deltaLike.id = id;deltaLike.x = deltaLike.x + x;deltaLike.y = deltaLike.y + y;const delta = DeltaSet.create(deltaLike);delta &&this.editor.state.apply(new Op(OP_TYPE.INSERT, { delta, parentId: ROOT_DELTA }));});}}e.stopPropagation();e.preventDefault();}
};

最后

本文我们介绍总结了应该如何操作剪贴板,也就是我们在浏览器的复制粘贴行为,并且在此基础上聊到了在Canvas图形编辑器中的焦点问题以及如何实现复制粘贴行为,虽然暂时不涉及到Canvas本身,但是这都是作为编辑器本身的基础能力,也是通用的能力可以学习。针对于这个编辑器我们可以介绍的能力还有很多,整体来看会涉及到数据结构、History模块、复制粘贴模块、画布分层、事件管理、无限画布、按需绘制、性能优化、焦点控制、参考线、富文本、快捷键、层级控制、渲染顺序、事件模拟、PDF排版等等,整体来说还是比较有意思的,欢迎关注我并留意后续的文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/837374.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker 部署 Nginx 实现一个极简的 负载均衡

背景: Nginx是异步框架的网页服务器&#xff0c;其常用作反向代理(负载均衡器)。在一般的小项目中, 服务器不多, 如果不考虑使用服务注册与发现, 使用Nginx 可以容易实现负载均衡。 在特此写一个快速入门 Nginx 的技术贴, 使用 Docker 部署 Nginx, 实现一个极简的加权轮询负载均…

现在的原创内容博客 SEO 最好就选谷歌和必应!

当我们在国内讨论搜索引擎优化的时候&#xff0c;我们经常讨论的是百度 SEO&#xff0c;很少提及 Bing 搜索与 Google 搜索&#xff0c;但随着跨境电商的崛起&#xff0c;在国内做外贸 SEO 的小伙伴越来越多&#xff0c;有效的了解 Bing 搜索与 Google 搜索的优化规则是很有必要…

2024年武侯区建设企业科技创新平台申报范围条件、奖励标准和材料

一、申报对象 支持企业围绕数字健康、消费电子、新型材料等重点领域&#xff0c;布局建设一批重点实验室、创新中心、企业技术中心等高端研发平台&#xff0c;着力突破产业关键技术。实施产业链人才开源计划&#xff0c;支持链主企业为上下游关联配套企业提供技术与人才支持、…

(第17天)栈与队列理论基础

目录 栈栈的逻辑结构基于逻辑结构的特性 栈的底层实现 队列队列的逻辑结构基于逻辑结构的特性 队列的底层实现 总结 栈 栈的逻辑结构 栈是一种先入后出的结构。 基于逻辑结构的特性 栈中的元素必须遵循先入后出的规则&#xff0c;因此栈提供pop()、push()接口进行对元素的操作…

开源aodh学习小结

1 介绍 aodh是openstack监控服务&#xff08;Telemetry&#xff09;下的一个模块&#xff0c;telemetry下还有一个模块ceilometer OpenStack Docs: 2024.1 Administrator Guides Get Started on the Open Source Cloud Platform - OpenStack Telemetry - OpenStack 1.1 代码仓…

softmax函数与交叉熵损失详解

文章目录 一、softmax函数1.1 引入指数形式的优点1.2 引入指数形式的缺点 二、交叉熵损失函数2.1 交叉熵损失函数2.2 softmax与交叉熵损失 参考资料 一、softmax函数 softmax用于多分类过程中&#xff0c;它将多个神经元的输出&#xff0c;映射到&#xff08;0,1&#xff09;区…

【C++ 内存管理】深拷贝和浅拷贝你了解吗?

文章目录 1.深拷贝2.浅拷贝3.深拷贝和浅拷贝 1.深拷贝 &#x1f34e; 深拷⻉: 是对对象的完全独⽴复制&#xff0c;包括对象内部动态分配的资源。在深拷⻉中&#xff0c;不仅复制对象的值&#xff0c;还会复制对象所指向的堆上的数据。 特点&#xff1a; &#x1f427;① 复制对…

蓝桥杯-移动距离(最简单的写法)

X星球居民小区的楼房全是一样的&#xff0c;并且按矩阵样式排列。 其楼房的编号为 1,2,3…当排满一行时&#xff0c;从下一行相邻的楼往反方向排号。 比如&#xff1a;当小区排号宽度为 6 时&#xff0c;开始情形如下&#xff1a; 1 2 3 4 5 6 12 11 10 9 8 7 13 14 15 … 我…

程序设计语言理论中的范畴论及其简单应用

程序设计语言理论中的范畴论及其简单应用 范畴论是一个深奥的数学分支&#xff0c;近年来在程序设计语言理论中得到了广泛的应用。本文将简要介绍范畴论的基本概念&#xff0c;并通过简单示例来说明其在程序设计中的应用。 范畴论的基本概念 范畴&#xff08;Category&#…

Vue3:数据交互axios

回调函数 > 回调函数: 一些特殊的函数,表示未来才会执行的一些功能,后续代码不会等待该函数执行完毕就开始执行了 1. Promise 1.1 简介 > 前端中的异步编程技术&#xff0c;类似Java中的多线程线程结果回调&#xff01; * Promise 是异步编程的一种解决方案&#xff0c…

记录一下 log4j的漏洞

目录 背景 bug的产生 bug复现 JNDI 网络安全学习路线 &#xff08;2024最新整理&#xff09; 学习资料的推荐 1.视频教程 2.SRC技术文档&PDF书籍 3.大厂面试题 特别声明&#xff1a; 背景 log4j这次的bug&#xff0c;我相信大家都已经知道了&#xff0c;仅以…

网络安全软件堡垒机推荐行云管家云堡垒机!

随着互联网技术的快速发展&#xff0c;以及数字化转型的快速转变&#xff0c;网络安全已成为企业生存和发展的关键要素。网络安全不仅是国家等保要求&#xff0c;也是企业发展必须面对的挑战。目前市面上网络安全软件较多&#xff0c;这里我给推荐行云管家堡垒机&#xff01; …

【unity小技巧】减少Unity中的构建打包大小

文章目录 正常默认打包查看编辑器打包日志压缩图片压缩网格模型压缩贴图压缩音频文件只打64位包最终大小完结 正常默认打包 这里以安卓为例。先什么都不干&#xff0c;直接打包安卓apk&#xff0c;查看包大小 查看编辑器打包日志 搜索build report构建报告。构建报告我们应该…

Pytorch学习-引言

Pytorch相关链接 Pytorch官方网站 https://pytorch.org/ Pytorch的Github仓库 https://github.com/pytorch/pytorch Pytorch论坛 https://discuss.pytorch.org/ Pytorch离线下载包链接 https://download.pytorch.org/whl/torch_stable.html Pytorch学习视频推荐链接 http://【…

ubuntu 升级23.10 wifi固件缺失

昨晚家里ubuntu老机器23.04升级到23.10&#xff0c;出现wifi无法联网的故障&#xff0c;提示固件缺失。 查了不少资料&#xff0c;估计是要手工安装了&#xff0c;今天带跟网线回家&#xff0c;先要能上网啊。 经过几天折腾&#xff0c;我又从23.10升级到24.02 LTS版本&#…

手写一个SPI FLASH 读写擦除控制器

文章目录 flash读写数据的特点1. 扇擦除SE&#xff08;Sector Erase&#xff09;1.1 flash_se 模块设计1.1.1 信号连接示意图&#xff1a;1.1.2 SE状态机1.1.3 波形图设计&#xff1a;1.1.4 代码 2. 页写PP(Page Program)2.1 flash_pp模块设计2.1.1 信号连接示意图&#xff1a;…

apt结尾总是报错 ERROR: Timeout was reached

问题排查 尝试查看dpkg.log&#xff0c;重新安装异常的包&#xff0c;试了没用尝试清除apt缓存sudo apt clean sudo rm -rf /var/lib/apt/lists sudo apt update看到报错信息Failed to activate service org.freedesktop.PackageKit: timed out(service_start_timeout25000ms)…

学习前端第三十五天(原型继承,F.prototype,原生的原型)

一、原型继承 1、[ [ Prototype ] ]&#xff0c;对象属性 所有对象都有一个[ [ Prototype ] ] 当从object中读取一个缺失的属性时&#xff0c;JavaScript 会自动从原型中获取该属性&#xff0c;“原型继承”。 其中之一设置原型的方法&#xff0c;使用特殊的名字 __proto__&…

JavaScript 对象入门:基础用法全解析

目录 对象 语法 属性和访问 方法和调用 this关键字 null 遍历对象 内置对象 Math 属性 方法 Date 创建日期对象 获取和设置日期 ⭐对象 对象是 JavaScript 数据类型的一种&#xff0c;数据类型也包括数值类型、字符串类型、布尔类型、undefined。对象数据类型可…

mcu 常用宏定义展示

简介 MCU开发中一般有很多地方需要对寄存器进行操作。 寄存器操作 #define SET_BIT(REG, BIT) ((REG) | (BIT))#define CLEAR_BIT(REG, BIT) ((REG) & ~(BIT))#define READ_BIT(REG, BIT) ((REG) & (BIT))#define CLEAR_REG(REG) ((REG) (0x0))#de…