js input 自动换行_深入Slate.js - 拯救 ContentEditble

fbb519ede4611b37b873e6185c67e639.png

我们是钉钉的文档协同团队,我们在做一些很有意义的事情,其中之一就是自研的文字编辑器。为了把自研文字编辑器做好,我们调研了开源社区各种优秀编辑器,Slate.js 是其中之一(实际上,自研文字编辑器前,我们就使用了很久的 Slate)。

我们团队的同学把对 Slate 的理解,写成了小册子,想通过连载的形式分享给你,下面是小册子的大纲及第 2 篇 - 「拯救 ContentEditble」。


TOC

  • 一行代码实现富文本编辑器
  • 拯救 ContentEditable
  • Slate.js 设计
  • HTML 中的富文本
  • Slate.js 中的富文本
  • 节点寻址
  • 附录 - 不可变数据
  • 附录 - Memorize
  • Slate.js 是怎么工作的
  • 大脑 - Controller
  • 指令系统
  • Operation
  • 插件体系
  • Normalize
  • Decoration
  • Annotation
  • 模型与视图的同步
  • Tiny Slate.js:实现一个 Mini Slate.js
  • 设计数据结构
  • 设计 Controller
  • 实现编辑器组件
  • Slate.js 生态现状
  • 兼容其他格式
  • 单元测试
  • 移动端编辑器
  • 挑战与变革
  • 难于完美的编辑器
  • 大跃进 - Slate.js 0.50
  • 附录 - 协同理论
  • OT 算法
  • 协同调度
  • 关于作者

在富文本编辑器出现之前,浏览器已经具备了「展示」富文本的能力,开发者可以通过编排 HTML 和 CSS,实现对字号,字色等样式控制。但对于用户输入,浏览器所提供的 <textarea /><input /> 都只允许用户输入「纯文本」,能力十分单薄。

因此,如果我们能够直接编辑 HTML 内容,也就具备了「编辑」富文本的能力。例如当我们选中文本 xxx 并按下加粗的快捷键后,若能生成 <b>xxx</b> 或者 <strong>xxx</strong> 这样的 HTML,就能看到被加粗的文本。

浏览器为 DOM 节点提供的 contentEditble 属性即能为节点赋予「编辑其 HTML 内容」的能力。

contentEditable:让节点可编辑

极早期的富文本编辑器实现中,为了处理换行,就要拦截用户的键盘事件,判断用户是按下了回车键后,就要为其创建一个新的段落(如生成一个 <p /> 节点),这可能是通过 document.createElement() 或者类似 API 实现的。我们可能认为在新世纪初,这样的实现方式也持续了很长一段时间,其实早在 2000 年下半年,微软的 Internet Explorer 5.5 便引入了 contentEditable 特性,顾名思义,让 IE 浏览器中的 DOM 节点的 HTML 可以被编辑。只需要为节点声明 contenteditabletrue,那么用户在这个节点按下回车后,IE 就能自动为用户生成新的段落。

943ffb9fc9b7e22d1967b48ee897c080.png

时间再往前推 3 年,IE 4 引入的 designMode 已经能让整个文档的 HTML 内容可以被编辑,contentEditable 所做的,是让开发者能够更细粒度地控制节点内容的可编辑性。但比较遗憾的是,当时微软发布的这个特性,除了一个简要的使用文档,没有对可编辑性的内在行为和实现做更多描述。

行为及实现规范的缺乏,导致该特性只能在了 IE 浏览器中使用。刚才我们提到的,在 contentEditable 节点下,用户敲击回车后,浏览器能为之产生一个新的段落,但应该产生什么样的段落呢?类似的问题却没有规范来约束。

WHATWG 成员 Anne van Kesteren 也在 2005 年发表了一篇名为 More on contenteditable 的博文,在其中列举了同样内容,但是结构不同的两个 contentEditable 节点:

<!-- 例子 1 -->
<div contenteditable>test</div><!-- 例子 2 -->
<div contenteditable><div>test</div>
</div>

当我们分别在这两个例子中敲入回车时,在 IE 5.5 中,前者生成的新段落是一个 <p> 元素,而后者生成的却是一个 <div>

因此,为了推动 HTML 在浏览器中可编辑性的应用,WHATWG 小组开始着力于 contentEditable 的规范制定。同年 7 月,Anne van Kesteren 撰写了第一版 contentEditable 的规范。最终,经过标准委员会的不断努力,终于形成了 HTML 5 中的 contentEditable 规范,这也是目前几乎所有浏览器所遵循的规范。

在规范中,定义了两个角色:

  • editing host:即正被编辑的 HTML 元素。如果某个 HTML 元素开启了 contentEditable 属性,那么这个元素就是一个 editing host;而如果 document 开启了 designMode,那么整个 HTML 文档下的元素都是 editing host。
  • editable:即可编辑元素。若 HTML 元素是 editing host 的子孙,那么它就可以被编辑,另外,可编辑元素的子孙也是可编辑的(除非这些子孙被声明了 contentEditable 为 false)。

document.execCommand :使用命令进行编辑

通过 contenteditabledesignMode 属性能让 HTML 内容能够被编辑,但是,它们所做的,仅仅是为节点或者文档开启 HTML 内容的编辑能力,IE 5.5 引入 contentEditable 特性时,所有的编辑行为都托管给了其自己处理,并没有对外暴露编辑相关的 API。直到 Firefox 3 问世,其不仅支持了 contentEditable,还配套了能够与可编辑元素进行互动的 API: document.execCommand

例如,我们想要对选中的文本加粗,就可以执行:

744dba0a49bf312f7c6aa435ea88fefb.png

但是,浏览器提供的 document.execCommand 并非无所不能,甚至还成为了文本编辑器的实现掣肘,不仅仅是支持的「指令有限」,就连同一个指令,各浏览器的「实现都有可能不同」。因此,更多的编辑功能仍然需要开发者进行事件劫持等操作才能实现。

Why ContentEditable is Terrible

自从 contenteditable 被 IE 引入后,用户在浏览器的撰写文档时,拥有了更强大的能力,各大浏览器厂商也纷纷跟进,但是经过十多年的发展,各个浏览器仍然难以战胜特性背后的复杂性,带来统一的实现。

几年前,Medium Editor 的开发者之一 Nick Santos 发表过一篇著名的博文:Why ContentEditable is Terrible?,我们不妨先回顾下这篇博文,一方面了解 contentEditable 的给编辑器造成的困扰,一方面也了解编辑器为此做出了怎样的应对。

视觉内容与实际内容的一对多关系

令用户看见的内容为「视觉内容」,视觉内容对应的 DOM 结构为 「实际内容」,在不同的浏览器中,虽然用户看到了同样的内容,但这些内容背后却对应了不同的 DOM 结构:

86c51fc822370f015888d1611a6f5a53.png
视觉内容与实际内容的一对多关系

例如下面这段文本:

The hobbit was a very well-to-do hobbit, and his name was Baggins.

在不同的浏览器中,有可能形成不同的 DOM 内容:

<strong><em>Baggins</em></strong>
<em><strong>Baggins</strong></em>
<em><strong>Bagg</strong><strong>ins</strong></em>
<em><strong>Bagg</strong></em><strong><em>ins</em></strong>

视觉选区与实际选区的多对多关系

选区的情况则更加糟糕,用户看到的选区,可能被映射为不同的 DOM 选区;而同一个 DOM 选区,用户也会看到不同的视觉选区:

35de1b6fe5d3b4fbd98cfb30bf2fd7cf.png
视觉选区与实际选区的多对多关系

例如,我们的 HTML 如果是:

his name was <strong><em>Baggins</em></strong>

用户看到的光标落在 Baggins 前面,这样的视觉选区,可以被不同的 DOM 选区表示(我们用 <cursor /> 表示光标):

his name was <cursor/><strong><em>Baggins</em></strong> his name was
<strong><cursor/><em>Baggins</em></strong> his name was
<strong><em><cursor/>Baggins</em></strong>

继续在光标位置插入字符 I,由于插入位置(DOM 选区)的不同,将形成不同的内容:

  • his name was IBaggins
  • his name was IBaggins
  • his name was IBaggins

假如我们的文本是:

The hobbit was a very well-to-do hobbit, and his name was Baggins.

well-to- 后面换行,用户看到的文本内容是:

The hobbit was a very well-to
do hobbit, and his name was Baggins.

换行后,DOM 选区则选中了「从第一行末尾到第二行开头的」,那么怎么将这个选区展示给用户呢?光标究竟应该落在第一行末尾,还是第二行开头呢?

DOM 选区可以被映射为不同的视觉选区,也就会造成悬摆选区问题:

b86f833bc7a781171f341b0e930d3fb4.png

主流的编辑器架构

由于 contentEditable 的不可靠,Medium Editor 在架构时,通过下面两个方式规避上面提到的问题:

  • 模型与视图分离:编辑器自定义视图无关的数据结构,视图的渲染不再由浏览器控制,而是由编辑器控制,从而满足「视觉与实际内容的一一映射」,避免在不同的浏览器中发散
  • 自定义指令:自定义编辑器的指令集,一方面能扩充编辑器能力,但更重要的一方面,是避免直接调用 document.execCommand 在不同浏览器形成不一致的结果

7f065bf657ac58e5acf055a29f772c69.png

目前,主流富文本编辑器的大多也采用了这样的架构,例如 CKEditor 5、ProseMirror、Draft.js、Slate.js 等。

接下来,我们将深入目前流行的 Slate.js ,更加细致地了解主流富文本编辑器的设计哲学和实现细节。通过阅读后续章节,你将认识到:

  • Web 富文本编辑器的内核模型是怎么设计的,为什么要这样设计
  • 编辑器的模型和视图之间是如何同步的
  • 编辑器是如何通过插件体系扩展能力的
  • 编辑器是怎么支持多人协同编辑的
  • 当前 Web 富文本编辑器面临的问题

最后,我们还会一起尝试造一个简化版的 Slate.js 来验证我们的学习成果。


如果你对协同文档技术感兴趣,也可以加入下面的群(钉钉/微信),和我们一同讨论。

22fd094d10565f007c099f058eec69b9.png
一起讨论技术

也欢迎关注本账号,我们每周都会更新~

参考资料

  • The WHATWG Blog
  • Why ContentEditable is Terrible?
  • Wiki - WYSIWYG

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

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

相关文章

printf 地址_C程序显示主机名和IP地址

查找本地计算机的主机名和IP地址的方法有很多。这是使用C程序查找主机名和IP地址的简单方法。我们将使用以下功能&#xff1a;gethostname() &#xff1a;gethostname函数检索本地计算机的标准主机名。gethostbyname() &#xff1a;gethostbyname函数从主机数据库中检索与主机名…

java 定义变量时 赋值与不赋值_探究Java中基本类型和部分包装类在声明变量时不赋值的情况下java给他们的默认赋值...

探究Java中基本类型和部分包装类在声明变量时不赋值的情况下java给他们的默认赋值当基本数据类型作为普通变量(八大基本类型&#xff1a; byte,char,boolean,short,int,long,float,double)只有开发人员对其进行初始化&#xff0c;java不会对其进行初始化&#xff0c;如果不初始…

java 字符串 移位_使用位运算、值交换等方式反转java字符串-共四种方法

在本文中&#xff0c;我们将向您展示几种在Java中将String类型的字符串字母倒序的几种方法。StringBuilder(str).reverse()char[]循环与值交换byte循环与值交换apache-commons-lang3如果是为了进行开发&#xff0c;请选择StringBuilder(str).reverse()API。出于学习的目的&…

xstream xml模板_XStream – XStreamely使用Java中的XML数据的简便方法

xstream xml模板有时候&#xff0c;我们不得不处理XML数据。 而且大多数时候&#xff0c;这不是我们一生中最快乐的一天。 甚至有一个术语“ XML地狱”描述了程序员必须处理许多难以理解的XML配置文件时的情况。 但是&#xff0c;不管喜欢与否&#xff0c;有时我们别无选择&…

python知识点智能问答_基于知识图谱的智能问答机器人

研究背景及意义 智能问答是计算机与人类以自然语言的形式进行交流的一种方式&#xff0c;是人工智能研究的一个分支。 知识图谱本质上是一种语义网络&#xff0c;其结点代表实体&#xff08;entity&#xff09;或者概念&#xff08;concept&#xff09;&#xff0c;边代表实体/…

java会了还学什么_java都学哪些内容?学完之后可以做哪些工作?

展开全部阶段一&#xff1a;揭开企业开发神秘面纱 (4周32313133353236313431303231363533e78988e69d8331333431336163)1) Web开发基础&#xff1a;HTML语言、JavaScript、CSS、DOM等2) Oracle数据库基础&#xff1a;安装、配置Oracle数据库&#xff0c;熟练掌握SQL语句3) 操作系…

Java中的RAII

资源获取即初始化&#xff08; RAII &#xff09;是Bjarne Stroustrup用C 引入的一种用于异常安全资源管理的设计思想。 感谢垃圾回收&#xff0c;Java 没有此功能&#xff0c;但是我们可以使用try-with-resources实现类似的功能。 约翰哈德斯&#xff08;John Huddles&#x…

eclipse juno_Eclipse Juno上带有GlassFish的JavaEE 7

eclipse junoJava EE 7很热。 前四个JSR最近通过了最终批准选票&#xff0c;与此同时GlassFish 4达到了升级版83。 如果您关注我的博客&#xff0c;那么您将了解NetBeans的大部分工作。 但是我确实认识到&#xff0c;那里还有其他IDE用户&#xff0c;他们也有权试用最新和最出色…

java 生成校验验证码_java 验证码生成与校验

java绘图相关类验证码工具类package dt2008.util;import javax.imageio.ImageIO;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.awt.*;import java.awt.image.BufferedImage;import java.io.IOException;import ja…

红黑树中nil结点_什么是红黑树?程序员面试必问!

点击上方java小组&#xff0c;选择“置顶公众号”优质文章&#xff0c;第一时间送达当在10亿数据中只需要进行10几次比较就能查找到目标时&#xff0c;不禁感叹编程之魅力&#xff01;人类之伟大呀&#xff01; —— 学红黑树有感。终于&#xff0c;在学习了几天的红黑树相关的…

杰克逊JSON解析错误-UnrecognizedPropertyException:无法识别的字段,未标记为可忽略[已解决]...

在解析从我们的一个RESTful Web服务接收到的JSON字符串时&#xff0c;我收到此错误“线程“ main”中的异常com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException&#xff1a;无法识别的字段“人”&#xff08;类Hello $ Person&#xff09;&#xff0c;不是标记…

mysql2008数据库配置_SQL Server 2008 R2 超详细安装图文教程

这篇文章主要介绍了SQL Server 2008 R2 超详细安装图文教程,需要的朋友可以参考下一、下载SQL Server 2008 R2安装文件二、将安装文件刻录成光盘或者用虚拟光驱加载&#xff0c;或者直接解压&#xff0c;打开安装文件&#xff0c;出现下面的界面安装SQL Server 2008 R2需要.NET…

hdfs读写流程_深度探索Hadoop分布式文件系统(HDFS)数据读取流程

一、开篇Hadoop分布式文件系统(HDFS)是Hadoop大数据生态最底层的数据存储设施。因其具备了海量数据分布式存储能力&#xff0c;针对不同批处理业务的大吞吐数据计算承载力&#xff0c;使其综合复杂度要远远高于其他数据存储系统。因此对Hadoop分布式文件系统(HDFS)的深入研究&a…

mysql隔离级别验证_MySQL事务隔离级别以及验证

查询初始数据开启A事务  并做更新操作再另一端 B开始另一个事务查询 事务级别设置为读未提查询到事务未提交的数据 a的count修改为3 但是没有提交2 第二个级别 读已提交 避免脏读问题 但是有不可重复读问题回滚数据 修改隔离级别 确保都是 读已提交级别客户端A客户端B验证 脏…

注意力机制可视化_目标跟踪中的(STAM)时空注意力机制

目标跟踪分为单目标跟踪和多目标跟踪&#xff0c;单目标跟踪较为简单&#xff0c;这里我们只讨论多目标跟踪。多目标跟踪的遮挡问题多目标跟踪时特别容易发生目标间的相互遮挡&#xff0c;从而导致严重的预测偏移问题&#xff0c;如下图所示&#xff1a;红色框的行人在和蓝色框…

js给标签添加属性和值_jquery节点属性

一.节点操作1.DOM内容节点操作&#xff1a;​ ①innerHTML属性&#xff1a;设置或获取文本的内容&#xff08;普通文本和标签&#xff09;。​ ②innerText属性&#xff1a;设置或获取文本的内容&#xff08;普通文本&#xff09;&#xff0c;存在兼容性问题。2.jQuery内容节点…

sci translate好用吗_228个学科分类对应12000+本SCI和SSCI期刊,总有你要的那款!

最近有很多小伙伴询问选刊的问题&#xff0c;而且都是非常具体的学科方向&#xff0c;我们的小编虽然非常热心且礼貌的回答“近期安排”&#xff0c;但其实我们也感觉到鸭梨山大:根据WOS最新一期&#xff08;2020/9/21&#xff09;名单公布&#xff0c;WOS目前总共收录了12266本…

arrays.sort(._Arrays.sort与Arrays.parallelSort

arrays.sort(.我们都使用Arrays.sort对对象和原始数组进行排序。 此API在下面使用合并排序或Tim排序对内容进行排序&#xff0c;如下所示&#xff1a; public static void sort(Object[] a) {if (LegacyMergeSort.userRequested)legacyMergeSort(a);elseComparableTimSort.sor…

适用于Idea的面向现代TDD的Java 8 JUnit测试模板(带有Mockito和AssertJ)

使用类似BDD的语法&#xff0c;Java 8和Mockito-AssertJ二重奏为Idea调整JUnit测试类模板。 本文涵盖的主题似乎很简单。 但是&#xff0c;根据我的培训师经验&#xff0c;我知道&#xff08;不幸的是&#xff09;这不是常见的做法。 因此&#xff0c;我决定写这篇简短的博客文…

Java编程字符逆序输出_用JAVA编写一程序:从键盘输入多个字符串到程序中,并将它们按逆序输出在屏幕上。...

展开全部代码如下&#xff1a;import java.util.Scanner;public class ScannerDemo{public static void main(String[] args) throws Exception{Scanner scannew Scanner(System.in);System.out.println("请输入内容&#xff1a;");String strscan.nextLine();char[]…