人人都会写的富文本编辑器

10,289

这个本来是给 vm-manager 写的一个富文本编辑器,后来觉得独立出来维护比较方便,就单独分离出来放到NPM。之所以说人人都会写, 是因为这个组件实现起来确实比较简单,不需要很厉害的Js水平, 基本是对document.execCommand()指令的绑定。在此将过程分享给大家

预览地址 luosijie.github.io/vm-editor/

源码地址 github.com/luosijie/vm…

安装

npm install --save vm-editor

使用

//upload绑定事件将编译的html字符传给父组件
<VmEditor @upload="showHtml"></VmEditor>

<script>
import VmEditor from 'vm-editor'
export default {
  ...
  methods: {
    showHtml: function(data){
      console.log(data)
    }
  }
}
</script>

准备

因为是Vue组件, 所以写这样的一个组件,需要掌握的知识点有:

  1. Vue单文件组件开发
  2. execCommand指令

组件结构

组件结构图
组件结构图

组件由 菜单部分内容区域 2大部分组成, 其中菜单区域又由各种 指令按钮 组成,部分指令按钮还有下拉选项

指令按钮

指令按钮是 execConmand 的装载器,需要实现以下功能

  1. 划过背景变灰
  2. 显示按钮图标
  3. 部分按钮需要实现点击展开下拉菜单
<template>
  <button class="vm-editor-button" :class="{ active: slot }">
    // 显示按钮图标
    <img :src="require('../assets/iconimg/' + icon + '.png')" height="16" width="16" alt="" @click="showSlot">
    <!-- <i :class="icon" @click="showSlot"></i> -->
    // 部分按钮需要实现点击展开下拉菜单
    <slot v-if="slot"></slot>
  </button>
</template>
<style>
  ...
  // 划过显示背景
  button.vm-editor-button:hover{        
    background-color: #eee;
  }
  ...
</style>
export default {
  name: 'VmEditorButton',
  props: {
    icon: {
      type: String,
      default: 'heading'
    }
  },
  data: function () {
    return {
      slot: false
    }
  },
  methods: {
    showSlot () {
      this.slot === false ? this.slot = true : this.slot = false
    }
  }
}
</script>

菜单区域

菜单区域
菜单区域

菜单区域放置按钮,主要实现的功能是

  1. 将execCommand指令绑定到按钮中
  2. 实现点击上传按钮编译html
<template>
  <div class="vm-editor-menu">
    // 引入按钮组建, 通过 **click事件** 绑定封装的 **execCommand方法** 实现样式的变化 
    <VmEditorButton icon="paragraph" @click.native="execCommand('formatBlock', '<p>')">
    </VmEditorButton>
    <VmEditorButton icon="heading">
      <VmEditorDropdown>
        // 这是部分按钮需要下拉菜单功能
        <ul class="vm-editor-ul">
          <li @click="execCommand('formatBlock', '<h1>')">
            <h1>H1</h1>
          </li>
          <li @click="execCommand('formatBlock', '<h2>')">
            <h2>H2</h2>
          </li>
          <li @click="execCommand('formatBlock', '<h3>')">
            <h3>H3</h3>
          </li>
          <li @click="execCommand('formatBlock', '<h4>')">
            <h4>H4</h4>
          </li>
          <li @click="execCommand('formatBlock', '<h5>')">
            <h5>H5</h5>
          </li>
        </ul>
      </VmEditorDropdown>
    </VmEditorButton>
    // 省略其他按钮代码
    ...
    <slot></slot>
  </div>
</template>
<style>
  ...
</style>
<script>
...
export default {
  name: 'VmEditorMenu',
  components: {
    VmEditorButton,
    VmEditorDropdown,
    VmEditorAddlink,
    VmEditorAddimage,
    VmEditorFontcolor
  },
  methods: {
    // 封装 document.execCommand 指令
    execCommand: function (commandName, valueArgument) {
      // let body = document.querySelector('.body');
      if (!valueArgument) {
        valueArgument = null
      }
      document.execCommand('styleWithCSS', null, true)
      document.execCommand(commandName, false, valueArgument)
    },
    // 插入图片功能
    setImage: function (evt) {
      let reader = new FileReader()
      let file = evt.target.files[0]
      reader.readAsDataURL(file)
      reader.onload = function (evt) {
        let base64Image = evt.target.result
        document.execCommand('insertImage', false, base64Image)
      }
    }
  }
}
</script>

主组件

主组件
主组件

主组件就是将 菜单组件内容区域 整合在一起

另外还要实现导出html的功能

<div class="vm-editor">
  <VmEditorMenu>
    <div class="global-control">
      // 到处html的按钮,放在这里因为,需要获取 内容区域 的html数据
      <VmEditorButton icon="upload" @click.native="uploadHtml"></VmEditorButton>
    </div>
  </VmEditorMenu>
  // 内容区域 只要设置 **contenteditable="true"** 就可以了,其他的交给指令去做
  <div class="content" contenteditable="true" v-html="html">
  </div>
</div>
<style>
 ...
</style>
<script>
  name: 'VmEditor',
  components: {
    VmEditorMenu,
    VmEditorButton
  },
  data: function () {
    return {
      html: 'Please Enter ...'
    }
  },
  methods: {
     导出html数据
    // 目前 内容区域 的样式都是 **CSS样式**, 导出时需要转化为 **内联样式**
    uploadHtml: function () {
      // 获取各个模块的 CSS样式
      let style = {
        ul: `
              margin: 10px 20px;
              list-style-type: square;
              padding: 0;
            `,
        ol: `
              margin: 10px 20px;
              list-style-type: decimal;
              padding: 0;
            `,
        li: `
              display: list-item;
              padding: 0;
            `,
        hr: `
              margin: 15px 0;
              border-top: 1px solid #eeeff1;
            `,
        pre: `
              display: block;
              margin: 10px 0;
              padding: 8px;
              border-radius: 4px;
              background-color: #f2f2f2;
              color: #656565;
              font-size: 14px;
             `,
        blockquote: `
                      display: block;
                      border-left: 4px solid #ddd;
                      margin: 15px 0;
                      padding: 0 15px;
                    `,
        img: `
               margin: 20px 0;
             `,
        a: `
            color: #41b883;
           `
      }
      let html = document.getElementsByClassName('content')[0]
      let htmlContainerParent = document.createElement('div')
      let htmlContainer = document.createElement('div')
      let tagNames = Object.keys(style)
      // 遍历html节点并插入对应的内联样式 
      for (let i = 0; i < tagNames.length; i++) {
        let _tagNames = html.getElementsByTagName(tagNames[i])
        if (_tagNames.length > 0) {
          for (let j = 0; j < _tagNames.length; j++) {
            _tagNames[j].style = style[tagNames[i]]
          }
        }
      }
      htmlContainer.style = `
                              text-align: left;
                              padding: 15px;
                              font-size: 16px; 
                            `
      htmlContainer.innerHTML = html.innerHTML
      htmlContainerParent.appendChild(htmlContainer)

      // 注册自定义事件 **upload**
      this.$emit('upload', htmlContainerParent.innerHTML)
    }
  }
}
</script>

其他组件

其他的组建主要是按钮下拉菜单, 因为每个都不一样,所以要独立出来

  1. vm-editor-addimage: 插入图片
  2. vm-editor-addlink: 插入链接
  3. vm-editor-fontcolor: 修改颜色

不足之处

因为这个富文本编辑器的开发时间比较短,没有认真研究类似优秀插件的源码 也没有 深入调研过富文本编辑器的需求。
只是参考了一些同类编辑器的实现效果和UI风格,比如simditor,然后简单实现了一下功能。

所以肯定还有很多需要改善的地方,比较明显的有:

  1. 浏览器的兼容
  2. 表格功能的实现

不管怎样,仅供大家学习使用

原文地址

先这样了, 欢迎star