如何通过Vue实现@人的功能

作者:HAN_Trevor 时间:2024-06-05 15:32:04 

本文采用vue,同时增加鼠标点击事件和一些页面小优化

如何通过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

标签:Vue,@人
0
投稿

猜你喜欢

  • 利用Pandas读取表格行数据判断是否相同的方法

    2022-07-30 22:10:20
  • python中判断文件结束符的具体方法

    2021-09-28 13:31:53
  • 利用J2ME与ASP建立数据库连接

    2010-04-03 20:53:00
  • python3的数据类型及数据类型转换实例详解

    2022-06-30 11:24:45
  • Python项目打包成二进制的方法

    2023-11-07 05:22:02
  • mysql密码过期导致连接不上mysql

    2024-01-22 12:28:06
  • 元组列表字典(莫烦python基础)

    2022-03-23 09:15:58
  • python将Dataframe格式的数据写入opengauss数据库并查询

    2024-01-12 19:35:28
  • 关于Theano和Tensorflow多GPU使用问题

    2023-10-10 13:58:59
  • Python多进程的使用详情

    2022-09-21 23:55:58
  • 用Python制作简单的钢琴程序的教程

    2022-08-20 07:18:49
  • 浅谈Selenium+Webdriver 常用的元素定位方式

    2022-04-17 23:14:43
  • 解决vue2.x中数据渲染以及vuex缓存的问题

    2023-07-02 16:59:24
  • pytorch tensor计算三通道均值方式

    2022-06-26 00:02:41
  • 总结Go语言中defer的使用和注意要点

    2024-02-07 01:48:19
  • jquery validate.js表单验证的基本用法入门

    2023-07-02 05:30:47
  • 使用SQL SERVER存储过程实现历史数据迁移方式

    2024-01-13 04:07:40
  • python sklearn常用分类算法模型的调用

    2021-06-18 11:42:25
  • 微信小程序自定义支持图片的弹窗

    2024-04-16 10:31:25
  • gorm golang 并发连接数据库报错的解决方法

    2024-01-24 07:54:44
  • asp之家 网络编程 m.aspxhome.com