一文入门富文本编辑器

小白学Java:奇怪的RandomAccess

简介

富文本编辑器,能够使web页面像word一样,实现对文本的编辑,通常应用在一些文本处理比较多的系统中。现在业界有很多成熟的富文本编辑器,比如功能齐全啊TinyMCE、轻量高效的wangEditor、百度出品的UEditor等。富文本编辑器很多,但是却很少思考如何从零开始,实现一个富文本编辑器。本文主要简述如何从零开始,实现一个简易的富文本编辑器。

基本使用

普通的HTML标签,能够输入的通常只是表单,表单输入的是纯文本,不带格式的内容。富文本相对于表单,能够给输入文本内容增加一些自定义内容样式,比如加粗、字体颜色、背景…。富文本的实现,主要是给HTML标签,比如div增加一个contenteditable属性,拥有该属性的HTML标签,就能够对该标签里的内容,实现自定义的编辑。最简单的富文本编辑器如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
     
</head>
<body>
    <div id="app" style="width: 200px;height: 200px;background-color: antiquewhite;" contenteditable='true'></div>
</body>
</html>

基本操作

富文本类似于Word,有很多操作文本选项,比如文本的加粗、添加背景颜色、段落缩进等,使用方式是命令式的,只需要执行document.execCommand(aCommandName, aShowDefaultUI, aValueArgument),其中aCommandName命令名称aShowDefaultUI
一个 Boolean是否展示用户界面,一般为 false。Mozilla 没有实现。aValueArgument额外参数,一般为null

基本操作命令

以下简单列举一些富文本操作命令,下面给出一些例子的简单使用

BFT-SMaRt:用Netty做客户端的可靠信道

命令 说明
backcolor 颜色字符串 设置文档的背景颜色
bold null 将选择的文本加粗
createlink URL字符串 将选择的文本转换成一个链接,指向指定的URL
indent null 缩进文本
copy null 将选择的文本复制到剪切板
cut null 将选择文本剪切到剪切板
inserthorizontalrule null 在插入字符处插入一个hr元素

Example:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
    <style>
      html, body{
          width: 100%;
          height: 100%;
          padding: 0;
          margin: 0;
      }
      #app{
          display: flex;
          flex-direction: column;
          justify-content: flex-start;
          width: calc(100% - 100px);
          height: calc(100% - 100px);
          padding: 50px;
      }

      .operator-menu{
          display: flex;
          justify-content: flex-start;
          align-items: center;
          width: 100%;
          min-height: 50px;
          background-color: beige;
          padding: 0 10px;
      }
      .edit-area{
          width: 100%;
          min-height: 600px;
          background-color: blanchedalmond;
          padding: 20px;
      }
      .operator-menu-item{
          padding: 5px 10px;
          background-color: cyan;
          border-radius: 10px;
          cursor: pointer;
          margin: 0 5px;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div class="xssyun-org the-best-plugins operator-menu">
        <div class="xssyun-org the-best-plugins operator-menu-item" data-fun='fontBold'>加粗</div>
        <div class="xssyun-org the-best-plugins operator-menu-item" data-fun='textIndent'>缩进</div>
        <div class="xssyun-org the-best-plugins operator-menu-item" data-fun='inserthorizontalrule'>插入分隔符</div>
        <div class="xssyun-org the-best-plugins operator-menu-item" data-fun='linkUrl'>链接百度</div>
      </div>
      <div class="xssyun-org the-best-plugins edit-area" mce-contenteditable="true"></div>
    </div>
    <script>
      let operationItems = document.querySelector('.operator-menu')
      // 事件监听采用mousedown,click事件会导致富文本编辑框失去焦点
      operationItems.addEventListener('mousedown', function(e) {
        let target = e.target
        let funName = target.getAttribute('data-fun')
        if (!window[funName]) return
        window[funName]()
        // 要阻止默认事件,否则富文本编辑框的选中区域会消失
        e.preventDefault()
      })

      function fontBold () {
        document.execCommand('bold')
      }
      function textIndent () {
        document.execCommand('indent')
      }
      function inserthorizontalrule () {
        document.execCommand('inserthorizontalrule')
      }
      function linkUrl () {
        document.execCommand('createlink', null, 'www.baidu.com')
      }
    </script>
  </body>
</html>

文本范围与选区

富文本中,文本范围和选区是一个非常强大的功能,借助于文本选区,我们可以对选中文本做一些自定义设置。核心是两个对象,SelectionRange对象。用比较官方的说法是,Selection对象,表示用户选择的文本范围或光标的当前位置Range对象表示一个包含节点与文本节点的一部分的文档片段。简单来说,Selection是指页面中,我们鼠标选中的所有区域,Range是指页面中我们鼠标选中的单个区域,属于一对多的关系。比如,我们要获取当前页面的选区对象,可以调用var selection = window.getSelection(),如果想要获取到第一个文本选区信息,可以调用var rang = selection.getRangeAt(0),获取到选区文本信息,采用range.toString()
文本范围与选区,一个比较经典的用法就是,富文本粘贴格式过滤。在我们往富文本编辑器中复制文本时,会保留原文本的格式,如果我们要去除复制的默认格式,只保留纯文本,该如何操作呢?
博主在处理这个问题时,首先想到的是,能不能监听粘贴事件(paste),在粘贴文本时,将剪切板内容替换掉。这一个里面也是有坑的,粘贴时操作剪切板是不生效的。在实现功能需求时,最初采用的是正则匹配,去除HTML标签。奈何文本格式五花八门,经常出现各种奇奇怪怪的字符,问题比较多,而且复制大文本时,页面存在性能问题,这并不是一种好的处理方式,直到后来真正理解了文本范围与选区,才发现这个设置,真香。
富文本选区的处理逻辑大致思路如下:

  1. 监听文本粘贴事件
  2. 阻止默认事件(阻止浏览器默认复制操作)
  3. 获取复制纯文本
  4. 获取页面文本选区
  5. 删除已选中文本选区
  6. 创建文本节点
  7. 将文本节点插入到选区中
  8. 将焦点移动到复制文本结尾

示例代码如下:

let $editArea = document.querySelector('.edit-area')
$editArea.addEventListener('paste', e => {
    // 阻止默认的复制事件
    e.preventDefault()
    let txt = ''
    let range = null
    // 获取复制的文本
    txt = e.clipboardData.getData('text/plain')
    // 获取页面文本选区
    range = window.getSelection().getRangeAt(0)
    // 删除默认选中文本
    range.deleteContents()
    // 创建一个文本节点,用于替换选区文本
    let pasteTxt = document.createTextNode(txt)
    // 插入文本节点
    range.insertNode(pasteTxt)
    // 将焦点移动到复制文本结尾
    range.collapse(false)
})

除此之外,还有很多操作可以借助于选区来实现,比如光标的定位选中区域内容包裹其他样式等。

实现手动将光标定位到最后一个字符


function keepLastIndex(element) {
    if (element && element.focus){
        element.focus();
    } else {
        return
    }
    let range = document.createRange();
    range.selectNodeContents(element);
    range.collapse(false);
    let sel = window.getSelection();
    sel.removeAllRanges();
    sel.addRange(range);
}

选中区域包裹其他样式

function addCode () {
    let selection = window.getSelection()
    // 暂时处理第一个选区
    let range = selection.getRangeAt(0)
    // 拷贝一份原始选中数据
    let cloneNodes = range.cloneContents()
    // 移除选区
    range.deleteContents()
    // 创建内容容器
    let codeContainer = document.createElement('code')
    codeContainer.appendChild(cloneNodes)
    // 往选区内添加文本
    range.insertNode(codeContainer)
}

附件

以下为测试代码

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <!-- https://electronjs.org/docs/tutorial/security#csp-meta-tag -->
    <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" />
    <style>
      html, body{
          width: 100%;
          height: 100%;
          padding: 0;
          margin: 0;
      }
      #app{
          display: flex;
          flex-direction: column;
          justify-content: flex-start;
          width: calc(100% - 100px);
          height: calc(100% - 100px);
          padding: 50px;
      }

      .operator-menu{
          display: flex;
          justify-content: flex-start;
          align-items: center;
          width: 100%;
          min-height: 50px;
          background-color: beige;
          padding: 0 10px;
      }
      .edit-area{
          width: 100%;
          min-height: 600px;
          background-color: blanchedalmond;
          padding: 20px;
      }
      .operator-menu-item{
          padding: 5px 10px;
          background-color: cyan;
          border-radius: 10px;
          cursor: pointer;
          margin: 0 5px;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div class="xssyun-org the-best-plugins operator-menu">
        <div class="xssyun-org the-best-plugins operator-menu-item" data-fun='fontBold'>加粗</div>
        <div class="xssyun-org the-best-plugins operator-menu-item" data-fun='textIndent'>缩进</div>
        <div class="xssyun-org the-best-plugins operator-menu-item" data-fun='inserthorizontalrule'>插入分隔符</div>
        <div class="xssyun-org the-best-plugins operator-menu-item" data-fun='linkUrl'>链接百度</div>
        <div class="xssyun-org the-best-plugins operator-menu-item" data-fun='addCode'>code</div>
      </div>
      <div class="xssyun-org the-best-plugins edit-area" mce-contenteditable="true"></div>
    </div>
    <script>
      let operationItems = document.querySelector('.operator-menu')
      // 事件监听采用mousedown,click事件会导致富文本编辑框失去焦点
      operationItems.addEventListener('mousedown', function(e) {
        let target = e.target
        let funName = target.getAttribute('data-fun')
        if (!funName) return
        window[funName]()
        // 要阻止默认事件,否则富文本编辑框的选中区域会消失
        e.preventDefault()
      })
      let $editArea = document.querySelector('.edit-area')
      $editArea.addEventListener('paste', e => {
        // 阻止默认的复制事件
        e.preventDefault()
        let txt = ''
        let range = null
        // 获取复制的文本
        txt = e.clipboardData.getData('text/plain')
        // 获取页面文本选区
        range = window.getSelection().getRangeAt(0)
        // 删除默认选中文本
        range.deleteContents()
        // 创建一个文本节点,用于替换选区文本
        let pasteTxt = document.createTextNode(txt)
        // 插入文本节点
        range.insertNode(pasteTxt)
        // 将焦点移动到复制文本结尾
        range.collapse(false)
        keepLastIndex($editArea)
      })

      function fontBold () {
        document.execCommand('bold')
      }
      function textIndent () {
        document.execCommand('indent')
      }
      function inserthorizontalrule () {
        document.execCommand('inserthorizontalrule')
      }
      function linkUrl () {
        document.execCommand('createlink', null, 'www.baidu.com')
      }

      function addCode () {
        let selection = window.getSelection()
        // 暂时处理第一个选区
        let range = selection.getRangeAt(0)
        // 拷贝一份原始选中数据
        let cloneNodes = range.cloneContents()
        // 移除选区
        range.deleteContents()
        // 创建内容容器
        let codeContainer = document.createElement('code')
        codeContainer.appendChild(cloneNodes)
        // 往选区内添加文本
        range.insertNode(codeContainer)
      }

      function keepLastIndex(element) {
        if (element && element.focus){
          element.focus();
        } else {
          return
        }
        let range = document.createRange();
        range.selectNodeContents(element);
        range.collapse(false);
        let sel = window.getSelection();
        sel.removeAllRanges();
        sel.addRange(range);
      }
    </script>
  </body>
</html>

参考资料

详细解析Redis中的布隆过滤器及其应用

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享