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

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

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

剪贴板

我们在平时使用一些在线文档编辑器的时候,可能会好奇一个问题,为什么我能够直接把格式复制出来,而不仅仅是纯文本,甚至于说从浏览器中复制内容到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排版等等,整体来说还是比较有意思的,欢迎关注我并留意后续的文章。

Canvas简历编辑器-我的剪贴板里究竟有什么数据的更多相关文章

  1. [Android Pro] Android Support 包里究竟有什么

    reference to : http://www.2cto.com/kf/201411/350928.html 随着 Android 5.0 Lollipop 的发布,Android 又为我们提供了 ...

  2. 50代码HTML5 Canvas 3D 编辑器优雅搞定

    1024程序员节刚过,手痒想实现一个html的3d编辑器,看了three.js 同时还看了网上流传已久的<<基于 HTML5 Canvas 的简易 2D 3D 编辑器>>,都觉 ...

  3. 【转】Android Support 包里究竟有什么

    随着 Android 5.0 Lollipop 的发布,Android 又为我们提供了更多的支持包,但是我相信大部分开发者都同我之前一样不知道这些包里究竟有些什么东西,我们应该在什么时候使用它.现在, ...

  4. 基于NoCode构建简历编辑器

    基于NoCode构建简历编辑器 基于NoCode构建简历编辑器,要参加秋招了,因为各种模版用起来细节上并不是很满意,所以尝试做个简单的拖拽简历编辑器. 描述 Github | Resume DEMO ...

  5. 解剖SQLSERVER 第四篇 OrcaMDF里对dates类型数据的解析(译)

    解剖SQLSERVER 第四篇  OrcaMDF里对dates类型数据的解析(译) http://improve.dk/parsing-dates-in-orcamdf/ 在SQLSERVER里面有几 ...

  6. 解剖SQLSERVER 第五篇 OrcaMDF里读取Bits类型数据(译)

    解剖SQLSERVER 第五篇  OrcaMDF里读取Bits类型数据(译) http://improve.dk/reading-bits-in-orcamdf/ Bits类型的存储跟SQLSERVE ...

  7. django django中的HTML控件及参数传递方法 以及 HTML form 里的数据是怎么被包成http request 的?如何在浏览器里查看到这些数据?

    https://www.jb51.net/article/136738.htm django中的HTML控件及参数传递方法 下面小编就为大家分享一篇django中的HTML控件及参数传递方法,具有很好 ...

  8. canvas图形编辑器

    原文地址:http://jeffzhong.space/2017/11/02/drawboard/ 使用canvas进行开发项目,我们离不开各种线段,曲线,图形,但每次都必须用代码一步一步的实现.有没 ...

  9. FairyGUI编辑器的和unity里的Obj对应关系

    1.在FairyGUI官网上下载好unity的工程,用FairyGUI编辑器打开它的官方案例 2.在FairyGUI编辑器和Unity中,从一个最简单的示例"Bag"着手.     ...

  10. (已解决)富文本编辑器:使用layui的layedit怎么回显存放在数据库里的富文本数据(包含有图片base64码)?

    1. 背景 我把富文本内容从后台导入到前端,回显在layui的layedit里面. 2. 步骤 直接在<textarea></textarea>中间进行赋值(我用的是模板赋值) ...

随机推荐

  1. 简单c++构建第一人称

    本文内容为UE4.27的文档教程 GameMode确定 新建的项目会自动生成GameMode,如果有更改,而不是使用默认的GameMode类,就需要在引擎的设置中更改 角色的实现 前后左右移动 //前 ...

  2. 如何在Java中读取超过内存大小的文件

    读取文件内容,然后进行处理,在Java中我们通常利用 Files 类中的方法,将可以文件内容加载到内存,并流顺利地进行处理.但是,在一些场景下,我们需要处理的文件可能比我们机器所拥有的内存要大.此时, ...

  3. wordpress固定链接+宝塔nginx配置伪静态访问URL

    一.站点设置 打开站点设置,选择伪静态,选择wordpress 二.wordpress设置 打开wordpress后台,选择设置 --->固定链接 选择一个你喜欢的格式点击保存 之后打开你的文章 ...

  4. 【Java面试题】SpringBoot

    十.SpringBoot (66)SpringBoot 简介 Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,它的产⽣简化了框架的使⽤,所谓简化,是 ...

  5. MyBatis 简介、优缺点

    40)谈谈 MyBatis Mybatis 是一个半自动化的 ORM 框架,它对 jdbc 的操作数据库的过程进行封装,使得开发者只需要专注于 SQL 语句本身,而不用去关心注册驱动,创建 conne ...

  6. Spring源码 19 IOC getBean方法

    前面实现了 ClassPathXmlApplicationContext 的构造,接下来分析其调用的 getBean 方法. 以 getBean(UserDao.class) 为例. 1 Abstra ...

  7. HMS Core助力同程旅行,打造更贴心的用户出行体验

    作为中国在线旅行行业的创新者,同程旅行聚焦年轻.时尚.个性的消费群体,致力于为用户提供更便捷.聪明.安全的出行服务.近年来,同程旅行通过人工智能等创新科技的应用将平台原本的交易撮合角色转变为" ...

  8. 【实变函数】四、Lebesgue积分

    [实变函数]4. Lebesgue积分 本文介绍Lebesgue积分的定义,并给出积分的一些常用性质.注意Lebesgue积分的定义是从非负函数向一般函数扩展的,这依托于一般函数的分解\(f(x)=f ...

  9. 通过path在windows下临时修改python和pip路径 以便于配置环境只对当前命令行窗口生效

    配置前 在cmd命令行下输入新env的路径 path=D:\Miniconda2\envs\openmmlab\openmmlab;D:\Miniconda2\envs\openmmlab\openm ...

  10. HarmonyOS语言基础类库开发指南上线啦!

      语言基础类库提供哪些功能?多线程并发如何实现?TaskPool(任务池)和Worker在实现和使用场景上有何不同? 针对开发者关注的并发等语言基础类库的相关能力,我们在新推出的语言基础类库开发指南 ...