我的上一篇译文 “
[译] 通过 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 编辑器有什么难的?现在我们已经有了 contentEditable、execCommand() 和 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 的出现让你开始意识到,他喵的到底有多少种情况要考虑啊。
你仍然需要处理回车操作和该死的粘贴逻辑。
你发现还需要重新实现
Blink
和
WebKit
的退格和删除操作,因为它们比你想象的更喜欢行内样式
(inline styles)
。
你发现有些浏览器对于链接总是返回一个绝对路径的 href 属性,即使你使用的是相对路径。
天了噜……我们这才聊了 4 个最基本的功能,你什么时候添加对图片的支持?
你知道选区还有方向的区别吗?
够了,我想我已经很好的表达了一个观点:
contentEditable 真可怕
(ContentEditable is Terrible)
。selection、clipboard、drag、drop 等相关 API 以及它们的实现是不完善的,并且(或者)不统一和有各种 bug,Range API 复杂而麻烦。
但冰箱还是空的(工作还得继续),现在你应该知道怎么做了。
再见,contentEditable
既然 contentEditable 和 selection API 是恶魔双星,那就尽可能离它们远点。所以,方案是什么?
你需要一个自定义的选区系统,在 DOM 中按下鼠标/移动鼠标时,可以获取到位置信息(表示一个范围),你可以显示自定义的插入符号(哇……你现在可以控制它的样式了呢)和文本选区(就是一个简单的
<span>
)。
你需要捕获文字输入,侦听键盘事件,并把获取到的字符插入编辑器中。
你需要处理通过方向键导航光标,左和右比较简单,上和下有点蛋疼,但如果已经实现了第 1 点,这个就不是问题了。等等,按下 Alt 键时,左右方向键导航的是整个单词呀。你可以检索文本内容的空白字符呀,这都不是事儿。
然后,你需要处理下粘贴操作。参考 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)
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-apps
和
public-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)。与此同时,我们实现了
Blink
和
WebKit
中对自定义退格和删除的支持。最后,就在几周前,我们完全接管了剪贴板,这意味着在一些浏览器中,复制、剪切和粘贴操作,完全在 CKEditor 的掌握之中。
也就是说,如今的 CKEditor 不仅不让浏览器对内容进行任何处理(除了输入、删除等基本操作),连选区系统、键盘导航和剪贴板、焦点管理等其它相关的 API 也一起接管了。
CKEditor 5 我们计划在编辑器的控制下,仅通过浏览器插入文本就可以完成这个过程(参考:
CKEditor 5: The Future of Rich Text Editing
)。讽刺的是,我们准备将所有的编辑算法都建立在一个自定义的数据模型之上,我们承认 DOM 不是完成这项任务的最佳工具。当然,我们也到了面临国际化问题的时候了(如对 Alt+退格的支持),我们都知道这一点。但是,这个功能很可能包含在不久后浏览器开放的第一批特性中,感谢编辑特攻队的辛苦付出。
这一切听起来都很美好,但要实现这个目标仍有大量的工作需要完成。即使知道这似乎不是一个独立开发者、甚至是中等规模公司可以完成的项目,但我们相信,终将会出现一个编辑框架,允许其他开发者在其基础上建立自己的定制化方案,这就是我们给 CKEditor 5 定下的目标,虽然
Alloy Editor
这样的项目已经证明即使基于 CKEditor 4 也是可行的。如果不是开发者一旦发现现有的编辑器无法满足需求,就立刻从一个光秃秃的 contentEditable 开始倒腾,我们今天可能在一个不同的地方 ;)。