如何通过Vue实现@人的功能
作者:HAN_Trevor 发布时间:2024-06-05 15:32:04
标签:Vue,@人
本文采用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>
来源:https://blog.csdn.net/weixin_45785873/article/details/122099225


猜你喜欢
- 问题描述:将python脚本设置成开机自启。环境:windows7 64位前段时间,一直想把文件打包成exe文件,然后设置成开机自启,虽然感
- 获取表字段:select * from user_tab_columns where Table_Name='用户表' or
- 前言在 Qt 中可以使用信号和槽机制很方便地实现部件之间的通信,考虑下面这样的场景:我想要点击任意一个专辑卡并通知主界面跳转到专辑界面,那么
- 本文实例讲述了python实现通过pil模块对图片格式进行转换的方法。分享给大家供大家参考。具体分析如下:python的pil模块相当的智能
- 这一版,对虹软的功能进行了一些封装,添加了人脸特征比对,比对结果保存到文件,和从文件提取特征进行比对,大体功能基本都已经实现,可以进行下一步
- lambda表达式python中形如:lambda parameters: expression称为lambda表达式,用于创建匿名函数,该
- 本文实例为大家分享了vue动态控制el-table表格列的展示与隐藏的具体代码,供大家参考,具体内容如下1.引入el-table组件,这里我
- 即使在urlencode之前str.decode(“cp936″).encode(“utf-8″)做了编码转换也是没用的。后来查询手册查到一
- 长话短说,看这个 form 元素:<form method="post" action=&qu
- 最近使用Python调用百度的REST API实现语音识别,但是百度要求音频文件的压缩方式只能是pcm(不压缩)、wav、opus、spee
- 1. 监测端口我们要引用的socket模块来校验端口是否被占用。1.1 socket是什么?简单一句话:网络上的两个程序通过一个双向的通信连
- 0.前言最近学习的python第一个项目实战,《外星人入侵》,成功实现所有功能,给大家提供源代码环境安装:python 3.7+ pygam
- 前言字符串是 字符的序列 。字符串基本上就是一组单词。我几乎可以保证你在每个Python程序中都要用到字符串,所以请特别留心下面这部分的内容
- 记录使用pytorch构建网络模型过程遇到的点1. 网络模型构建中的问题1.1 输入变量是Tensor张量各个模块和网络模型的输入,一定要是
- 绘制一个菱形四边形,边长为 200 像素。方法1和2绘制了内角为60和120度的菱形,方法3绘制了内角为90度的菱形。方法1
- 由于tornado内置的AsyncHTTPClient功能过于单一, 所以自己写了一个基于Tornado的HTTP客户端库, 鉴于自己多处使
- <?php function CreateShtml() { ob_start(&quo
- python启用多线程后,调用exit出现无法退出的情况,原因是exit会抛出Systemexit的异常,如果在exit外围调用了try,就
- 今天,在在使用 pycharm 的使用,进行创建 python的时候,发现使用默认的创建的选项使用的python 3环境 。而我系统默认的p
- 本文实例为大家分享了JavaScript实现简单计算器的具体代码,供大家参考,具体内容如下代码:<!DOCTYPE html>&