最近一段時間,工作中的需求用到了富文本編輯器,剛好用到了quill.js,開發過程中遇到了各種各樣的古怪問題,這篇筆記就總結一下。方便其他童鞋參考。由于小呆在開發中使用Vue框架開發。所以采用vue-quill-editor來進行示范。但本質上的依賴還是quill.js。

項目引用

NPM 安裝到項目中
npm install vue-quill-editor --save

安裝好依賴之后,我們把它引入到項目當中。

<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': [] }], // 文字顏色 背景顏色
     [{ 'align': [] }], // 對齊方式
     ['clean'] //清除字體樣式
    ]
   }
  }
 }
}
...

通過options來對富文本編輯器進行配置,通過placeholder屬性來設置提示語,通過modules屬性來配置toolbar功能區的功能。這樣我們便得到了以下的效果。(更多功能配置請參考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屬性即可。但是在quill中卻并沒有配置該功能的方法,這時候我們要如何實現該功能呢?下面是小呆實現該功能的思路:

在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 非中文下 處理最大可輸入范圍]
  * @param {Object} event evt對象
  * @param {Number} decimalNum 最大輸入數
  */
 keydown (event, decimalNum) {
  let length = this.editor.getLength()
  if (length > decimalNum && event.key !== 'Backspace') {
   event.preventDefault()
  }
 }
}

這個時候,小呆在沒有輸入中文的情況下,確實實現了最大可輸入范圍。但是當小呆打開輸入法,輸入中文的時候,中文還是能輸入進去的。也就是說這個方法只針對國外的用戶管用,那么如何對中文也進行監聽呢?小呆想到了composition這個事件。感興趣的童鞋可以參考該文檔MDN 文本寫作事件

由于quill采用delta的方式來記錄每次輸入的變化,于是我們可以利用這一點,配合focus事件composition,來對中文的輸入進行監聽及判斷。關于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 非中文下 處理最大可輸入范圍]
  * @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()
   }
  }
 }
}

最后附上完整代碼