在 vue 項目中使用 quill-editor 2.0
更多精彩
- 更多技術(shù)博客鹃操,請移步 IT人才終生實訓(xùn)與職業(yè)進階平臺 - 實訓(xùn)在線
寫在前面的話
- 之前做過 vue-quill-editor 富文本框編輯器 ,這個版本是基于 Github 上一個已經(jīng)集成好的組件做的二次開發(fā)
- 這個組件是基于 quill@1.3.6 犀变,但是現(xiàn)在需要編輯器能支持插入表格拉岁,這個需求 quill@1.3.6 做不到
- 但是 quill@2.0.0-dev.3 支持在編輯器中插入表格,不過這不是正式版,而是開發(fā)版
- 而原版的 vue-quill-editor 兩年前就沒更新了嗽上,所以 quill 的版本一直停留在 1.x
- 那么要實現(xiàn)新需求,就只能重新集成一個新的了
相關(guān)網(wǎng)址
基礎(chǔ)功能集成
- 最初的實現(xiàn)引導(dǎo)來自 在Vue中使用富文本編輯器Quill - SegmentFault 思否
項目引入 quill@2.x
- 從 Releases · quilljs/quill · GitHub 可以看到當前官方的正式版是 1.3.7
- 不過直接進入 GitHub - quilljs/quill 的首頁會發(fā)現(xiàn)項目的默認分支已經(jīng)切換到了開發(fā)版熄攘,所以 2.x 版本雖然是開發(fā)版兽愤,但實際用起來不會有什么問題,完全可以放心使用
- 通過
npm view quill
可以看到當前的開發(fā)版本的具體版本號是 2.0.0-dev.3 挪圾,所以直接在項目根目錄使用npm install quill@2.0.0-dev.3 --save
安裝即可浅萧,如下圖
創(chuàng)建編輯器組件
- 真正會被渲染成編輯器的 DIV 是
in-editor
,其外層的in-editor-wrapper
只是作為父級包裹一層- 因為當編輯器初始化后哲思,其結(jié)構(gòu)是如下圖
-
所以如果沒有父級 DIV 進行包裹洼畅,初始化的時候會拋出沒有容器的錯誤
<template>
<div class="in-editor-wrapper">
<div class="in-editor"></div>
</div>
</template>
<script>
// 引入原始組件
import Quill from 'quill'
// 引入核心樣式和主題樣式
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
export default {
name: 'inEditor',
props: {
// 用于雙向綁定
value: String
},
data () {
return {
// 待初始化的編輯器
editor: null,
// 配置參數(shù)
options: {
theme: 'snow',
modules: {
// 工具欄的具體配置
toolbar: {
container: [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{'list': 'ordered'}, {'list': 'bullet'}],
[{'script': 'super'}],
[{'indent': '-1'}, {'indent': '+1'}],
[{'size': ['small', false, 'large', 'huge']}],
[{'header': [1, 2, 3, 4, 5, 6, false]}],
[{'color': []}, {'background': []}],
[{'align': []}],
['link', 'image']
]
}
},
placeholder: '請輸入內(nèi)容 ...'
}
}
},
watch: {
// 監(jiān)聽外部值的傳入,用于將值賦予編輯器
'value' (val) {
// 如果編輯器沒有初始化棚赔,則停止賦值
if (!this.editor) {
return
}
// 獲取編輯器當前內(nèi)容
let content = this.editor.root.innerHTML
// 外部傳入了新值帝簇,而且與當前編輯器的內(nèi)容不一致
if (val && val !== content) {
// 將外部傳入的HTML內(nèi)容轉(zhuǎn)換成編輯器識別的delta對象
let delta = this.editor.clipboard.convert({
html: val
})
// 編輯器的內(nèi)容需要接收delta對象
this.editor.setContents(delta)
}
}
},
mounted () {
// 初始化編輯器
this._initEditor()
},
methods: {
// 初始化編輯器
_initEditor () {
// 獲取編輯器的DOM容器
let editorDom = this.$el.querySelector('.in-editor')
// 初始化編輯器
this.editor = new Quill(editorDom, this.options)
// 雙向綁定
this.editor.on('text-change', () => {
this.$emit('input', this.editor.root.innerHTML)
})
}
}
}
</script>
<style lang="stylus" type="text/stylus">
.in-editor-wrapper
flex-grow 1
display flex
flex-direction column
overflow hidden
.ql-toolbar
.ql-formats
.ql-picker-label
&::before
position relative
top -5px
button
i.icon
font-size 14px
.ql-container
flex-grow 1
height 0
overflow hidden
</style>
使用編輯器組件
- 按正常的組件引入方式即可
<template>
<in-editor v-model="description"></in-editor>
</template>
<script type="text/ecmascript-6">
import inEditor from "components/in-editor"
export default {
name: "inEditorForm",
data() {
return {
description: 'Hello World'
};
},
components: {
inEditor
}
};
</script>
<style lang="stylus" type="text/stylus"></style>
啟用表格功能
配置工具欄
- 在 2.x 中原始版本就已經(jīng)支持插入表格,只需要按照如下代碼的方式靠益,在 toolbar 中配置對應(yīng)按鈕即可
options: {
theme: 'snow',
modules: {
// 啟用表格功能
table: true,
toolbar: {
container: [
// 為了減少代碼內(nèi)容丧肴,這里被省略了,可以參考上述代碼
...
[
{'table': 'TD'},
{'table-insert-row': 'TIR'},
{'table-insert-column': 'TIC'},
{'table-delete-row': 'TDR'},
{'table-delete-column': 'TDC'}
]
]
}
},
placeholder: this.placeholder
}
渲染表格按鈕的圖標
-
按照上述代碼對工具欄進行配置后胧后,只能在工具欄上看到 1 個按鈕芋浮,后續(xù) 4 個按鈕都看不到,如下圖
- 其實在上圖的第 1 個按鈕后面還有 4 個按鈕壳快,查看代碼結(jié)構(gòu)可知纸巷,如下圖
-
可以很清楚的看到江醇,一共生成了 5 個按鈕,但只有第一個按鈕中有 SVG 格式的圖標何暇,后續(xù) 4 個按鈕都是空的
-
- 為什么后續(xù) 4 個按鈕沒有圖標陶夜,難道因為開發(fā)版的原因,表格功能的按鈕圖標還沒有完整提供裆站?
- 其實并不是条辟,查看 2.x 的源碼目錄,在
/assets/icons
目錄下可以看到表格的圖標非常完整宏胯,如下圖
- 那么為什么有圖標羽嫡,但是卻不顯示?
- 繼續(xù)查看 2.x 的源碼目錄肩袍,在
/ui/icons.js
中找到如下代碼- 可以看到關(guān)于表格的按鈕杭棵,就只引入了第 1 個
- 后續(xù) 4 個按鈕雖然有提供 SVG 文件,但并沒有引入
...
import tableIcon from '../assets/icons/table.svg';
...
export default {
...
table: tableIcon,
...
};
- 既然按鈕圖標的 SVG 文件是有的氛赐,那么只需要擴充一下
/ui/icons.js
即可-
let icons = Quill.import('ui/icons')
就是調(diào)用 quill 的原生圖標庫魂爪,從 Issue #1099 · quilljs/quill · GitHub 學(xué)來的 - 之后通過 Lodash 遍歷準備好的自定義圖標庫,將其逐個插入到
/ui/icons.js
中即可 - 注意艰管,是在編輯器組件初始化之前將自定義圖標庫插入原生圖標庫
-
<script>
import Quill from 'quill'
import _ from 'lodash'
import { ICON_SVGS } from 'components/in-editor/ui/icon'
export default {
...
mounted () {
this._initCustomToolbarIcon()
this._initEditor()
},
methods: {
_initCustomToolbarIcon () {
// 獲取quill的原生圖標庫
let icons = Quill.import('ui/icons')
// 從自定義圖標SVG列表中找到對應(yīng)的圖標填入到原生圖標庫中
_.forEach(ICON_SVGS, (iconValue, iconName) => {
icons[iconName] = iconValue
})
}
...
}
}
</script>
-
ICON_SVGS
所在文件內(nèi)容如下- 這里其實是一個敗筆滓侍,本來是想通過和
/ui/icons.js
一樣的方式把 SVG 文件通過import
方式引入 - 但不管怎么嘗試獲得的都是文本內(nèi)容,最后不得以只能采用如下方式
- 后期找到解決方案會再次優(yōu)化
- 這里其實是一個敗筆滓侍,本來是想通過和
export const ICON_SVGS = {
'table-insert-row': `<svg viewbox="0 0 18 18">
<g class="ql-fill ql-stroke ql-thin ql-transparent">
<rect height="3" rx="0.5" ry="0.5" width="7" x="4.5" y="2.5"></rect>
<rect height="3" rx="0.5" ry="0.5" width="7" x="4.5" y="12.5"></rect>
</g>
<rect class="ql-fill ql-stroke ql-thin" height="3" rx="0.5" ry="0.5" width="7" x="8.5" y="7.5"></rect>
<polygon class="ql-fill ql-stroke ql-thin" points="4.5 11 2.5 9 4.5 7 4.5 11"></polygon>
<line class="ql-stroke" x1="6" x2="4" y1="9" y2="9"></line>
</svg>`,
'table-insert-column': `<svg viewbox="0 0 18 18">
<g class="ql-fill ql-transparent">
<rect height="10" rx="1" ry="1" width="4" x="12" y="2"></rect>
<rect height="10" rx="1" ry="1" width="4" x="2" y="2"></rect>
</g>
<path class="ql-fill" d="M11.354,4.146l-2-2a0.5,0.5,0,0,0-.707,0l-2,2A0.5,0.5,0,0,0,7,5H8V6a1,1,0,0,0,2,0V5h1A0.5,0.5,0,0,0,11.354,4.146Z"></path>
<rect class="ql-fill" height="8" rx="1" ry="1" width="4" x="7" y="8"></rect>
</svg>`,
'table-delete-row': `<svg viewbox="0 0 18 18">
<g class="ql-fill ql-stroke ql-thin ql-transparent">
<rect height="3" rx="0.5" ry="0.5" width="7" x="4.5" y="2.5"></rect>
<rect height="3" rx="0.5" ry="0.5" width="7" x="4.5" y="12.5"></rect>
</g>
<rect class="ql-fill ql-stroke ql-thin" height="3" rx="0.5" ry="0.5" width="7" x="8.5" y="7.5"></rect>
<line class="ql-stroke ql-thin" x1="6.5" x2="3.5" y1="7.5" y2="10.5"></line>
<line class="ql-stroke ql-thin" x1="3.5" x2="6.5" y1="7.5" y2="10.5"></line>
</svg>`,
'table-delete-column': `<svg viewbox="0 0 18 18">
<g class="ql-fill ql-transparent">
<rect height="10" rx="1" ry="1" width="4" x="2" y="6"></rect>
<rect height="10" rx="1" ry="1" width="4" x="12" y="6"></rect>
</g>
<rect class="ql-fill" height="8" rx="1" ry="1" width="4" x="7" y="2"></rect>
<path class="ql-fill" d="M9.707,13l1.146-1.146a0.5,0.5,0,0,0-.707-0.707L9,12.293,7.854,11.146a0.5,0.5,0,0,0-.707.707L8.293,13,7.146,14.146a0.5,0.5,0,1,0,.707.707L9,13.707l1.146,1.146a0.5,0.5,0,0,0,.707-0.707Z"></path>
</svg>`
}
-
按照上述方式渲染完成后的表格圖標如下圖
擴展表格按鈕的功能
- 表格按鈕的圖標渲染完成后牲芋,點擊這幾個表格按鈕會發(fā)現(xiàn)撩笆,只有第 1 個按鈕有效果,后續(xù) 4 個按鈕沒有任何反應(yīng)
- 這其實很容易想通缸浦,畢竟后續(xù) 4 個按鈕默認連圖標都沒有渲染夕冲,所以功能自然也是需要自定義的
- 實現(xiàn)方式同樣參考自 在Vue中使用富文本編輯器Quill - SegmentFault 思否
- 和上文中稍有區(qū)別的位置在于我把按鈕的觸發(fā)事件聲明在
methods
中,而不是直接聲明在options.modules.toolbar.handlers
中- 因為按照上文直接聲明在配置中裂逐,會出現(xiàn)編輯器還沒有初始化導(dǎo)致
this.editor = undefined
的情況
- 因為按照上文直接聲明在配置中裂逐,會出現(xiàn)編輯器還沒有初始化導(dǎo)致
- 按照如下代碼配置后歹鱼,編輯器的表格插入功能就大功告成了
<script>
...
export default {
...
data () {
return {
...
options: {
modules: {
toolbar: {
container: [
...
],
handlers: {
'table': this._tableHandler,
'table-insert-row': this._tableInsertRowHandler,
'table-insert-column': this._tableInsertColumnHandler,
'table-delete-row': this._tableDeleteRowHandler,
'table-delete-column': this._tableDeleteColumnHandler
}
}
}
}
}
},
methods: {
...
_tableHandler () {
this.editor.getModule('table').insertTable(2, 3)
},
_tableInsertRowHandler () {
this.editor.getModule('table').insertRowBelow()
},
_tableInsertColumnHandler () {
this.editor.getModule('table').insertColumnRight()
},
_tableDeleteRowHandler () {
this.editor.getModule('table').deleteRow()
},
_tableDeleteColumnHandler () {
this.editor.getModule('table').deleteColumn()
}
}
}
</script>
重寫圖片上傳功能
- quill 的原生圖片上傳是通過將待上傳的圖片文件轉(zhuǎn)義成 BASE64 格式后直接插入到文本中
- 圖片會作為文本的一部分被直接傳入后端,進行持久化操作
- BASE64 格式的圖片非常冗長絮姆,這不是個好的解決方案醉冤,所以需要優(yōu)化
引入圖片上傳模塊
- GitHub - NextBoy/quill-image-extend-module 是在 quill@1.3.6 階段就非常好用的圖片上傳模塊
- 實測發(fā)現(xiàn)在 quill@2.x 中也能正常使用秩霍,在項目根目錄執(zhí)行
npm install quill-image-extend-module --save-dev
安裝即可- 不過該模塊的作者已經(jīng)在 README 中表示不再維護了篙悯,所以有時間的話,我可能會自己參考著重寫一份铃绒,方便后期維護
- 接下來在組件按照如下方式中引入模塊
- 從
quill-image-extend-module
中引入了兩個模塊鸽照,分別是ImageExtend
和QuillWatch
-
ImageExtend
用于進行圖片上傳功能的重寫,因為是自定義的modules
颠悬,所以放在options.modules
中矮燎,和toolbar
同級 -
QuillWatch
用于監(jiān)聽圖片上傳的操作定血,監(jiān)聽操作需要放置在options.modules.toolbar.handles.image
中,表示是監(jiān)聽圖片按鈕的點擊操作
- 從
<script>
import Quill from 'quill'
import { ImageExtend, QuillWatch } from 'quill-image-extend-module'
Quill.register('modules/ImageExtend', ImageExtend)
export default {
...
data () {
return {
options: {
modules: {
toolbar: {
container: [
...
['link', 'image']
],
handlers: {
...
'image': this._imageHandler
}
},
ImageExtend: {
loading: true,
name: 'image',
size: 2,
action: `/api/file/upload/image`,
response: (res) => {
return res.data
}
}
}
}
}
},
methods: {
_imageHandler () {
QuillWatch.emit(this.quill.id)
}
}
}
</script>
- 實際上是通過異步的表單上傳方式將圖片上傳到了服務(wù)端诞外,如果服務(wù)端代碼正好是 Java 澜沟,又正好是 SpringBoot ,可以參考以下代碼作為服務(wù)端的圖片接收接口
@RestController
@RequestMapping("/api/file/upload")
public class FileUploadController {
@PostMapping("")
public ResponseData fileUpload(@RequestParam MultipartFile file) {
...
}
}
設(shè)置圖片大小
- 原生的圖片上傳功能峡谊,不僅圖片是作為 BASE64 格式進行保存茫虽,而且上傳的圖片無法修改大小
- 就算將圖片的上傳方式修改后,圖片依舊是無法直接修改大小的既们,這塊的需求也需要手動實現(xiàn)
- 在 quill@1.x 版本中濒析,GitHub - kensnyder/quill-image-resize-module 是一個非常好用的調(diào)整圖片大小的擴展模塊
- 在之前使用的 vue-quill-editor/04-example.vue · GitHub 都有提供集成案例
- 但是當我準備把這個模塊引入到 2.x 中的時候,安裝過程中提示這個模塊引入的依賴包都太老了啥纸,瘋狂報錯号杏,所以最后只能罷休
-
要自己實現(xiàn)一個功能如此強大的模塊,肯定是有點來不及斯棒,所以我做了一個極簡版盾致,實際操作效果如下圖
實現(xiàn)代碼
- 在初始化編輯器時,通過
this.editor.root.addEventListener
監(jiān)聽編輯器內(nèi)容的雙擊事件 - 如果雙擊的對象是圖片荣暮,則彈出一個對話框绰上,
this.$prompt()
是 ElementUI 的全局函數(shù),用于彈出對話框 - 在對話框中輸入準備修改的圖片寬度渠驼,即可按比例調(diào)整圖片的大小
<script>
...
export default {
...
methods: {
_initEditor () {
let editorDom = this.$el.querySelector('.in-editor')
this.editor = new Quill(editorDom, this.options)
// 監(jiān)聽圖片點擊
this.editor.root.addEventListener('dblclick', this._initImageResize, false)
// 雙向綁定
this.editor.on('text-change', () => {
this.$emit('input', this.editor.root.innerHTML)
})
},
_initImageResize (event) {
let currentTarget = event.target
// 判斷當前點擊的是不是圖片
if (currentTarget && currentTarget.tagName && currentTarget.tagName.toUpperCase() === 'IMG') {
this.$prompt('請輸入寬度', '提示', {
inputValue: currentTarget.width,
confirmButtonText: '確定',
cancelButtonText: '取消'
}).then(({value}) => {
// 賦值新寬度
currentTarget.width = value
}).catch(() => {})
}
}
}
}
</script>
實現(xiàn)編輯器的全屏擴展
- 有時候受制于表單頁的布局蜈块,編輯器的可編輯區(qū)域會比較拘謹
- 所以需要在工具欄上提供一個按鈕,可以讓編輯器實現(xiàn)瀏覽器范圍內(nèi)的全屏放大
- 這個需求沒有使用現(xiàn)成的擴展組件迷扇,而是結(jié)合** ElementUI** 的
el-dialog
組件實現(xiàn)了效果
在編輯器組件中引入 el-dialog
-
el-dialog
是 ElementUI 的模態(tài)窗口百揭,具體用法參考 Element - component/dialog-
fullscreen
表示窗口打開時是直接全屏展現(xiàn)的
-
- 在
el-dialog
中增加了一個 DIV ,標記為in-full-editor
蜓席,用于在窗口展現(xiàn)時初始化一個用于全屏顯示的編輯器
<template>
<div class="in-editor-wrapper">
<div class="in-editor"></div>
<el-dialog
modal
append-to-body
fullscreen
custom-class="in-editor-modal"
:visible.sync="fullEditorShow"
:title="title">
<div class="in-full-editor"></div>
</el-dialog>
</div>
</template>
在工具欄上增加一個全屏按鈕
- 從如下代碼中可以看到器一,在工具欄上新增了一個
expand
按鈕,顯示效果如下圖
- 圖標內(nèi)容同樣直接存放在之前的
import { ICON_SVGS } from 'components/in-editor/ui/icon'
中厨内,內(nèi)容如下- 之前的表格按鈕內(nèi)容被省略了
- 至于初始化按鈕的函數(shù)中不需要做任何更改
export const ICON_SVGS = {
...
'expand': `<svg viewBox="0 0 18 18">
<path d="M5.797 9.76a.6.6 0 1 1 .849.848L2.253 15h3.379a.6.6 0 0 1 .592.503l.008.097a.6.6 0 0 1-.6.6H.8a.612.612 0 0 1-.162-.022A.6.6 0 0 1 .2 15.6v-4.832a.6.6 0 0 1 1.2 0l-.001 3.389zM15.588.2a.61.61 0 0 1 .176.025l.041.016a.373.373 0 0 1 .053.021c.007.006.015.01.022.014a.599.599 0 0 1 .31.588l-.002 4.768a.6.6 0 0 1-1.2 0V2.254L10.6 6.642a.6.6 0 0 1-.765.07l-.083-.07a.6.6 0 0 1 0-.848L14.144 1.4h-3.388a.6.6 0 0 1-.592-.503L10.156.8a.6.6 0 0 1 .6-.6z"/>
</svg>`
}
實現(xiàn)兩個編輯器之間的數(shù)據(jù)交互
- 從如下代碼中可以看到祈秕,
fullEditor
的初始化并不在mounted()
函數(shù)中,而是通過監(jiān)聽fullEditorShow
的顯示來對fullEditor
進行初始化- 因為當全屏窗口沒有展現(xiàn)之前雏胃,全屏編輯器自然也是不需要初始化的
-
expand
按鈕點擊后觸發(fā)的_expandHandler()
函數(shù)會修改fullEditorShow = true
请毛,全屏窗口就會展現(xiàn) - 在
_initFullEditor()
函數(shù)中,對全屏編輯器中了初始化以及賦值操作- 首先要判斷全屏編輯器是否已經(jīng)初始化瞭亮,防止重復(fù)初始化方仿,在 quill@2.x 中已經(jīng)移除了
destory()
函數(shù),所以需要通過這種方式判斷 - 然后初始化操作需要放置在
$nextTick()
函數(shù)中,因為初始化需要在 DOM 元素加載完畢后才能進行 - 最后賦值操作則是直接從
this.editor
中獲取仙蚜,但需要通過setTimeout(() => {}, 20)
做一個小小的延遲此洲,防止編輯器的初始化還沒有完成
- 首先要判斷全屏編輯器是否已經(jīng)初始化瞭亮,防止重復(fù)初始化方仿,在 quill@2.x 中已經(jīng)移除了
- 仔細對比
_initFullEditor()
和_initEditor()
函數(shù)可以發(fā)現(xiàn),在全屏編輯器的初始化函數(shù)中委粉,沒有對數(shù)據(jù)進行雙向綁定- 因為全屏編輯器在打開的情況下呜师,當前瀏覽器窗口就只能處理編輯器的數(shù)據(jù),要想處理其他操作贾节,則需要先退出全屏編輯器
- 所以只需要監(jiān)聽全屏窗口的打開和關(guān)閉狀態(tài)匣掸,在打開時將原始編輯器的內(nèi)容賦值給全屏編輯器,在關(guān)閉時將全屏編輯器的內(nèi)容賦值給原始編輯器即可
<script>
...
export default {
...
data () {
return {
title: '請輸入內(nèi)容',
editor: null,
fullEditor: null,
fullEditorShow: false,
options: {
modules: {
toolbar: {
container: [
...
['expand']
],
handlers: {
...
'expand': this._expandHandler
}
}
}
}
}
},
watch: {
...
'fullEditorShow' (val) {
if (val) {
this._initFullEditor()
} else {
this.editor.setContents(this.fullEditor.getContents())
}
}
},
methods: {
...
_initFullEditor () {
// 全屏編輯器不存在氮双,則初始化
if (!this.fullEditor) {
this.$nextTick(() => {
let fullEditorDom = document.querySelector('.in-full-editor')
this.fullEditor = new Quill(fullEditorDom, this.options)
this.fullEditor.root.addEventListener('dblclick', this._initImageResize, false)
})
}
// 將當前編輯器的內(nèi)容賦值給全屏編輯器
setTimeout(() => {
this.fullEditor.setContents(this.editor.getContents())
}, 20)
},
...
_expandHandler () {
this.fullEditorShow = !this.fullEditorShow
}
}
}
</script>
全屏編輯器在全屏窗口中的樣式參考
- 只提供參考碰酝,樣式這種東西,根據(jù)實際情況靈活調(diào)整即可
.in-editor-modal
&.is-fullscreen
display flex
flex-direction column
.el-dialog__header
flex 0 0 24px
.el-dialog__body
flex-grow 1
padding 0
display flex
flex-direction column
overflow hidden