前端富文本基础及实现

前端富文本基础及实现

https://www.zoo.team/article/rich-text



前言
在日常生活中我们会经常接触到各种各样的文档格式和形式,其中富文本在文档格式中扮演了重要角色。对于前端而言,富文本产品也层出不穷,其应用也越来越广。

这篇文章将会为大家介绍前端富文本的一些基础知识以及简单的实现思路。

什么是富文本
纯文本就是用纯文字编辑器编写,输入什么就是什么的文档,只包含字符。

富文本对应的是富文本格式(Rich Text Format),即 RTF 格式,又称多文本格式,是由微软公司开发的跨平台文档格式。除字符外还有丰富的样式。doc,docx,rtf,pdf 等都是富文本格式的文件类型。

如图所示:



前端中的富文本
前端富文本通过 html 的各个元素配合各种样式(一般是内联样式)实现。

例如:图片富文本编辑器中的富文本,是由红色框中带有语义化标签和内联样式的 html 渲染实现的。通过富文本编辑器,即可实现富文本的编写、展示。

目前常见的前端富文本编辑器有 tinymce,UEeditor,draft 等。

文章下文将会讲述实现前端富文本编辑器的一些基础知识和步骤。

富文本输入模式实现
实现前端富文本编辑器首先要实现文本输入,一般常用两种方式实现。

iframe
第一种方式是使用 iframe 标签。

在空白的 HTML 文档中嵌入一个 iframe,并将 designMode 属性设置为 on,文档就会变成可编辑的,实际编辑的则是 iframe 内的 body 元素。文档变成可编辑后,就可以像使用文字处理程序一样编辑文本。

效果如图:




元素设置 contenteditable
第二种方式是使用 contenteditable 属性指定 HTML 文档中的元素。该方式是 IE 最早实现的。使用方式是在一个元素上添加 contenteditable 属性并设置为 true,然后该元素会立即被用户编辑。

此种方式通常会和 autocapitalize(首字母自动大写属性)、spellcheck(检查元素的拼写错误,实验功能)等属性共同使用以提升体验。

效果如图:




两者特点
两种方式都可以实现编辑模式,并且这种编辑模式与 textarea 不同,其内部会用块级元素(默认为 div 元素)做换行处理,最终体现在 dom 结构中。






两者不同的是:iframe 方式可做到样式隔离,内部样式与外部样式不存在污染与冲突( tinymce 实现方式);元素设置 contentEditable 的方式( wangEditor 等实现方式)则和其他元素一样受到页面 css 作用。个人认为两者没有优劣之分,开发者根据自身需求选择即可。

富文件选区
富文本编辑中我们在进行编辑时首先会先选择一块文本区域(即选区),比如选择一段文字并进行字体加粗等操作,那么选区本身包含了哪些信息呢,下面为大家简单介绍一下。

Selection 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。调用 window.getSelection()(https://developer.mozilla.org/zh-CN/docs/Web/API/Window/getSelection) 可得到此对象,其内部常用属性如下:

anchorNode

返回选中区域对应的节点

anchorOffset

返回选中区域的起始下标,需要注意起始下标会根据左右方向选择的次序不同来展示不同的下标。如果 anchorNode 是字符串则对应文字下标,anchorNode 是元素,则对应选中区域对应它之前的同级节点的数目。

focusNode

返回选中区域终点所在的节点。

focusOffset

与 anchorOffset 类似,如果是 focusNode 是字符串,则对应最后一个选中的字符所在的位置,focusOffset 是元素,则对应选中区域对应同级节点的总数。

rangeCount

返回选中的区域所对应的连续的范围内的数量。

type

返回选中区域所对应的类别是连续 (Range),还是同一个位置的 (Caret)。

我们常通过 anchorNode 与 anchorOffset 属性判断选区起始位置,通过 focusNode 与 focusOffset 属性判断选区终止位置。

选区示例
如图:anchorNode 为选区起始位置所在节点("政采云"文本节点),focusNode 为选区结束位置所在节点("ZOO" 文本节点),anchorOffset 与 focusOffset 分别为起始位置的 index,通过此信息可得到选区范围,此时 Selection 对象 type 为 Range。



光标示例(起始位置是同一个位置的选区)
如图:anchorNode 与 focusNode 为同一节点 ("ZOO" 文本节点),anchorOffset 与 focusOffset 指向节点同一处,通过此信息可得到光标位置,此时 Selection 对象 type 为 Caret。



用途
删除、替换选区内容&插入操作
Selection 对象有 deleteFromDocument(https://developer.mozilla.org/zh-CN/docs/Web/API/Selection/deleteFromDocument) 方法,可以在编辑区域删除选区内容。如想删除后插入,可获取新的 Selection 对象,利用此时位置所在 dom 元素的方法插入对应的文字、元素。

效果如图:








插入逻辑代码如下:

  const insert = () => {
    // 删除所选内容
    window.getSelection().deleteFromDocument()
    const selection = window.getSelection()
    // 删除后选取的起始位置就是插入位置,由 anchorNode 及 anchorOffset 确定
    const { anchorNode, anchorOffset } = selection
    // anchorNode 分为两种情况,一种是文本节点,另一种是其他类型节点,处理逻辑不同
    if (anchorNode.nodeType === 3) {
      const string = anchorNode.nodeValue
      // anchorNode 为文本节点时,需要将内部字符串与索要插入的内容拼接
      anchorNode.nodeValue = (string.substring(0, anchorOffset) + '😄' + string.substring(anchorOffset, Infinity))
    } else {
      const newNode = document.createElement('span')
      newNode.innerText = '😄'
      // anchorNode 为其他类型节点时,需要根据 anchorOffset 在 anchorNode 中插入片元素
      anchorNode.insertBefore(newNode, anchorNode.childNodes[anchorOffset])
    }
  }
 
  //也可根据 Selection 提供的原生方法实现
  const insert2 = () => {
    lastRange = window.getSelection().getRangeAt(0);
    const newNode = document.createElement('span');
    newNode.textContent = '😄'
    lastRange.deleteContents()
    lastRange.insertNode(newNode)
  }
关于选区的更多用途,可参考选区属性和方法进行灵活实现:https://developer.mozilla.org/zh-CN/docs/Web/API/Selection#methods

富文本工具栏实现
根据前文介绍的方法实现输入功能后,我们即实现了纯文本编辑的功能,那么如何进一步实现富文本编辑呢?

document 提供了 execCommand() 方法,该方法会影响使用 designMode 或  contentEditable 属性实现可编辑区域的元素。方法说明如下所示:

document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)

aCommandName

一个 DOMString(https://developer.mozilla.org/zh-CN/docs/conflicting/Web/JavaScript/Reference/Global_Objects/String_6fa58bba0570d663099f0ae7ae8883ab) ,命令的名称。可用命令列表请参阅 命令 (https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand#%E5%91%BD%E4%BB%A4) 。

aShowDefaultUI

一个 Boolean(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Boolean), 是否展示用户界面,一般为 false。Mozilla 没有实现。

aValueArgument

一些命令(例如 insertImage)需要额外的参数(insertImage 需要提供插入 image 的 url),默认为 null。

该方法执行后,会返回 boolean 值,如果是 false,表示操作不被支持或未被启用。

不同浏览器支持的命令也不一样。下方标列出了最常用的命令。

命令作用可选值
backColor设置文档背景颜色。在 styleWithCss 模式下,则只影响容器元素的背景颜色。颜色值字符串(IE 使用这个命令设置文本背景色)
bold切换选中文本的粗体样式null
createLink将选中内容转换为指向给定 URL的链接URL 链接值,至少包含一个字符
fontSize将选中文本改为指定字体大小提供 HTML 字体尺寸 (1-7)
foreColor将选中文本改为指定颜色颜色值字符串
formatBlock将选中文本包含在指定的 HTML标签中提供 HTML 标签,如
insertImage在光标位置插入图片图片的 URL 链接
insertParagraph在光标位置插入

元素

null
italic切换选中文本的斜体样式null
styleWithCSS用这个取代 useCSS 命令。切换使用 HTML tags 还是 CSS 来生成标记。Boolean 值,false 使用CSS,true 使用 HTML
关于 document.exexCommand 的更多命令,可参考 (https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand)


常用功能(字体样式、插入图片)演示
下图挑选了几个常用命令(加粗、斜体、改变字体颜色、插入图片)作为演示:




代码示例如下:

  // 加粗
  const bold = (val) => {
    document.execCommand('StyleWithCSS', true, true)
    document.execCommand('Bold', false, val)
  }
  // 斜体
  const italic = (val) => {
    document.execCommand('StyleWithCSS', true, true)
    document.execCommand('italic', false, val)
  }
  // 改变字体颜色
  const changeColor = (val = '#ff0000') => {
    document.execCommand('StyleWithCSS', true, true)
    document.execCommand('foreColor', false, val)
  }
  // 插入图片
  const insertImage = (val = 'https://avatar-static.segmentfault.com/339/131/3391311562-5d5653daaad5f_huge256') => {
    document.execCommand('StyleWithCSS', true, true)
    document.execCommand('insertImage', false, val)
  }
富文本数据收集存储与回填
富文本容器的 innerHTML 即是富文本数据。

编辑区域可通过获取编辑元素的 innerHTML 拿到对应富文本数据,存入数据库。

网络请求的富文本数据设置为富文本容器的 innerHTML,即可展示富文本内容。






下列图片可简单表明:




结尾(附 Demo)
根据本文介绍内容我们依次了解了前端富文本的概念、输入模式实现、选区的信息及应用、富文本工具栏的实现和富文本数据收集回填。将这些内容汇总即可实现一个简单的前端富文本编辑器。

下方附上本文内容汇总的代码 demo ,内含基于 iframe 和 div 元素分别实现的富文本编辑器,功能简单,供读者参考。读者可根据文章内容进行拓展实现自己的前端富文本编辑器。

<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<style>
  .rt-container {
    height: 200px;
    width: 500px;
    padding: 10px;
    overflow: auto;
  }
</style>

<body>
  --------------------------------------------------------------<br />
  <button onclick="bold()">粗体</button>
  <button onclick="italic()">斜体</button>
  <button onclick="changeColor()">改变颜色</button>
  <button onclick="insertImage()">插入图片</button>
  <button onclick="insert()">插入字符(表情)</button><br />
  元素设置contenteditable<br />
  --------------------------------------------------------------<br />
  // 元素设置 contenteditable 方式
  <div class="rt-container" contenteditable="true">政采云前端团队</div>
  ------------------------------------------------------------------<br />

  <button onclick="boldIframe()">iframe粗体</button><br />
  iframe设置designMode<br />
  // iframe 设置 designMode 方式
  <iframe class="rt-container" name="editor"></iframe><br />

  ------------------------------------------------------------------
  <div>政采云<span>前端</span>团队<img src="https://avatar-static.segmentfault.com/339/131/3391311562-5d5653daaad5f_huge256"
      width="32" height="32">
    <div>ZOO</div>TEAM
  </div>

</body>
<script>
  window.addEventListener("load", () => {
    frames["editor"].document.designMode = "on";
  });
  const bold = (val) => {
    document.execCommand('StyleWithCSS', true, true)
    document.execCommand('Bold', false, val)
  }
  const italic = (val) => {
    document.execCommand('StyleWithCSS', true, true)
    document.execCommand('italic', false, val)
  }
  const changeColor = (val = '#ff0000') => {
    document.execCommand('StyleWithCSS', true, true)
    document.execCommand('foreColor', false, val)
  }
  const insertImage = (val = 'https://avatar-static.segmentfault.com/339/131/3391311562-5d5653daaad5f_huge256') => {
    document.execCommand('StyleWithCSS', true, true)
    document.execCommand('insertImage', false, val)
  }

  const boldIframe = (val) => {
    frames["editor"].document.execCommand('StyleWithCSS', true, true)
    frames["editor"].document.execCommand('Bold', false, val)
  }
  const insert = () => {
    window.getSelection().deleteFromDocument()
    const selection = window.getSelection()
    const { anchorNode, anchorOffset } = selection
    if (anchorNode.nodeType === 3) {
      const string = anchorNode.nodeValue
      anchorNode.nodeValue = (string.substring(0, anchorOffset) + '😄' + string.substring(anchorOffset, Infinity))
    } else {
      const newNode = document.createElement('span')
      newNode.innerText = '😄'
      anchorNode.insertBefore(newNode, anchorNode.childNodes[anchorOffset])
    }
  }
  const insert2 = () => {
    lastRange = window.getSelection().getRangeAt(0);
    const newNode = document.createElement('span');
    newNode.textContent = '😄'
    lastRange.deleteContents()
    lastRange.insertNode(newNode)
  }
</script>
</html>
效果如图:




作者:页航


欢迎关注微信公众号 :政采云前端团队