译者注

我的上一篇译文 “[译] 通过 contentEditable 属性创建一个所见即所得的编辑器” 的原文 “Create a WYSIWYG Editor With the contentEditable Attribute” 被本文作者叼了一翻,说会误导吃瓜群众,让初学者误以为富文本编辑器很简单(见原文第一条评论),吓得我赶紧在译文头部郑重申明了一翻。

顺着评论过去看了下这篇文章,虽然有点 CKEditor 软文的嫌疑,但确实有些点值得思考下,遂译之。

不过 CKEditor 确实牛逼,搞了十几年了,编辑器里的老前辈。

本文对于 contentEditable 有点危言耸听,本人观点是基于实际需求出发,如果需要各种格式化功能、甚至是对 Excel/Word 内容的支持,那么使用 CKEditor 这类成熟的框架无疑是非常合适的选择,也是唯一的选择,除非你有拯救世界的梦想;如果开发的只是一个简单的回复功能,只需要支持文本和 Emoji 表情的混排,用框架的确有点重,通过 contentEditable 自己实现也无可厚非,但确实要做好踩坑的心理准备,如果这篇文章可以让你避开一些坑,那就功德无量了。

文中提到的另一篇文章 “Why ContentEditable is Terrible”,有兴趣的可以看下萝卜哥的译文 “【译】为什么web富文本编辑器是天坑?”。

格式说明:链接,原文,说明


原文:ContentEditable — The Good, the Bad and the Ugly

每隔一段时间,就会有一些开发者发现,业界还没有一款完美的网页 WYSIWYG 编辑器,然后就会发生下面的场景:

这简直太奇怪了,嘿,哥们,我们有一点时间,要不自己搞一个?

WYSIWYG 编辑器有什么难的?现在我们已经有了 contentEditableexecCommand()queryCommandState(),只需加个工具栏,放些漂亮的按钮,贴上漂亮的 SVG 或字体图标,点击的时候应用下加粗或者文本链接,简单地排个版,再弄点 CSS 动画,主要工作不就搞定了吗?

剩下的就是一些细节问题了……

怎么让加粗命令使用 <strong> 而不是 <b>,按回车键是创建一个新的 <p> 而不是插入一个恶心的 <br><div> 导致一大坨东西挤在一起。

在 StackOverflow 上提了几个问题,一个月后,项目里用了一堆好像能解决问题的黑科技(这其实是很可怕的),已经快要死翘翘了。然后开发者气急败坏地加入了 “contentEditable 你个大坑货” 小组,开始对 XYSIWYG 编辑器深恶痛绝。

加粗命令、回车操作和粘贴处理只是冰山一角,但足以让 JavaScript 开发老鸟对 contentEditable 感到厌烦。

我们再来看看如何改变加粗命令的行为,在所有的处理方法中,有一种是通过变化监视器监控编辑器内容的变更,重新标准化 HTML 结构,这是重新实现 execCommand() 行为的一个完美解决方案(在我看来,也是唯一的一个完美方案)。但这里再次强调,这只是冰山一角。

如果使用变化监视器方案,你可能需要处理保留选中区域的逻辑(因为当你修改 DOM 的时候它可能被重置),以及 撤销管理 的逻辑。

如果上面这些都处理好了,那么恭喜,终于搞定了加粗问题。但再想想,回车操作也要这么来一遍,我已经开始怀疑人生了……

如果你和我一样是一个纯粹主义者,可能会尝试自己实现一个 execCommand(),算法非常简单—— 获取选中内容获得选中区域(我有提过 Firefox 支持多选区(multi-range selections)吗?)、做些 DOM 处理、重建选区、实现撤销管理(这次可没人帮你做历史记录),搞定!

然而:

  • 你因为项目延迟了几个月被解雇。
  • 在其它浏览器上测试发现一大堆问题(哎呀,我有提过 Blink 和 WebKit 的选区机制不一样吗?嗯……这个问题已经存在 8 年了),你好像实现了一个史上最烂的编辑器。
  • bug 的出现让你开始意识到,他喵的到底有多少种情况要考虑啊。
  • 你仍然需要处理回车操作和该死的粘贴逻辑。
  • 你发现还需要重新实现 BlinkWebKit 的退格和删除操作,因为它们比你想象的更喜欢行内样式(inline styles)。
  • 你发现有些浏览器对于链接总是返回一个绝对路径的 href 属性,即使你使用的是相对路径。
  • 天了噜……我们这才聊了 4 个最基本的功能,你什么时候添加对图片的支持?
  • 你知道选区还有方向的区别吗?

够了,我想我已经很好的表达了一个观点:contentEditable 真可怕(ContentEditable is Terrible)。selection、clipboard、drag、drop 等相关 API 以及它们的实现是不完善的,并且(或者)不统一和有各种 bug,Range API 复杂而麻烦。

但冰箱还是空的(工作还得继续),现在你应该知道怎么做了。

再见,contentEditable

既然 contentEditable 和 selection API 是恶魔双星,那就尽可能离它们远点。所以,方案是什么?

  1. 你需要一个自定义的选区系统,在 DOM 中按下鼠标/移动鼠标时,可以获取到位置信息(表示一个范围),你可以显示自定义的插入符号(哇……你现在可以控制它的样式了呢)和文本选区(就是一个简单的 <span>)。
  2. 你需要捕获文字输入,侦听键盘事件,并把获取到的字符插入编辑器中。
  3. 你需要处理通过方向键导航光标,左和右比较简单,上和下有点蛋疼,但如果已经实现了第 1 点,这个就不是问题了。等等,按下 Alt 键时,左右方向键导航的是整个单词呀。你可以检索文本内容的空白字符呀,这都不是事儿。
  4. 然后,你需要处理下粘贴操作。参考 Clipboard API,你可以侦听 document 上的粘贴事件,从参数中获取到数据。你也可以使用 老的粘贴机制(paste bin mechanism)。

世界是多么美好,空气是多么清新。展现在你面前的,都是常用的 API 或者你自己的代码,全部是可控的。和可怕的绝望说拜拜。Selection API 和选区仅仅用于和浏览器进行交互,但在内部你实现了更适用于文本编辑的、不同的机制。再也不需要和那些不确定的 DOM 打交道,只需要操作 DOM 的文本、样式、索引(indexex)和变换(translation)。现在,应用加粗是一个非常简单的算法:给选中的内容添加一个样式,然后自动变换更新 DOM。

冰箱里的食物还不完美,所以你把项目发布到 GitHub 上,开始收到第一个 bug 反馈,不要紧张,没有一个软件是一天炼成的。

Q:我该怎样输入波兰文字?

当我按下 Alt+L,我希望插入的是 “ł”,但你的编辑器插入的是 “l”。

A:对的,一起来看看我们可以做些什么。我们不知道键盘布局,所以不能简单地检查 Alt 的状态。此外,语言的种类也太多了吧喂。好吧,DOM3(DOM level 3)已经有 KeyboardEvent.key 了,但目前仅 IE 和 Firefox 支持,Blink 很快也会支持(译者注:Chrome 51.0 已支持),所以你可以再等等,很有可能所有的主流浏览器会在合适的时间内支持该特性。

Q:我该怎样输入重音符号?

我有一个西班牙语的键盘布局,当我输入一个 “`” ,再输入一个字母(如 “u”),我希望先看到那个标记,然后在同个位置出现字母 “ù”。

A:等等,啥?他是说两次按键会合体成一个字母吗?你他喵的在逗我……我们可以对这种情况做特殊处理(前提是能够获取到当前正在使用的键盘布局),让我们祈祷不会有太多的语言需要这么搞。

Q:我该怎样输入平假名?

在输入时,相关内容可以自动添加下划线,并弹出这种组合弹框。

A:对的,复合事件(composition events)。你知道可以利用这个做点什么(如果浏览器支持的话)。但不幸的是,你会发现 输入法引擎(Input Method Engine)在每个操作系统中表现都不一样,而且通常是系统集成的(例如,它会学习新词汇并实现智能补全)。你再一次感到生无可恋。然后,突然灵光一闪:可以搞一个隐藏的文本区域(hidden textarea),从中读取输入呀,如果把它放到插入符号(caret)的位置,不就可以搞这个弹框了吗,只是失去了上下文建议。打开 IME API 还有些工作要做,你总有一天会放弃这些黑科技的。

译者注:

  • conposition events:复合事件,输入的字符重新组合成文字的过程:

    • 进入组字模式(compositionstart)
    • 组字内容更新(compositionupdate)
    • 组字模式结束(compositionend)

    参考资料:用 Composition Event 改進 CodeMirror 對輸入法的支援

  • IME:Input Method Editors,输入法编辑器。
  • caret:插入符号,就是编辑的时候那个一闪一闪的竖线。

Q:我该怎样使用 iPad 输入?

当我触碰编辑器时,键盘没有出现。

A:可编辑区域没有获得焦点 = 没有键盘。当然,通过一个隐藏的文本区域(hidden textarea)可以变相解决,真的吗?那你现在怎么粘贴?

Q:Alt + 左/右方向键导航时,光标跳过了不止一个单词

ພາສາຈີນແມ່ນພາສາໜຶ່ງທີ່ເວົ້າໃນປະເທດຈີນ.” 这个文本包含了很多个单词,但你的编辑器把它当成一个单词处理了。

A:问题是,空格在哪?!

Q:在 iPhone 上“摇一摇撤销操作” 不起作用

A:对的,必须用 Ctrl + Z,是时候试下加速器(accelerometer)了。

Q:在 iPhone/iPad 上,当我操作一个选取时,键盘不见了

A:对的,焦点从隐藏的文本区域(hidden textarea)移动到文档(document)了……

Q:拼写检查失效了

A:这太可怕了!太可怕了!

Q:你的编辑器不能同时使用键盘和屏幕阅读器(Screen Reader)

你的编辑器根本用不了。通常,当我进入编辑器时,屏幕阅读器会通知我,然后,当我导航文本时,它会阅读选中的单词,此外,它也会阅读我输入的内容。但在你的编辑器里,啥都木有发生。顺便说下,你有看过 http://www.w3.org/TR/2dcontext/#best-practices 吗?

A:是的,目前没有,但很快会有。

……

回到起点

在上面的讨论中可以得出,contentEditable 也许真的很糟糕,但它已经在那儿。我相信所有上面提到的问题,总有一天原生 API 都会支持,但请相信我,那天还没到来。

标准化那些复杂的特性是一个非常艰辛的工作,因为你能看到的永远是冰山一角。虽然我已经和 contentEditable 打交道将近 4 年了(当然,和搞了 13 年的 Frederico Knabben 是完全没得比的 :D),但当看到 W3C 的 public-web-appspublic-editing-tf 的邮件列表时,仍感到大开眼界。此外,在浏览器中编辑有很多用例,甚至 XWSISWYG 文本编辑器之间也存在差异,所以,锁定浏览器只针对一个用例是不合理的。

编辑特攻队(The Editing Task Force)

说到标准,contentEditable 必然是其中之一,因为我们需要 contentEditable。也许几年之后,我们将有可能无需 contentEditable 就可以实现一个功能齐全、稳定可用的 XYSIWYG 编辑器。所以,W3C 编辑特攻队 在去年成立(我和 Frederico Knabben 都加入了),就“目前的情况该如何处理”展开了激烈的讨论。关于这方面,我将单独写个帖子,因为它看起来很有前景。

(编辑:你可以在 “Fixing ContentEditable” 一文中查看更多关于 contentEditable 标准化的信息。)

contentEditable 好的部分

contentEditable 就像 JavaScript,除了道格拉斯(Douglas Crockford)还没有给它写一本书。它也有好的一面(是,我知道,到底好的多还是坏的多存在争议)。只需要给 HTML 元素添加一个属性,就能启用文本输入、选中内容、键盘导航、拼写检查、拖放、粘贴、撤销管理,太神奇了!这些都是系统集成的,使得编辑器可以使用屏幕阅读器(screen reader)或者在触控设备上运行,并且非常的 国际化。让我们聚焦在这些好的部分,暂时忘掉它不好的一面。

在过去的几年中,CKEditor 的工作让我逐渐意识到,我们正在逐渐使用自己的实现来替代原生的特性,源于一个回车操作的自定义行为、命令(像 execCommand() API,能够应用、移除、检查一个特定的样式(如加粗)的状态)、撤销管理和劫持粘贴操作以过滤粘贴内容。然后添加了一些选区系统的实现(比如在模态状态下锁定选中内容),加强在表格和列表编辑中的导航功能。从 4.0 版本开始,CKEditor 拥有了自己的 “插入 HTML 到选区” 机制和一个 允许到达不可编辑位置 的特性。CKEditor 4.1 引入了高定制化的 内容过滤器(粘贴操作不再乱七八糟)。CKEditor 4.3 带来对 不可编辑区块包含可编辑区块(non-editable islands with editable islands inside)的支持,这个需要重写许多原生的行为(selection、keyboard、focus、clipboard)。与此同时,我们实现了 BlinkWebKit 中对自定义退格和删除的支持。最后,就在几周前,我们完全接管了剪贴板,这意味着在一些浏览器中,复制、剪切和粘贴操作,完全在 CKEditor 的掌握之中。

也就是说,如今的 CKEditor 不仅不让浏览器对内容进行任何处理(除了输入、删除等基本操作),连选区系统、键盘导航和剪贴板、焦点管理等其它相关的 API 也一起接管了。

CKEditor 5 我们计划在编辑器的控制下,仅通过浏览器插入文本就可以完成这个过程(参考:CKEditor 5: The Future of Rich Text Editing)。讽刺的是,我们准备将所有的编辑算法都建立在一个自定义的数据模型之上,我们承认 DOM 不是完成这项任务的最佳工具。当然,我们也到了面临国际化问题的时候了(如对 Alt+退格的支持),我们都知道这一点。但是,这个功能很可能包含在不久后浏览器开放的第一批特性中,感谢编辑特攻队的辛苦付出。

编辑框架

这一切听起来都很美好,但要实现这个目标仍有大量的工作需要完成。即使知道这似乎不是一个独立开发者、甚至是中等规模公司可以完成的项目,但我们相信,终将会出现一个编辑框架,允许其他开发者在其基础上建立自己的定制化方案,这就是我们给 CKEditor 5 定下的目标,虽然 Alloy Editor 这样的项目已经证明即使基于 CKEditor 4 也是可行的。如果不是开发者一旦发现现有的编辑器无法满足需求,就立刻从一个光秃秃的 contentEditable 开始倒腾,我们今天可能在一个不同的地方 ;)。

[译] ContentEditable 那些好的、坏的和坑的更多相关文章

  1. paip.常用汉字形声字大全3500字

    paip.常用汉字形声字大全3500字 作者Attilax  艾龙,  EMAIL:1466519819@qq.com 来源:attilax的专栏 地址:http://blog.csdn.net/at ...

  2. “会”和 "好”纯粹是两个概念

    你会吗? 如果我现在问下大家你会OOP 吗?你会OOD吗? 你知道SOLID吗?你会在实际工作中运用这些原则吗? 你知道模式吗,你会在实际项目中适时引入合理的设计模式来解决项目中的代码坏味吗? 你知道 ...

  3. 转:Unicode汉字编码表

    转自:http://blog.csdn.net/huangxy10/article/details/10012119 Unicode汉字编码表 1 Unicode编码表  Unicode只有一个字符集 ...

  4. Unicode汉字编码表

    U+  0   1  2  3  4   5  6   7   8   9   A  B  C  D  E  F  ------------------------------------------ ...

  5. Unicode编码的熟悉与研究过程(内附全部汉字编码列表)

    我有一个问题是:是不是会有个别汉字无法在Unicode下表示,这种情况下就不能完全显示了? 各种编码查询表:http://bm.kdd.cc/ ---------------------------- ...

  6. .Net程序员学用Oracle系列(7):视图、函数、存储过程、包

    1.视图 1.1.创建.删除及调用普通视图 1.2.高级视图介绍 2.函数 2.1.系统函数介绍 2.2.创建.删除及调用自定义函数 3.存储过程 3.1.创建.修改及删除存储过程 3.2.调用存储过 ...

  7. .Net程序员学用Oracle系列:视图、函数、存储过程、包

    1.视图 在实际操作过程中,本人发现 Oracle 视图定义有一个缺陷,就是不大方便注释,每次写好的注释执行之后再打开视图定义所有注释就全都没了.后来我发现把注释写到末尾就不会被清除,但这样总感觉乖乖 ...

  8. Unicode汉字编码表以及参考源码分享

    1 Unicode编码表  Unicode只有一个字符集,中.日.韩的三种文字占用了Unicode中0x3000到0x9FFF的部分  Unicode目前普遍采用的是UCS-2,它用两个字节来编码一个 ...

  9. 简体和繁体加起来有六七万个汉字,所以Unicode只能排除一些几乎不用的汉字,Unicode编码的熟悉与研究过程(内附全部汉字编码列表)

    我有一个问题是:是不是会有个别汉字无法在Unicode下表示,这种情况下就不能完全显示了? 各种编码查询表:http://bm.kdd.cc/ ---------------------------- ...

随机推荐

  1. visio画任意形状图形

    1,连接线--右击---曲线连接线 2,选中组合 3,开发工具--操作--连接--填充

  2. 大数据技术之_16_Scala学习_09_函数式编程-高级

    第十三章 函数式编程-高级13.1 偏函数(partial function)13.1.1 提出一个需求,引出思考13.1.2 解决方式-filter + map 返回新的集合13.1.3 解决方式- ...

  3. Java中堆和栈有什么区别

    stack 和 heep 都是内存的一部分stack 空间小,速度比较快, 用来放对象的引用heep 大,一般所有创建的对象都放在这里. 栈(stack):是一个先进后出的数据结构,通常用于保存方法( ...

  4. POJ 2785 4 Values whose Sum is 0(哈希表)

    [题目链接] http://poj.org/problem?id=2785 [题目大意] 给出四个数组,从每个数组中选出一个数,使得四个数相加为0,求方案数 [题解] 将a+b存入哈希表,反查-c-d ...

  5. redis 安装并且设置开机后台自动启动(转)

      1,安装redis wget http://download.redis.io/releases/redis-2.8.8.tar.gz .tar.gz cd redis- make 2,建立Red ...

  6. Linux下查看某个进程的网络带宽占用情况

    说明: 1.可能查看某个进程的带宽占用需要明确知道PID.进程名字.发送速度.接收速度. 2.很遗憾,在Linux原生的软件中没有这样的一款,只能额外装,最符合以上的情况就只有nethogs. 3.n ...

  7. MySQL索引,如何正确创建MySQL索引?

    索引可以提高数据的检索效率,也可以降低数据库的IO成本,并且索引还可以降低数据库的排序成本.排序分组操作主要消耗的就是CPU资源和内存,所以能够在排序分组操作中好好的利用索引将会极大地降低CPU资源的 ...

  8. vue中自定义指令vue.direvtive,自定义过滤器vue.filter(),vue过渡transition

    自定义指令 默认设置的核心指令( v-model,v-bind,v-for,v-if,v-on等 ),Vue 也允许注册自定义指令.注意,在 Vue2.0 里面,代码复用的主要形式和抽象是组件——然而 ...

  9. django模板解析 循环列表中 切片和求长度

    {% for subrow in subdic.content|slice:":5" %} {% endfor %} {% if "{{subdic.content|le ...

  10. python多线程,守护线程

    https://www.cnblogs.com/liuyang1987/p/6292321.html