contenteditable 踩坑记

@0xinhua 发布于

关于富文本编辑器

知乎上有个问题为什么都说富文本编辑器是天坑?,很早就听说了实现一个富文本编辑器需要填很多的坑,“有幸”接触到富文本编辑器,记录下遇到的一些问题及解决方案.

富文本编辑器的实现一般有两种:

  1. 通过设置contenteditable属性,使得在HTML中的任何元素都可以编辑,加上使用一些JavaScript事件处理逻辑,可以将你的网页转换为完整且快速的富文本编辑器。
  2. 基于Draft.js实现编辑器功能,Draft.js是Facebook开源的开发React富文本编辑器开发框架。 而使用contenteditable无疑是最简单的一种方式,但是 DOM 的处理存在很多兼容性的问题,并且处理起来异常麻烦,😢详情查看为什么说contenteditable很糟糕,而这里主要记录使用contenteditable属性实现一个简单编辑器过程的一些坑。

之所以不直接使用inputtextarea,是因为考虑到实现以下功能contenteditable更具有优势:

  1. 输入框的高度无限制,并且自适应
  2. 对一些特定文本进行样式高亮调整等自定义工具栏
  3. 指定位置插入图片、表情等内容
  4. 所见即所得(What you see is what you get)

伴随而来的就是一堆需要解决的问题(这只是其中的很小的一部分...):

  1. placeholder提示语
  2. 获取输入框的内容
  3. 光标位置
  4. 使用delete缩进时,插入多余的dom节点

placeholder提示语

inputtextarea能轻松实现placeholder提示语的效果,但作用于contenteditable的元素,placeholder不起作用,可以通过css:empty解决:

[contenteditable=true]:empty::before {
  content: attr(placeholder);
}

获取输入框的内容

可以利用innerHTMLinnerTexttextContent获取输入框的内容,详细对比介绍一下这几个方法:

innerHTML 返回或修改标签之间的内容,包括标签和文本信息,基本上所有浏览器都支持。

innerText 打印标签之间的纯文本信息,会将标签过滤掉,此功能最初由Internet Explorer引入,在Firefox上存在兼容问题。

innerText !== textContent

innerTexttextContent均能获取标签的内容,但二者存在差别,使用的时候还需注意浏览器兼容性:

  1. textContent会获取style元素里的文本(若有script元素也是这样),而innerText不会
  2. textContent会保留空行、空格与换行符
  3. innerText并不是标准,而textContent更早被纳入标准中
  4. innerText会忽略display: none标签内的内容,textContent则不会
  5. 性能上textContent > innerText

具体查看下面的例子:

光标的位置

首先遇到的一个问题是利用上述方法实现placeholder后,输入框的光标在Firefox中的位置会比其它浏览器要高一截。
图片例子来自medium-editor: 请在friefox浏览器下查看这个bughttps://jsfiddle.net/wooLksnx/

尝试了很多方法来解决均无果,最终发现默认放置 <\br> 标签后,光标位置正常了。

<div class="rich-editor" data-placeholder="Placeholder Text"><br></div>

而我的另一个需求是需要准确地在光标位置的后面插入指定的内容,获取光标位置,然后插入内容。

// getSelection、createRange兼容
export function isSupportRange () {
  return typeof document.createRange === &#39;function&#39; || typeof window.getSelection === &#39;function&#39;
}

// 获取光标位置
export function getCurrentRange () {
  let range = null
  let selection = null
  if (isSupportRange()) {
    selection = document.getSelection()
    if (selection.getRangeAt &amp;&amp; selection.rangeCount) {
      range = document.getSelection().getRangeAt(0)
    }
  } else {
    range = document.selection.createRange()
  }
  return range
}
// 插入内容
export function insertHtmlAfterRange (html) {
  let selection = null
  let range = null
  if (isSupportRange()) {
    // IE &gt; 9 and 其它浏览器
    selection = document.getSelection()
    if (selection.getRangeAt &amp;&amp; selection.rangeCount) {
      let fragment, node, lastNode
      range = selection.getRangeAt(0)
      range.deleteContents()
      let el = document.createElement(&#39;span&#39;)
      el.innerHTML = html
      // 创建空文档对象,IE &gt; 8支持documentFragment
      fragment = document.createDocumentFragment()

      while ((node = el.firstChild)) {
        lastNode = fragment.appendChild(node)
      }
      range.insertNode(fragment)
    
      if (lastNode) {
        range = range.cloneRange()
        range.setStartAfter(lastNode)
        range.collapse(true)
        selection.removeAllRanges()
        selection.addRange(range)
      }
    }
  } else if (document.selection &amp;&amp; document.selection.type != &#39;Control&#39;) {
    // IE &lt; 9
    document.selection.createRange().pasteHTML(html)
  }
}

使用delete缩进时,Chrome插入多余的dom节点

发现的另一个bug是在编辑器进行删除缩进操作时,浏览器会在dom节点中插入节点。

例如:

&lt;div contenteditable=&quot;true&quot;&gt;
  &lt;div&gt;这是第一行的内容&lt;/div&gt;
  &lt;div&gt;这是第二行的内容&lt;/div&gt;
&lt;/div&gt;

当年使用delete进行缩进成一行时,其它浏览器正常显示:

&lt;div contenteditable=&quot;true&quot;&gt;
  &lt;div&gt;这是第一行的内容这是第二行的内容&lt;/div&gt;
&lt;/div&gt;

而Chrome会插入span标签,并且带上继承的一些style属性,font-family, font-size, line-height等:

&lt;div contenteditable=&quot;true&quot;&gt;
  &lt;div&gt;这是第一行的内容&lt;span style=&quot;line-height: 1.5em&quot;&gt;这是第二行的内容&lt;span&gt;&lt;/div&gt;
&lt;/div&gt;

解决方案是使用方法动态移除这些多余的标签,如http://jsfiddle.net/THPmr/6/

参考的一些资料:

  1. INNERTEXT VS. TEXTCONTENT
  2. Why ContentEditable is Terrible
  3. Working around Chrome's contenteditable span bug

几款不错的开源富文本编辑器:

  1. medium-editor
  2. wangEditor —— 轻量级web富文本框

以上。