一文入门富文本编辑器

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

简介

富文本编辑器,能够使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     contenteditable='true'></div>
</body>
</html>

基本操作

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

基本操作命令

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

命令说明
backcolor颜色字符串设置文档的背景颜色
boldnull将选择的文本加粗
createlinkURL字符串将选择的文本转换成一个链接,指向指定的URL
indentnull缩进文本
copynull将选择的文本复制到剪切板
cutnull将选择文本剪切到剪切板
inserthorizontalrulenull在插入字符处插入一个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{
           100%;
          height: 100%;
          padding: 0;
          margin: 0;
      }
      #app{
          display: flex;
          flex-direction: column;
          justify-content: flex-start;
           calc(100% - 100px);
          height: calc(100% - 100px);
          padding: 50px;
      }

      .operator-menu{
          display: flex;
          justify-content: flex-start;
          align-items: center;
           100%;
          min-height: 50px;
          background-color: beige;
          padding: 0 10px;
      }
      .edit-area{
           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="operator-menu">
        <div   data-fun='fontBold'>加粗</div>
        <div   data-fun='textIndent'>缩进</div>
        <div   data-fun='inserthorizontalrule'>插入分隔符</div>
        <div   data-fun='linkUrl'>链接百度</div>
      </div>
      <div   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{
           100%;
          height: 100%;
          padding: 0;
          margin: 0;
      }
      #app{
          display: flex;
          flex-direction: column;
          justify-content: flex-start;
           calc(100% - 100px);
          height: calc(100% - 100px);
          padding: 50px;
      }

      .operator-menu{
          display: flex;
          justify-content: flex-start;
          align-items: center;
           100%;
          min-height: 50px;
          background-color: beige;
          padding: 0 10px;
      }
      .edit-area{
           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="operator-menu">
        <div   data-fun='fontBold'>加粗</div>
        <div   data-fun='textIndent'>缩进</div>
        <div   data-fun='inserthorizontalrule'>插入分隔符</div>
        <div   data-fun='linkUrl'>链接百度</div>
        <div   data-fun='addCode'>code</div>
      </div>
      <div   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>

参考资料

免责声明:文章转载自《一文入门富文本编辑器》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Android下OpenCV的环境搭建C#实现PDF转SWF的操作下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

flutter stack嵌套,appbar透明,Container设置背景图片并且图片在appbar之下

stack嵌套 一般情况下 stack是无法嵌套,出现stack嵌套,布局就会出乱 解决方式:就是第二个stack需要确定宽高 appbar透明 AppBar( backgroundColor: Colors.transparent, elevation: 0, } container设置背景 Contai...

ie6,ie7,ie8,FF 浏览器兼容问题

javascript部分1. document.form.item 问题问题:代码中存在 document.formName.item("itemName") 这样的语句,不能在FF下运行解决方法:改用 document.formName.elements["elementName"]2. 集合类对象问题问题:代码中许多集合类对象取用时使用(),IE能接受...

什么是盒模型?

  css盒模型是在网页设计中经常用到的css技术所使用的一种思维模型!   在网页中,css盒模型主要4个部分组成,有Content(内容)-Margin(外边距)-Border(边框)-Padding(内边距)       在网页中,一个元素占有空间的大小由几个部分构成,其中包括元素的内容,元素的补白,元素的边框,元素的边界四个部分。这四个部分占有的空...

Qt StyleSheet样式表实例(转)

QT论坛看到的,收藏一下! 在涉及到Qt 美工的时候首先需要掌握CSS 级联样式表。 下面将通过几个例子来介绍一下怎样使用Qt中的部件类型设计。自定义的前台背景与后台背景的颜色: 如果需要一个文本编辑器的背景变为黄色, 下面是代码行: qApp->setStyleSheet("QLineEdit {  针对一个对话框的内容中使用QLineEdit以及...

h5 下ios适配底部小黑条,简单解决方案,只需一步

在页面加入一行css代码即可 @supports (bottom: env(safe-area-inset-bottom)) { body, .footer{ box-sizing:content-box;padding-bottom:constant(safe-area-inset-bottom);...

元素大小-偏移量(offset)客户区大小(client)滚动大小(scroll)

一、偏移量---offset 1、定位父级   在理解偏移大小之前,首先要理解offsetParent。人们并没有把offsetParent翻译为偏移父级,而是翻译成定位父级,很大原因是offsetParent与定位有关   定位父级offsetParent的定义------》与当前元素最近的经过定位(position不等于static)的父级元素,主要...