需求描述:在输入框输入@后显示用户列表,实现@人功能
当前环境:vue3+vite+elementPlus+wangEditor@5
需要插件:@wangeditor/plugin-mention
安装插件:npm i @wangeditor/plugin-mention
输入框组件分两部分:1. wangEditor富文本编辑器部分,2. 用户列表对话框部分
1. 富文本编辑器组件代码:AutoComplete.vue
文件
<template>
<div style="border: 1px solid #ccc; position: relative;">
<Editor style="height: 100px" :defaultConfig="editorConfig" v-model="valueHtml" @onCreated="handleCreated"
@onChange="onChange" @keydown.enter.native="keyDown" />
<mention-modal v-if="isShowModal" @hideMentionModal="hideMentionModal" @insertMention="insertMention"
:position="position"></mention-modal>
</div>
</template>
<script setup lang="ts">
import {
ref, shallowRef, onBeforeUnmount, nextTick, watch } from 'vue'
import {
Boot } from '@wangeditor/editor'
import {
Editor } from '@wangeditor/editor-for-vue'
import mentionModule from '@wangeditor/plugin-mention'
import MentionModal from './MentionModal.vue'
Boot.registerModule(mentionModule)
const props = withDefaults(defineProps<{
content?: string
}>(), {
content: ''
})
const editorRef = shallowRef()
const valueHtml = ref('')
const isShowModal = ref(false)
watch(() => props.content, (val: string) => {
nextTick(() => {
valueHtml.value = val
})
})
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const position = ref({
left: '15px',
top: '40px'
})
const handleCreated = (editor: any) => {
editorRef.value = editor
position.value = editor.getSelectionPosition()
}
const showMentionModal = () => {
nextTick(() => {
const editor = editorRef.value
console.log(editor.getSelectionPosition());
position.value = editor.getSelectionPosition()
})
isShowModal.value = true
}
const hideMentionModal = () => {
isShowModal.value = false
}
const editorConfig = {
placeholder: '请输入内容...',
EXTEND_CONF: {
mentionConfig: {
showModal: showMentionModal,
hideModal: hideMentionModal,
},
},
}
const onChange = (editor: any) => {
console.log('changed html', editor.getHtml())
console.log('changed content', editor.children)
}
const insertMention = (id: any, username: any) => {
const mentionNode = {
type: 'mention',
value: username,
info: {
id },
children: [{
text: '' }],
}
const editor = editorRef.value
if (editor) {
editor.restoreSelection()
editor.deleteBackward('character')
editor.insertNode(mentionNode)
editor.move(1)
}
}
const keyDown = (e: any) => {
const editor = editorRef.value
console.log(editor.children[0].children.filter((item: any) => item.type === 'mention').map((item: any) => item.info.id), 'key === 发song')
if (e != undefined) {
e.preventDefault();
}
}
</script>
<style src="@wangeditor/editor/dist/css/style.css"></style>
<style scoped>
.w-e-scroll {
max-height: 100px;
}
</style>
2. 用户列表对话框 MentionModal.vue
文件
<template>
<div id="mention-modal" :style="{ top, left, right, bottom }">
<el-input id="mention-input" v-model="searchVal" ref="input" @keyup="inputKeyupHandler" onkeypress="if(event.keyCode === 13) return false" placeholder="请输入用户名搜索" />
<el-scrollbar height="200px">
<ul id="mention-list">
<li v-for="item in searchedList" :key="item.id" @click="insertMentionHandler(item.id, item.username)">{
{
item.username }}({
{ item.account }})
</li>
</ul>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import {
ref, computed, onMounted, nextTick } from 'vue'
const props = defineProps<{
position: any
}>()
const emit = defineEmits(['hideMentionModal', 'insertMention'])
const top = computed(() => {
return props.position.top
})
const bottom = computed(() => {
return props.position.bottom
})
const left = computed(() => {
return props.position.left
})
const right = computed(() => {
if (props.position.right) {
const right = +(props.position.right.split('px')[0]) - 180
return right < 0 ? 0 : (right + 'px')
}
return ''
})
const searchVal = ref('')
const tempList = Array.from({
length: 20 }).map((_, index) => {
return {
id: index,
username: '张三' + index,
account: 'wp'
}
})
const list = ref(tempList)
const searchedList = computed(() => {
const searchValue = searchVal.value.trim().toLowerCase()
return list.value.filter(item => {
const username = item.username.toLowerCase()
if (username.indexOf(searchValue) >= 0) {
return true
}
return false
})
})
const inputKeyupHandler = (event: any) => {
if (event.key === 'Escape') {
emit('hideMentionModal')
}
if (event.key === 'Enter') {
const firstOne = searchedList.value[0]
if (firstOne) {
const {
id, username } = firstOne
insertMentionHandler(id, username)
}
}
}
const insertMentionHandler = (id: any, username: any) => {
emit('insertMention', id, username)
emit('hideMentionModal')
}
const input = ref()
onMounted(() => {
nextTick(() => {
input.value?.focus()
})
})
</script>
<style>
#mention-modal {
position: absolute;
border: 1px solid #ccc;
background-color: #fff;
padding: 5px;
transition: all .3s;
}
#mention-modal input {
width: 150px;
outline: none;
}
#mention-modal ul {
padding: 0;
margin: 5px 0 0;
}
#mention-modal ul li {
list-style: none;
cursor: pointer;
padding: 5px 2px 5px 10px;
text-align: left;
}
#mention-modal ul li:hover {
background-color: #f1f1f1;
}
</style>
- 注意:对话框的定位是根据编辑器
editor.getSelectionPosition()
来确定的,因为我发现,当页面出现滚动时,根据页面获取光标定位不是很准确。
- 还有,如果你页面组件嵌套多层的话,其中有一个设置了
relative
就会影响到用户对话框的定位,所以根据富文本编辑器的光标来定位最好。