【注意】本文发布之后,插件有可能会继续更新。所以,最新的使用方法请参考插件文档
前言
wangEditor V5 发布在即,为了验证它的扩展性,我近期做了几个第三方插件。可查看插件列表。
本文分享 @
mention 插件的设计和实现,使用 Vue 做一个示例,源码在本文末尾。能用于 Vue ,也就能用于 Vue3 React 等其他框架。
使用 @wangeditor/plugin-mention
首先,需要了解 wangEditor V5 的基本使用,然后再查阅 @wangeditor/plugin-mention
的文档。
安装和注册
安装第三方插件
yarn add @wangeditor/plugin-mention
复制代码
注册到 wangEditor
import { Boot } from '@wangeditor/editor'
import mentionModule from '@wangeditor/plugin-mention'
// 注册。要在创建编辑器之前注册,且只能注册一次,不可重复注册。
Boot.registerModule(mentionModule)
复制代码
注册之后,编辑器讲监听 @
输入,并触发编辑器配置中的 showModal
和 hideModal
定义 showModal
和 hideModal
输入 @
之后,需要什么样子的弹框?每个项目都有不同的需求。有些比较简单,有些需要搜索,有些可能需要 ajax 异步加载...
所以,这个弹框需要你自己定义。你可以用普通 <div>
也可以用 Vue React 组件,如下文的 MentionModal
组件。
定义好弹框之后,你只需要传入 showModal
和 hideModal
到编辑器配置中,编辑器会适时调用它们。
import { IEditorConfig } from '@wangeditor/editor'
// 编辑器配置
const editorConfig: Partial<IEditorConfig> = {
EXTEND_CONF: {
mentionConfig: {
showModal() {
// 显示弹框,并定位到光标处
},
hideModal() {
// 隐藏弹框
},
},
},
// 其他配置...
}
// 创建 editor ,将会用到 editorConfig
复制代码
PS:获取光标位置其实很简单
// 获取光标位置,定位 modal
const domSelection = document.getSelection()
const domRange = domSelection.getRangeAt(0)
const selectionRect = domRange.getBoundingClientRect()
复制代码
插入 mention 节点
何时插入 mention 节点?有时是 <input>
回车时,有时是 click
某个 elem 时...所以,这个也需要开发者自己定义。
无论何时插入,只需要调用如下的函数即可。此时编辑器将插入一个 { type: 'mention' }
的节点,该节点是插件中已经定义好的,是一个 void
元素。
import { MentionElement } from '@wangeditor/plugin-mention'
function insertMention() {
const mentionNode: MentionElement = {
type: 'mention', // 必须是 'mention'
value: '张三', // 文本
info: { x: 1, y: 2 }, // 其他信息,自定义
children: [{ text: '' }], // 必须有一个空 text 作为 children
}
editor.restoreSelection() // 恢复选区
editor.deleteBackward('character') // 删除 '@'
editor.insertNode(mentionNode) // 插入 mention 节点
editor.move(1) // 移动光标
}
复制代码
获取和回显 HTML
执行 editor.getHtml()
时,一个 mention 节点获取的 html 格式如下。可以直接渲染到页面上。
<span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="张三" data-info="%7B%22x%22%3A10%7D">@张三</span>
复制代码
PS:其中 data-info
的值,需要 decodeURIComponent
解析。
创建编辑器 设置 HTML 时,可以直接传入获取的 HTML ,编辑器将正常的解析为一个 mention 节点。
Vue 示例
首先需要了解 wangEditor v5 Vue 组件 的使用。
MyEditorWithMention
组件
显示编辑器的组件。该组件的重点:
- 注册
mentionModule
能力,全局注册一次即可 - 使用
showModal
和hideModal
仅仅控制<mention-modal>
的显示和隐藏。<mention-modal>
内部的逻辑它自己处理,解耦 - 定义
insertMention
函数,传递给<mention-modal>
使用 defaultHtml
可支持editor.getHtml()
输出的 mention HTML 格式
<template>
<div>
<p>wangEditor mention demo</p>
<div style="border: 1px solid #ccc;">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editorId="editorId"
:defaultConfig="toolbarConfig"
/>
<Editor
style="height: 400px"
:editorId="editorId"
:defaultConfig="editorConfig"
:defaultHtml="defaultHtml"
@onChange="onChange"
/>
<mention-modal
v-if="isShowModal"
@hideMentionModal="hideMentionModal"
@insertMention="insertMention"
></mention-modal>
</div>
</div>
</template>
<script>
import { Boot } from '@wangeditor/editor'
import { Editor, Toolbar, getEditor, removeEditor } from '@wangeditor/editor-for-vue'
import mentionModule from '@wangeditor/plugin-mention'
import MentionModal from './MentionModal'
// 注册插件
Boot.registerModule(mentionModule)
export default {
name: 'MyEditorWithMention',
components: { Editor, Toolbar, MentionModal },
data() {
return {
editorId: 'wangEditor-1', // 定义一个编辑器 id ,要求:全局唯一且不变!!!
defaultHtml: '<p>你好<span data-w-e-type="mention" data-w-e-is-void data-w-e-is-inline data-value="A张三" data-info="%7B%22id%22%3A%22a%22%7D">@A张三</span></p>',
toolbarConfig: {},
editorConfig: {
placeholder: '请输入内容...',
EXTEND_CONF: {
mentionConfig: {
showModal: this.showMentionModal,
hideModal: this.hideMentionModal,
},
},
},
isShowModal: false
}
},
methods: {
onChange(editor) {
console.log('changed html', editor.getHtml())
console.log('changed content', editor.children)
},
showMentionModal() {
this.isShowModal = true
},
hideMentionModal() {
this.isShowModal = false
},
insertMention(id, name) {
const mentionNode = {
type: 'mention', // 必须是 'mention'
value: name,
info: { id },
children: [{ text: '' }], // 必须有一个空 text 作为 children
}
const editor = getEditor(this.editorId)
if (editor) {
editor.restoreSelection() // 恢复选区
editor.deleteBackward('character') // 删除 '@'
editor.insertNode(mentionNode) // 插入 mention
editor.move(1) // 移动光标
}
}
},
beforeDestroy() {
const editor = getEditor(this.editorId)
if (editor == null) return
editor.destroy() // 组件销毁时,及时销毁 editor ,重要!!!
removeEditor(this.editorId)
},
}
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>
复制代码
MentionModal
组件
编辑器输入 @
的弹出框,内容和样式都随意自定义。改组件的重点:
mounted
时- 获取光标位置,计算组件定位
- focus 到
<input>
- 插入 mention 节点时,调用外部组件的
hideMentionModal
事件 - 可以自定义很多行为,如:筛选 list 、回车时插入、
esc
时隐藏...
<template>
<div id="mention-modal" :style="{ top: top, left: left }">
<input id="mention-input" v-model="searchVal" ref="input" @keyup="inputKeyupHandler">
<ul id="mention-list">
<li
v-for="item in searchedList"
:key="item.id"
@click="insertMentionHandler(item.id, item.name)"
>{{item.name}}</li>
</ul>
</div>
</template>
<script>
export default {
name: 'MentionModal',
data() {
return {
// 定位信息
top: '',
left: '',
// list 信息
searchVal: '',
list: [
{ id: 'a', name: 'A张三' },
{ id: 'b', name: 'B李四' },
{ id: 'c', name: 'C小明' },
{ id: 'd', name: 'D小李' },
{ id: 'e', name: 'E小红' },
]
}
},
computed: {
// 根据 <input> value 筛选 list
searchedList() {
const searchVal = this.searchVal.trim().toLowerCase()
return this.list.filter(item => {
const name = item.name.toLowerCase()
if (name.indexOf(searchVal) >= 0) {
return true
}
return false
})
}
},
methods: {
inputKeyupHandler(event) {
// esc - 隐藏 modal
if (event.key === 'Escape') {
this.$emit('hideMentionModal')
}
// enter - 插入 mention node
if (event.key === 'Enter') {
// 插入第一个
const firstOne = this.searchedList[0]
if (firstOne) {
const { id, name } = firstOne
this.insertMentionHandler(id, name)
}
}
},
insertMentionHandler(id, name) {
this.$emit('insertMention', id, name)
this.$emit('hideMentionModal') // 隐藏 modal
}
},
mounted() {
// 获取光标位置
const domSelection = document.getSelection()
const domRange = domSelection?.getRangeAt(0)
if (domRange == null) return
const rect = domRange.getBoundingClientRect()
// 定位 modal
this.top = `${rect.top + 20}px`
this.left = `${rect.left + 5}px`
// focus input
this.$refs.input.focus()
},
}
</script>
<style>/* 参考源码 */</style>
复制代码
总结
@wangeditor/plugin-mention
给了用户最大的定制开发自由,可让你用于各种框架。有任何问题可去 github 提交 issue 。
Vue 示例源码 github.com/wangfupeng1…