本文采用vue,同时增加鼠标点击事件和一些页面小优化
基本结构
新建一个sandBox.vue文件编写功能的基本结构
<div class="content">
<!--文本框-->
<div
class="editor"
ref="divRef"
contenteditable
@keyup="handkeKeyUp"
@keydown="handleKeyDown"
></div>
<!--选项-->
<AtDialog
v-if="showDialog"
:visible="showDialog"
:position="position"
:queryString="queryString"
@onPickUser="handlePickUser"
@onHide="handleHide"
@onShow="handleShow"
></AtDialog>
</div>
<script>
import AtDialog from '../components/AtDialog'
export default {
name: 'sandBox',
components: { AtDialog },
data () {
return {
node: '', // 获取到节点
user: '', // 选中项的内容
endIndex: '', // 光标最后停留位置
queryString: '', // 搜索值
showDialog: false, // 是否显示弹窗
position: {
x: 0,
y: 0
}// 弹窗显示位置
}
},
methods: {
// 获取光标位置
getCursorIndex () {
const selection = window.getSelection()
return selection.focusOffset // 选择开始处 focusNode 的偏移量
},
// 获取节点
getRangeNode () {
const selection = window.getSelection()
return selection.focusNode // 选择的结束节点
},
// 弹窗出现的位置
getRangeRect () {
const selection = window.getSelection()
const range = selection.getRangeAt(0) // 是用于管理选择范围的通用对象
const rect = range.getClientRects()[0] // 择一些文本并将获得所选文本的范围
const LINE_HEIGHT = 30
return {
x: rect.x,
y: rect.y + LINE_HEIGHT
}
},
// 是否展示 @
showAt () {
const node = this.getRangeNode()
if (!node || node.nodeType !== Node.TEXT_NODE) return false
const content = node.textContent || ''
const regx = /@([^@\s]*)$/
const match = regx.exec(content.slice(0, this.getCursorIndex()))
return match && match.length === 2
},
// 获取 @ 用户
getAtUser () {
const content = this.getRangeNode().textContent || ''
const regx = /@([^@\s]*)$/
const match = regx.exec(content.slice(0, this.getCursorIndex()))
if (match && match.length === 2) {
return match[1]
}
return undefined
},
// 创建标签
createAtButton (user) {
const btn = document.createElement('span')
btn.style.display = 'inline-block'
btn.dataset.user = JSON.stringify(user)
btn.className = 'at-button'
btn.contentEditable = 'false'
btn.textContent = `@${user.name}`
const wrapper = document.createElement('span')
wrapper.style.display = 'inline-block'
wrapper.contentEditable = 'false'
const spaceElem = document.createElement('span')
spaceElem.style.whiteSpace = 'pre'
spaceElem.textContent = '\u200b'
spaceElem.contentEditable = 'false'
const clonedSpaceElem = spaceElem.cloneNode(true)
wrapper.appendChild(spaceElem)
wrapper.appendChild(btn)
wrapper.appendChild(clonedSpaceElem)
return wrapper
},
replaceString (raw, replacer) {
return raw.replace(/@([^@\s]*)$/, replacer)
},
// 插入@标签
replaceAtUser (user) {
const node = this.node
if (node && user) {
const content = node.textContent || ''
const endIndex = this.endIndex
const preSlice = this.replaceString(content.slice(0, endIndex), '')
const restSlice = content.slice(endIndex)
const parentNode = node.parentNode
const nextNode = node.nextSibling
const previousTextNode = new Text(preSlice)
const nextTextNode = new Text('\u200b' + restSlice) // 添加 0 宽字符
const atButton = this.createAtButton(user)
parentNode.removeChild(node)
// 插在文本框中
if (nextNode) {
parentNode.insertBefore(previousTextNode, nextNode)
parentNode.insertBefore(atButton, nextNode)
parentNode.insertBefore(nextTextNode, nextNode)
} else {
parentNode.appendChild(previousTextNode)
parentNode.appendChild(atButton)
parentNode.appendChild(nextTextNode)
}
// 重置光标的位置
const range = new Range()
const selection = window.getSelection()
range.setStart(nextTextNode, 0)
range.setEnd(nextTextNode, 0)
selection.removeAllRanges()
selection.addRange(range)
}
},
// 键盘抬起事件
handkeKeyUp () {
if (this.showAt()) {
const node = this.getRangeNode()
const endIndex = this.getCursorIndex()
this.node = node
this.endIndex = endIndex
this.position = this.getRangeRect()
this.queryString = this.getAtUser() || ''
this.showDialog = true
} else {
this.showDialog = false
}
},
// 键盘按下事件
handleKeyDown (e) {
if (this.showDialog) {
if (e.code === 'ArrowUp' ||
e.code === 'ArrowDown' ||
e.code === 'Enter') {
e.preventDefault()
}
}
},
// 插入标签后隐藏选择框
handlePickUser (user) {
this.replaceAtUser(user)
this.user = user
this.showDialog = false
},
// 隐藏选择框
handleHide () {
this.showDialog = false
},
// 显示选择框
handleShow () {
this.showDialog = true
}
}
}
</script>
<style scoped lang="scss">
.content {
font-family: sans-serif;
h1{
text-align: center;
}
}
.editor {
margin: 0 auto;
width: 600px;
height: 150px;
background: #fff;
border: 1px solid blue;
border-radius: 5px;
text-align: left;
padding: 10px;
overflow: auto;
line-height: 30px;
&:focus {
outline: none;
}
}
</style>
如果添加了点击事件,节点和光标位置获取,需要在【键盘抬起事件】中获取,并保存到data
// 键盘抬起事件
handkeKeyUp () {
if (this.showAt()) {
const node = this.getRangeNode() // 获取节点
const endIndex = this.getCursorIndex() // 获取光标位置
this.node = node
this.endIndex = endIndex
this.position = this.getRangeRect()
this.queryString = this.getAtUser() || ''
this.showDialog = true
} else {
this.showDialog = false
}
},
新建一个组件,编辑弹窗选项
<template>
<div
class="wrapper"
:style="{position:'fixed',top:position.y +'px',left:position.x+'px'}">
<div v-if="!mockList.length" class="empty">无搜索结果</div>
<div
v-for="(item,i) in mockList"
:key="item.id"
class="item"
:class="{'active': i === index}"
ref="usersRef"
@click="clickAt($event,item)"
@mouseenter="hoverAt(i)"
>
<div class="name">{{item.name}}</div>
</div>
</div>
</template>
<script>
const mockData = [
{ name: 'HTML', id: 'HTML' },
{ name: 'CSS', id: 'CSS' },
{ name: 'Java', id: 'Java' },
{ name: 'JavaScript', id: 'JavaScript' }
]
export default {
name: 'AtDialog',
props: {
visible: Boolean,
position: Object,
queryString: String
},
data () {
return {
users: [],
index: -1,
mockList: mockData
}
},
watch: {
queryString (val) {
val ? this.mockList = mockData.filter(({ name }) => name.startsWith(val)) : this.mockList = mockData.slice(0)
}
},
mounted () {
document.addEventListener('keyup', this.keyDownHandler)
},
destroyed () {
document.removeEventListener('keyup', this.keyDownHandler)
},
methods: {
keyDownHandler (e) {
if (e.code === 'Escape') {
this.$emit('onHide')
return
}
// 键盘按下 => ↓
if (e.code === 'ArrowDown') {
if (this.index >= this.mockList.length - 1) {
this.index = 0
} else {
this.index = this.index + 1
}
}
// 键盘按下 => ↑
if (e.code === 'ArrowUp') {
if (this.index <= 0) {
this.index = this.mockList.length - 1
} else {
this.index = this.index - 1
}
}
// 键盘按下 => 回车
if (e.code === 'Enter') {
if (this.mockList.length) {
const user = {
name: this.mockList[this.index].name,
id: this.mockList[this.index].id
}
this.$emit('onPickUser', user)
this.index = -1
}
}
},
clickAt (e, item) {
const user = {
name: item.name,
id: item.id
}
this.$emit('onPickUser', user)
this.index = -1
},
hoverAt (index) {
this.index = index
}
}
}
</script>
<style scoped lang="scss">
.wrapper {
width: 238px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
box-sizing: border-box;
padding: 6px 0;
}
.empty{
font-size: 14px;
padding: 0 20px;
color: #999;
}
.item {
font-size: 14px;
padding: 0 20px;
line-height: 34px;
cursor: pointer;
color: #606266;
&.active {
background: #f5f7fa;
color: blue;
.id {
color: blue;
}
}
&:first-child {
border-radius: 5px 5px 0 0;
}
&:last-child {
border-radius: 0 0 5px 5px;
}
.id {
font-size: 12px;
color: rgb(83, 81, 81);
}
}
</style>
以上就是如何通过Vue实现@人的功能的详细内容,更多关于Vue @人功能的资料请关注编程网其它相关文章!