Vue + wangEditor v5 实现 @ mention 功能

【注意】本文发布之后,插件有可能会继续更新。所以,最新的使用方法请参考插件文档

前言

wangEditor V5 发布在即,为了验证它的扩展性,我近期做了几个第三方插件。可查看插件列表

本文分享 @ mention 插件的设计和实现,使用 Vue 做一个示例,源码在本文末尾。能用于 Vue ,也就能用于 Vue3 React 等其他框架。

image.png

使用 @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)
复制代码

注册之后,编辑器讲监听 @ 输入,并触发编辑器配置中的 showModalhideModal

定义 showModalhideModal

输入 @ 之后,需要什么样子的弹框?每个项目都有不同的需求。有些比较简单,有些需要搜索,有些可能需要 ajax 异步加载...

所以,这个弹框需要你自己定义。你可以用普通 <div> 也可以用 Vue React 组件,如下文的 MentionModal 组件。

定义好弹框之后,你只需要传入 showModalhideModal 到编辑器配置中,编辑器会适时调用它们。

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) // 移动光标
}
复制代码

image.png

获取和回显 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 组件

image.png

显示编辑器的组件。该组件的重点:

  • 注册 mentionModule 能力,全局注册一次即可
  • 使用 showModalhideModal 仅仅控制 <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 组件

image.png

编辑器输入 @ 的弹出框,内容和样式都随意自定义。改组件的重点:

  • 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…

Guess you like

Origin juejin.im/post/7069548140294045709