likes
comments
collection
share

前端富文本基础及实现

作者站长头像
站长
· 阅读数 14

前端富文本基础及实现

前端富文本基础及实现

这是第 153 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:前端富文本基础及实现

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

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

什么是富文本

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

富文本对应的是富文本格式(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() 可得到此对象,其内部常用属性如下:

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 方法,可以在编辑区域删除选区内容。如想删除后插入,可获取新的 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)
  }

关于选区的更多用途,可参考选区属性和方法进行灵活实现:developer.mozilla.org/zh-CN/docs/…

富文本工具栏实现

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

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

document.execCommand(*aCommandName*, *aShowDefaultUI*, *aValueArgument*)

aCommandName

一个 DOMString ,命令的名称。可用命令列表请参阅 命令

aShowDefaultUI

一个 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 的更多命令,可参考 developer.mozilla.org/zh-CN/docs/…

常用功能(字体样式、插入图片)演示

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

前端富文本基础及实现

代码示例如下:

  // 加粗
  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>

效果如图:

前端富文本基础及实现

推荐阅读

可视化搭建系统之数据源

表单数据形式配置化设计

如何将传统 Web 框架部署到 Serverless

浅谈前端埋点 & 监控

如何让 x == 1 && x == 2 && x == 3 等式成立

开源作品

  • 政采云前端小报

开源地址 www.zoo.team/openweekly/ (小报官网首页有微信交流群)

  • 商品选择 sku 插件

开源地址 github.com/zcy-inc/sku…

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 90 余个前端小伙伴,平均年龄 27 岁,近 4 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

前端富文本基础及实现