我们是钉钉的文档协同团队,我们在做一些很有意义的事情,其中之一就是自研的文字编辑器。为了把自研文字编辑器做好,我们调研了开源社区各种优秀编辑器,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 就能自动为用户生成新的段落。

时间再往前推 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

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

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

Why ContentEditable is Terrible

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

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

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

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

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

例如下面这段文本:

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 选区,用户也会看到不同的视觉选区:

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

例如,我们的 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 选区可以被映射为不同的视觉选区,也就会造成悬摆选区问题:

主流的编辑器架构

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

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

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

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

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

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


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

一起讨论技术

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

参考资料

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

js input 自动换行_深入Slate.js - 拯救 ContentEditble相关推荐

  1. js input 自动换行_矿用自动灭火装置水基型自动灭火装置原理国内分析研讨_搜狐汽车...

    山 东潍坊九通消防科技(九通长胜)是国内最早针对矿用车辆发动机舱自动灭火的要求研发设计的超细干粉.水基型自动灭火装置是当下国内解决矿用车辆(地表车辆.井下车辆)灭火的非常好的技术,目前在掘进机.凿岩台 ...

  2. node.js编写网页_为Node.js编写可扩展架构

    node.js编写网页 by Zafar Saleem 通过Zafar Saleem 为Node.js编写可扩展架构 (Writing Scalable Architecture For Nodejs ...

  3. js list操作_使用 Node.js 实现一个命令行 todo-list(1)- 基本功能

    功能介绍 为了熟悉 Node.js,使用 Node.js 制作一个命令行小工具,项目仓库:https://github.com/FuZhouJohn/node-todo,先来介绍一下功能: 添加任务: ...

  4. node.js中模块_在Node.js中需要模块:您需要知道的一切

    node.js中模块 by Samer Buna 通过Samer Buna 在Node.js中需要模块:您需要知道的一切 (Requiring modules in Node.js: Everythi ...

  5. Node.js Web开发_设置Node.js(1)

    电子书推荐 Multithreaded JavaScript: Concurrency Beyond the Event Loop Computers For Seniors For Dummies, ...

  6. vue.js 构建项目_使用Vue.js和AWS Amplify构建Chatbot

    vue.js 构建项目 Over the last few years, chatbots have exploded in popularity. It makes sense that busin ...

  7. js input点击事件_Vue.js的旅程,简单的todo实例「602」

    对vue没有一丝了解的朋友可以看我的文章601Vue.js的旅程,初步认识Vue.js「601」. 这回我们做一个网上很多练习用的todo实例. 更多文章请关注我的头条号. 一.我们开始吧,先链接vu ...

  8. js splice方法_我用JS刷LeetCode | Day 8

    如有兴趣,微信搜索「九零后重庆崽儿」,我们一起学前端. 删除排序数组中的重复项: 说明:现阶段的解题暂未考虑复杂度问题 首发地址: 我用JS刷LeetCode | Day 8​www.brandhua ...

  9. nw.js 调用驱动程序_使用NW.js创建照片发现应用程序(第2部分)

    nw.js 调用驱动程序 NW.js (formerly known as Node Webkit) is a framework for creating cross-platform deskto ...

最新文章

  1. 久坐 缺乏运动 消化能力 会减弱
  2. 记一次金士顿DT100 G3 32G修复
  3. NeHe教程Qt实现——lesson14
  4. pyspark subtract代码示例
  5. Vue2.0 脚手架代码详解
  6. 【提交PR】如何在 GitHub 提交第一个 pull request
  7. 设计师必收藏!!!让你灵感迸发的配色网站
  8. 执行mount挂载命令 报错:mount: you must specify the filesystem type
  9. 菜鸟学Struts2——Interceptors
  10. Python(二)JavaPython混合编程
  11. 小技巧:Win7屏保变梦幻桌面
  12. FIT2CLOUD飞致云旗下多云管理平台完成华为FusionCompute兼容性测试
  13. Xamarin.Forms学习之路——黑猫时钟App
  14. kong安装启动问题
  15. ctfshow web入门 nodejs 334-341(更新中)
  16. 百度地图绘制行政区边界
  17. 执行python manage.py makemigrations出现如下错误解决方法
  18. 大数据之Flume:Flume概述
  19. POJ3618 绝对值排序
  20. 金蝶EAS,后台代码查询科目余额,SQL查询科目余额

热门文章

  1. 数据库连接池_DataSource_数据源(简单介绍C3P0和Druid)
  2. 小米路由器4Q的设置
  3. 微软家庭服务器,微软公布Windows Server 2012版本方案,不再提供家庭服务器版
  4. 模块pdf2image.dll加载失败_Webpack 原理从前端模块化开始
  5. 上网登录窗不弹出_配置 Windows XP 正常上网(TLS HTTPS),连接到 NAS
  6. 调用打印机_涨知识|你不知道的关于打印机的打印过程和打印机驱动的那些事...
  7. opencv yuv保存本地_OpenCV-dlib-python3实现人脸戴墨镜和含Y的抖音效果
  8. C语言指针用得好犹如神助!这些使用技巧值得收藏
  9. mui 时间样式错乱_微信公众号素材样式中心在哪?公众号动态分割线怎么添加?...
  10. android 打印机蜂鸣器,CANON喷墨打印机 蜂鸣器响5声不打印的问题解决办法