vue实现@人员功能(atSomeone)
实现@人员功能
·
@人员功能,at人员功能
因为项目需要,产品要实现@人员功能,网上找了一大堆没有好用的,只能自己写了
准备工作
- npm install wangeditor (最好是^4.7.15这个版本,怕下载错的话直接在package.json中写上"wangeditor": "^4.7.15"然后在运行 npm i)
- npm install caret-pos
- browser.js(自己写的,如果不需要判断浏览器则不需要,代码我贴在下面了)
重要代码分析
这里的enterEv方法是重点,主要用来监听@字符,然后弹出渲染窗口,该方法下调用了一个 getPosition() 方法,只要用来获取光标位置,设置渲染弹窗的渲染位置,详细看代码中的getPosition()
//根据输入@的位置定位下拉框的位置
this.getPosition()
关键的创建容器是使用wangeditor来初始化的,文档看这里
接下来就是代码了
首先创建一个单独的vue文件,抽离组件,文件位置看个人习惯
<template>
<div class="at-someone-content">
<div ref="editor" class="editor" :id="id" @keydown="enterEv($event)"></div>
<div
v-show="showFlag"
class="at-someone"
:style="{
left: left + 'px',
top: top + 'px',
visibility: showFlag
}"
>
<div class="at-someone-box" ref="atSomeoneBox">
<a-input
ref="searchInput"
class="search-input"
placeholder="请输入姓名"
v-model.trim="searchKeyword"
@input="handlerSearchInput"
@blur="searchBlue"
/>
<a-spin :loading="userLoading">
<ul v-if="userList.length" class="mention-option-ul">
<li
v-for="(item, index) in userList"
:key="index"
class="mention-option-li"
@click="selectPerson(item)"
>
<span class="mention-option-img-cont">
<img v-if="item.avatar" class="mention-option-img" :src="item.avatar">
<span v-else class="mention-option-img-name">{{ item.name[0] }}</span>
</span>
<span class="mention-option-name">{{ item.name }}</span>
<span class="mention-option-deptname">{{ item.deptNames}}</span>
</li>
</ul>
<div v-else class="top-team-search-empty">
<img class="blank-img" src="@/assets/img/zanwushuju.svg">
<small data-v-e44d7dbc="">未找到相关内容</small>
</div>
</a-spin>
</div>
</div>
</div>
</template>
<script>
import E from "wangeditor"
import { Tools } from '@/utils/browser.js'
import { position, offset } from 'caret-pos'
import { getAllUsers } from '@/services/investment/index.js'
export default {
name: "AtSomeone",
props: {
from: {
type: String,
default: ''
},
placeholder: {
type: String,
default: '添加项目跟进,记录重要进展(最多1000字)'
},
id: {
type: String,
default: 'editor'
},
record: {
type: String,
default: ''
},
height: {
type: Number,
default: 71
},
},
data() {
return {
showFlag: 'hidden',
userLoading: false,
left: '',
top: '',
browserType: Tools.browserType(),
editor: null,
position: {
range: '',
selection: ''
},
userList: [],
searchKeyword: '',
}
},
mounted() {
this.createEditor()
this.handlerSearchInput()
},
methods: {
reset() {
this.editor.txt.clear()
},
searchBlue() {
let time = setTimeout(() => {
this.showFlag = 'hidden'
this.searchKeyword = ''
clearTimeout(time)
time = null
}, 300);
},
handlerSearchInput() {
this.userLoading = true
getAllUsers({
data: {
name: this.searchKeyword
}
}).then(res => {
this.$nextTick(() => {
this.userList = res
})
}).finally(() => {
this.userLoading = false
})
},
createEditor() {
let editor = new E(`#${this.id}`)
editor.config.onchange = (newHtml) => {
// 获取纯文本
const text = editor.txt.text().replace(/ /gi, "")
// 超出1000字处理
if (text.length >= 1000) {
// 因为是富文本编辑器,无法做截取只能提示
this.$message.warning('最多可输入1000字!')
}
this.$emit('updataRecordHtml', newHtml, text)
}
// 配置触发 onchange 的时间频率,默认为 200ms
editor.config.onchangeTimeout = 500; // 修改为 500ms
editor.config.height = this.height
editor.config.placeholder = this.placeholder
//菜单置空
editor.config.menus = []
//创建
editor.create()
editor.txt.html(this.record)
this.editor = editor
},
enterEv (e) {
let ele = this.editor.$textElem.elems[0]
//getSelection是另一种获取用户选择的文本范围或光标的当前位置的方法
let selection = getSelection()
//判断输入的是@符号
if (((e.keyCode === 229 && e.key === '@') || (e.keyCode === 229 && e.code === 'Digit2') || e.keyCode === 50) && e.shiftKey) {
// 兼容
e.preventDefault ? e.preventDefault() : e.returnValue = false
this.position = {
range: selection.getRangeAt(0),
selection: selection
}
try {
if (this.editor.$textElem.elems[0].firstChild.tagName === "BR") {
this.editor.$textElem.elems[0].removeChild(this.editor.$textElem.elems[0].firstChild)
}
if (this.editor.$textElem.elems[0].firstChild.firstChild.tagName === "BR") {
// 解决不输入内容直接@时候出现的<br>换行标签
this.editor.$textElem.elems[0].firstChild.removeChild(this.editor.$textElem.elems[0].firstChild.firstChild)
}
} catch (e) {
console.log(e)
}
//根据输入@的位置定位下拉框的位置
this.getPosition()
this.showFlag = 'visible'
//下拉框搜索框自动获取焦点
this.$nextTick(()=>{
this.searchKeyword = ''
this.$refs.searchInput.focus()
})
// this.handlerSearchInput()
} else if (e.code === 'Backspace' || e.key === 'Backspace') {
// e.preventDefault()
// let selection = getSelection()
let range = selection.getRangeAt(0)
let removeNode = null
if (this.browserType === 'Firefox') {
console.log(range)
if (range.startContainer.className !== "at-text") {
removeNode = range.startContainer.previousElementSibling
console.dir('Firefox previousElementSibling',removeNode)
}
if (range.startContainer.parentElement.className !== "at-text" && range.startContainer.lastChild !== null) {
removeNode = range.startContainer.lastElementChild.previousElementSibling
console.dir('Firefox lastElementChild.previousElementSibling', removeNode)
}
}
if (this.browserType === 'Chrome') {
if (range.startContainer.textContent.length === 1 && range.startContainer.textContent.trim() === '') {
removeNode = range.startContainer.previousElementSibling
// console.dir(removeNode)
}
if (range.startContainer.parentNode.className === 'at-text') {
e.preventDefault ? e.preventDefault() : e.returnValue = false
removeNode = range.startContainer.parentNode
}
}
if (this.browserType === "IE") {
if (range.startContainer.nodeName !== "P" && range.startContainer.nodeValue.trim() === "" && range.startContainer.previousSibling.className === "at-text") {
removeNode = range.startContainer.previousSibling
console.log('parentElement',removeNode)
}
if (range.startContainer.parentNode.className === "at-text" && range.startContainer.nodeName === '#text' && range.startContainer.previousSibling == null) {
removeNode = range.startContainer.parentNode
console.log('parentNode', removeNode)
}
}
if (removeNode) {
// ele.removeChild(removeNode)
ele.firstChild.removeChild(removeNode)
}
this.showFlag = 'hidden'
}
},
selectPerson (item) {
const {name,id} = item
this.showFlag = 'hidden'
//获取选区对象
let selection = this.position.selection
let range = this.position.range
// 生成需要显示的内容,包括一个 span 和一个空格。
let spanNode1 = document.createElement('span')
let spanNode2 = document.createElement('span')
spanNode1.className = 'at-text'
spanNode1.style.color = '#3E74CA'
spanNode1.innerHTML = '@' + name
spanNode1.dataset.id = id
// 设置@人的节点不可编辑
spanNode1.contentEditable = false
// spanNode2.innerHTML = ' '
spanNode2.innerHTML = ' '
// 将生成内容打包放在 Fragment 中,并获取生成内容的最后一个节点,也就是空格。
//创建一个新的空白的文档片段
let frag = document.createDocumentFragment(),
node, lastNode;
frag.appendChild(spanNode1)
while ((node = spanNode2.firstChild)) {
lastNode = frag.appendChild(node)
}
// 将 Fragment 中的内容放入 range 中,并将光标放在空格之后。
range.insertNode(frag)
selection.collapse(lastNode, 1)
//将当前的选区折叠到最末尾的一个点
selection.collapseToEnd();
},
//获取@位置
getPosition () {
this.showFlag = 'visible'
const ele = this.editor.$textElem.elems[0]
const childEle = document.getElementsByClassName("at-someone")[0]
// console.log(childEle)
let parentW = ele.offsetWidth
let parentH = ele.offsetHeight
let childW = childEle.offsetWidth
let childH = childEle.offsetHeight
const pos = position(ele)
const off = offset(ele)
// 弹框偏移超出父元素的宽高
if (parentW - pos.left < childW) {
this.left = off.left - childW
} else {
this.left = off.left
}
if (childH + off.top + 20 > document.documentElement.clientHeight) {
this.top = (childH + parentH+ off.top + off.height + 40) - document.documentElement.clientHeight
} else {
this.top = off.top+20
}
// 如果是消息归档页面则写死left
if (['message'].includes(this.from)) {
this.left = 40
}
},
},
beforeDestroy() {
// 销毁编辑器
this.editor.destroy()
this.editor = null
}
}
</script>
<style lang="scss" scoped>
.top-team-search-empty {
display: flex;
height: 200px;
flex-direction: column;
align-items: center;
justify-content: center;
small {
font-size: 12px !important;
color: #c1c4cb;
margin-top: 6px !important;
display: block;
}
}
.at-someone-content {
width: 100%;
// position: relative;
:deep(.w-e-toolbar) {
display: none;
}
:deep(.w-e-text-container) {
border: 0 !important;
z-index: 1 !important;
.placeholder {
left: 0;
top: 0;
}
.w-e-text {
padding: 0 !important;
// min-height: 40px !important;
p {
margin: 0;
}
}
}
.search-input {
border: 0 !important;
}
.at-someone {
max-width: 260px;
position: fixed;
z-index: 5;
border-radius: 3px;
background: #fff;
padding: 8px 0;
border: 1px solid #E4E5EC;
box-shadow: 0 4px 10px rgb(0 0 0 / 10%);
// &::after {
// content: "";
// position: absolute;
// left: 14px;
// top: 0;
// display: block;
// box-sizing: border-box;
// width: 8px;
// height: 8px;
// transform: translate(-50%, -50%) rotate(45deg);
// z-index: 1;
// border: 1px solid #E4E5EB;
// border-right: 0;
// border-bottom: 0;
// background: #fff;
// border-top-left-radius: 2px;
// }
:deep(.arco-input-wrapper) {
border-left: 0;
border-top: 0;
border-right: 0;
border-color: #E4E5EC;
padding: 0 16px;
box-sizing: border-box;
border-radius: 0;
&:hover {
background: #fff;
}
}
}
}
</style>
组件引用,在需要用到的页面引入组件
import atSomeone from '@/components/atSomeone/index.vue'
components: {
atSomeone
},
<atSomeone
ref="atSomeone"
:from="from"
:record="record"
@updataRecordHtml="updataRecordHtml"
/>
更多推荐
已为社区贡献3条内容
所有评论(0)