最近一段时间,工作中的需求用到了富文本编辑器,刚好用到了quill.js,开发过程中遇到了各?#25351;?#26679;的古怪问题,这篇笔记就总结一下。方便其他童鞋参考。由于小呆在开发中使用Vue框架开发。所以采用vue-quill-editor来进行示范。但本质上的依赖还是quill.js。

项目引用

NPM 安装到项目中
npm install vue-quill-editor --save

安装好依赖之后,我们把它引入到项目当?#23567;?/p>

<template>
 <div class="test-wrap">
  <!-- 富文本编辑器 -->
  <quill-editor class="quill-editor" 
   ref="myQuillEditor"
   :content="content"
   @change="onEditorChange($event)"/>
  <!-- 预览 -->
  <h4>这里是内容预览</h4>
  <div class="ql-editor quill-editor-text" v-html="content"></div>
 </div>
</template>

<script>
// require styles
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'

import { quillEditor, Quill } from 'vue-quill-editor'

export default {
 components: {
  quillEditor
 },
 data () {
  return {
   content: '' // 富文本编辑器内容
  }
 },
 methods: {
  /**
   * [onEditorChange 监听富文本内容变化]
   * @param  {String} html [html格式的内容]
   * @param  {String} text [纯文本格式的内容]
   * @param  {Object} quill [quill对象]
  */
  onEditorChange ({html, text, quill}) {
   this.content = html
  }
 }
}
</script>

ok,这个时候,我们在富文本编辑器中输入这是一段测试代码,就会看到如下效果:

运行富文本编辑器

筛选Toobar 功能

通过上面的代码我们已经能运行起来编辑器了,但是顶部长长的功能区却并不一定全都用到,那么如何对功能区进行筛选和隐藏呢?来看下面的代码:

...
<quill-editor class="quill-editor" 
 ref="myQuillEditor"
 :content="content"
 :options="editorOption"
 @change="onEditorChange($event)"/>
...

data () {
 return {
  editorOption: { // 富文本编辑器配置
   placeholder: ‘请输入文本’,
   modules: {
    toolbar: [
     [{ 'size': sizes }], // 字体大小
     [{ 'font': fonts }], // 字体类型
     ['bold', 'italic', 'underline', 'strike'], //加粗,斜体,下划线,删除线
     ['blockquote'], //引用
     [{ 'script': 'sub' }, { 'script': 'super' }], // 上下标
     [{ 'color': [] }, { 'background': [] }], // 文字颜色 背?#25226;?#33394;
     [{ 'align': [] }], // 对齐方式
     ['clean'] //清除字体样式
    ]
   }
  }
 }
}
...

通过options来对富文本编辑器进行配置,通过placeholder属性来设置提示语,通过modules属性来配置toolbar功能区的功能。这样我们便得到了以下的效果。(更多功能配?#20204;?#21442;考Quill.js配置文档

配置Toolbar

设置自定义字体大小及字体类型

一个好的富文本编辑器,那必然是高度自由化,可配置化的。quill也不例外,虽然它是一款国外的富文本编辑器,但是我们还是可以通过配置来适应国内的某些需求,比如自定义字体类型,自定义字体大小。接下来我们就将对这两个功能进行配置。

...
import { quillEditor, Quill } from 'vue-quill-editor'

// 注册自定义的字体类型
const Font = Quill.import('formats/font')
const fonts = ['SimSun', 'SimHei', 'Microsoft-YaHei', 'KaiTi', 'FangSong', 'Arial']
Font.whitelist = fonts
Quill.register(Font, true)

// 注册自定义的字体大小
const Size = Quill.import('attributors/style/size')
const sizes = ['10px', '12px', '14px', '16px', '18px', '20px', '24px', '30px']
Size.whitelist = sizes
Quill.register(Size, true)
...

data () {
 return {
  content: '', // 富文本编辑器内容
  editorOption: { // 富文本编辑器配置
   placeholder: '请输入文本',
   modules: {
   toolbar: [
    [{ 'size': sizes }], // 字体大小
    [{ 'font': fonts }], // 字体类型
    ...
   ]
  }
 }
}

<style>
/* 自定义字体大小 */
.ql-toolbar .ql-picker.ql-size .ql-picker-label[data-value="10px"]::before,
.ql-toolbar .ql-picker.ql-size .ql-picker-item[data-value="10px"]::before {
  content: '10px';
  font-size: 14px;
}
.ql-toolbar .ql-picker.ql-size .ql-picker-label[data-value="11px"]::before,
.ql-toolbar .ql-picker.ql-size .ql-picker-item[data-value="11px"]::before {
  content: '11px';
  font-size: 14px;
}
.ql-toolbar .ql-picker.ql-size .ql-picker-label[data-value="12px"]::before,
.ql-toolbar .ql-picker.ql-size .ql-picker-item[data-value="12px"]::before {
  content: '12px';
  font-size: 14px;
}
.ql-toolbar .ql-picker.ql-size .ql-picker-label[data-value="14px"]::before,
.ql-toolbar .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before {
  content: '14px';
  font-size: 14px;
}
.ql-toolbar .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before,
.ql-toolbar .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before {
  content: '16px';
  font-size: 14px;
}
.ql-toolbar .ql-picker.ql-size .ql-picker-label[data-value="18px"]::before,
.ql-toolbar .ql-picker.ql-size .ql-picker-item[data-value="18px"]::before {
  content: '18px';
  font-size: 14px;
}
.ql-toolbar .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before,
.ql-toolbar .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before {
  content: '20px';
  font-size: 14px;
}
.ql-toolbar .ql-picker.ql-size .ql-picker-label[data-value="24px"]::before,
.ql-toolbar .ql-picker.ql-size .ql-picker-item[data-value="24px"]::before {
  content: '24px';
  font-size: 14px;
}
.ql-toolbar .ql-picker.ql-size .ql-picker-label[data-value="30px"]::before,
.ql-toolbar .ql-picker.ql-size .ql-picker-item[data-value="30px"]::before {
  content: '30px';
  font-size: 14px;
}

/* 自定义字体类型 */
.ql-font-SimSun {
  font-family: 宋体, SimSun, STSong!important;
}
.ql-font-SimHei {
  font-family: 黑体, SimHei, STHeiti!important;
}
.ql-font-Microsoft-YaHei {
  font-family: 微软雅黑, "Microsoft Yahei"!important;
}
.ql-font-KaiTi {
  font-family: 楷体, 楷体_GB2312, KaiTi, STKaiti!important;
}
.ql-font-FangSong {
  font-family: 仿宋, FangSong, STFangsong!important;
}
.ql-toolbar .ql-picker.ql-font .ql-picker-label[data-value=SimSun]::before,
.ql-toolbar .ql-picker.ql-font .ql-picker-item[data-value=SimSun]::before {
  content: "宋体";
  font-family: 宋体, SimSun, STSong;
}
.ql-toolbar .ql-picker.ql-font .ql-picker-label[data-value=SimHei]::before,
.ql-toolbar .ql-picker.ql-font .ql-picker-item[data-value=SimHei]::before {
  content: "黑体";
  font-family: 黑体, SimHei, STHeiti;
}
.ql-toolbar .ql-picker.ql-font .ql-picker-label[data-value=Microsoft-YaHei]::before,
.ql-toolbar .ql-picker.ql-font .ql-picker-item[data-value=Microsoft-YaHei]::before {
  content: "微软雅黑";
  font-family: 微软雅黑, "Microsoft Yahei";
}
.ql-toolbar .ql-picker.ql-font .ql-picker-label[data-value=KaiTi]::before,
.ql-toolbar .ql-picker.ql-font .ql-picker-item[data-value=KaiTi]::before {
  content: "楷体";
  font-family: 楷体, 楷体_GB2312, KaiTi, STKaiti;
}
.ql-toolbar .ql-picker.ql-font .ql-picker-label[data-value=FangSong]::before,
.ql-toolbar .ql-picker.ql-font .ql-picker-item[data-value=FangSong]::before {
  content: "仿宋";
  font-family: 仿宋, FangSong, STFangsong;
}
.ql-toolbar .ql-picker.ql-font .ql-picker-label[data-value=Arial]::before,
.ql-toolbar .ql-picker.ql-font .ql-picker-item[data-value=Arial]::before {
  content: "Arial";
  font-family: "Arial";
}

.ql-font-SimSun {
  font-family: "SimSun";
}
.ql-font-SimHei {
  font-family: "SimHei";
}
.ql-font-Microsoft-YaHei {
  font-family: "Microsoft YaHei";
}
.ql-font-KaiTi {
  font-family: "KaiTi";
}
.ql-font-FangSong {
  font-family: "FangSong";
}
.ql-font-Arial {
  font-family: "Arial";
}
</style>

自定义字体及大小

设置最大可输入长度

在工作中,另一个比较常见的需求便是对编辑器的内容进行限制,如果是使用textarea的话,那我们只需要加一个maxlength属性?#32431;傘?#20294;是在quill中却并没有配置该功能的方法,这时候我们要如?#38382;迪指?#21151;能呢?下面是小呆实?#25351;?#21151;能的思路:

在keydown 事件中,获取当前编辑器内容的长度,如果输入的长度超出最大限制,就限制用户继续输入。

...
<quill-editor class="quill-editor"
 ref="myQuillEditor"
 :content="content"
 :options="editorOption"
 @change="onEditorChange($event)"
 @keydown.native="keydown($event, 5)"/>
...
computed: {
 editor () {
  return this.$refs['myQuillEditor'].quill
 }
},
methods: {
 /**
  * [keydown 非中文下 处理最大可输入?#27573;
  * @param {Object} event evt对象
  * @param {Number} decimalNum 最大输入数
  */
 keydown (event, decimalNum) {
  let length = this.editor.getLength()
  if (length > decimalNum && event.key !== 'Backspace') {
   event.preventDefault()
  }
 }
}

这个时候,小呆在没有输入中文的情况下,确实实现了最大可输入?#27573;А?#20294;是当小呆打开输入法,输入中文的时候,中文还是能输入进去的。也就是说这个方法只针对国外的用户管用,那么如何对中文也进?#23633;?#21548;呢?小呆想到了composition这个事件。?#34892;?#36259;的童鞋可以参考该文档MDN 文本写作事件

由于quill采用delta的方式来记录?#30475;问?#20837;的变化,于是我们可以利用这一点,配合focus事件composition,来对中文的输入进?#23633;?#21548;及判断。关于delta的更多使用,请参考quill官方文档

...
<quill-editor class="quill-editor"
 ref="myQuillEditor"
 :content="content"
 :options="editorOption"
 @change="onEditorChange($event)"
 @focus="onEditorFocus($event)"
 @compositionstart.native="compositionstart($event)"
 @compositionend.native="compositionend($event, 5)"
 @keydown.native="keydown($event, 5)"/>
...
data () {
 return {
  ...
  focusDelta: {}, // focusDelta
  isInputChines: false, // 是否在输入中文
    ...
 }
},
methods: {
 ...
 /**
  * [onEditorFocus 富文本编辑框focus]
  * @param  {Object} quill [quill对象]
  */
 onEditorFocus (quill) {
  this.focusDelta = this.editor.getContents()
 },
 /**
  * [compositionstart 监听是否为中文输入]
  * @param {Object} event evt对象
  */
 compositionstart (event) {
  this.isInputChines = true
 },
 /**
  * [compositionend 监听中文输入结束]
  * @param {Object} event evt对象
  * @param {Number} decimalNum 最大输入数
 */
 compositionend (event, decimalNum) {
  this.isInputChines = false
  let delta = this.editor.getContents()
  let length = 0
  delta.ops.forEach(item => {
   if (item.insert) {
    length = length + item.insert.length
   }
  })
  length = length - 1 // 默认有个空格长度是1所以需要减1
  if (length > decimalNum) {
   this.editor.setContents(this.focusDelta)
  } else {
   this.focusDelta = delta
  }
 },
 /**
  * [keydown 非中文下 处理最大可输入?#27573;
  * @param {Object} event evt对象
  * @param {Number} decimalNum 最大输入数
 */
 keydown (event, decimalNum) {
  if (!this.isInputChines) {
   let length = this.editor.getLength()
   if (length > decimalNum && event.key !== 'Backspace') {
    event.preventDefault()
   }
  }
 }
}

最后附上完整代码