耀极客论坛

 找回密码
 立即注册
查看: 719|回复: 0

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

[复制链接]

193

主题

-17

回帖

276

积分

中级会员

Rank: 3Rank: 3

积分
276
发表于 2022-5-8 01:03:05 | 显示全部楼层 |阅读模式
  这篇文章主要介绍了如何通过vue实现微博中常见的@人的功能,同时增加鼠标点击事件和一些页面小优化。感兴趣的小伙伴可以跟随小编一起学习一下
  本文采用vue,同时增加鼠标点击事件和一些页面小优化


  基本结构
  新建一个sandBox.vue文件编写功能的基本结构
  1. ‹div class="content">
  2.     ‹!--文本框-->
  3.     ‹div
  4.       class="editor"
  5.       ref="divRef"
  6.       contenteditable
  7.       @keyup="handkeKeyUp"
  8.       @keydown="handleKeyDown"
  9.     >‹/div>
  10.     ‹!--选项-->
  11.     ‹AtDialog
  12.       v-if="showDialog"
  13.       :visible="showDialog"
  14.       :position="position"
  15.       :queryString="queryString"
  16.       @onPickUser="handlePickUser"
  17.       @onHide="handleHide"
  18.       @onShow="handleShow"
  19.     >‹/AtDialog>
  20.   ‹/div>
  21. ‹script>
  22. import AtDialog from '../components/AtDialog'
  23. export default {
  24.   name: 'sandBox',
  25.   components: { AtDialog },
  26.   data () {
  27.     return {
  28.       node: '', // 获取到节点
  29.       user: '', // 选中项的内容
  30.       endIndex: '', // 光标最后停留位置
  31.       queryString: '', // 搜索值
  32.       showDialog: false, // 是否显示弹窗
  33.       position: {
  34.         x: 0,
  35.         y: 0
  36.       }// 弹窗显示位置
  37.     }
  38.   },
  39.   methods: {
  40.     // 获取光标位置
  41.     getCursorIndex () {
  42.       const selection = window.getSelection()
  43.       return selection.focusOffset // 选择开始处 focusNode 的偏移量
  44.     },
  45.     // 获取节点
  46.     getRangeNode () {
  47.       const selection = window.getSelection()
  48.       return selection.focusNode // 选择的结束节点
  49.     },
  50.     // 弹窗出现的位置
  51.     getRangeRect () {
  52.       const selection = window.getSelection()
  53.       const range = selection.getRangeAt(0) // 是用于管理选择范围的通用对象
  54.       const rect = range.getClientRects()[0] // 择一些文本并将获得所选文本的范围
  55.       const LINE_HEIGHT = 30
  56.       return {
  57.         x: rect.x,
  58.         y: rect.y + LINE_HEIGHT
  59.       }
  60.     },
  61.     // 是否展示 @
  62.     showAt () {
  63.       const node = this.getRangeNode()
  64.       if (!node || node.nodeType !== Node.TEXT_NODE) return false
  65.       const content = node.textContent || ''
  66.       const regx = /@([^@\s]*)$/
  67.       const match = regx.exec(content.slice(0, this.getCursorIndex()))
  68.       return match && match.length === 2
  69.     },
  70.     // 获取 @ 用户
  71.     getAtUser () {
  72.       const content = this.getRangeNode().textContent || ''
  73.       const regx = /@([^@\s]*)$/
  74.       const match = regx.exec(content.slice(0, this.getCursorIndex()))
  75.       if (match && match.length === 2) {
  76.         return match[1]
  77.       }
  78.       return undefined
  79.     },
  80.     // 创建标签
  81.     createAtButton (user) {
  82.       const btn = document.createElement('span')
  83.       btn.style.display = 'inline-block'
  84.       btn.dataset.user = JSON.stringify(user)
  85.       btn.className = 'at-button'
  86.       btn.contentEditable = 'false'
  87.       btn.textContent = `@${user.name}`
  88.       const wrapper = document.createElement('span')
  89.       wrapper.style.display = 'inline-block'
  90.       wrapper.contentEditable = 'false'
  91.       const spaceElem = document.createElement('span')
  92.       spaceElem.style.whiteSpace = 'pre'
  93.       spaceElem.textContent = '\u200b'
  94.       spaceElem.contentEditable = 'false'
  95.       const clonedSpaceElem = spaceElem.cloneNode(true)
  96.       wrapper.appendChild(spaceElem)
  97.       wrapper.appendChild(btn)
  98.       wrapper.appendChild(clonedSpaceElem)
  99.       return wrapper
  100.     },
  101.     replaceString (raw, replacer) {
  102.       return raw.replace(/@([^@\s]*)$/, replacer)
  103.     },
  104.     // 插入@标签
  105.     replaceAtUser (user) {
  106.       const node = this.node
  107.       if (node && user) {
  108.         const content = node.textContent || ''
  109.         const endIndex = this.endIndex
  110.         const preSlice = this.replaceString(content.slice(0, endIndex), '')
  111.         const restSlice = content.slice(endIndex)
  112.         const parentNode = node.parentNode
  113.         const nextNode = node.nextSibling
  114.         const previousTextNode = new Text(preSlice)
  115.         const nextTextNode = new Text('\u200b' + restSlice) // 添加 0 宽字符
  116.         const atButton = this.createAtButton(user)
  117.         parentNode.removeChild(node)
  118.         // 插在文本框中
  119.         if (nextNode) {
  120.           parentNode.insertBefore(previousTextNode, nextNode)
  121.           parentNode.insertBefore(atButton, nextNode)
  122.           parentNode.insertBefore(nextTextNode, nextNode)
  123.         } else {
  124.           parentNode.appendChild(previousTextNode)
  125.           parentNode.appendChild(atButton)
  126.           parentNode.appendChild(nextTextNode)
  127.         }
  128.         // 重置光标的位置
  129.         const range = new Range()
  130.         const selection = window.getSelection()
  131.         range.setStart(nextTextNode, 0)
  132.         range.setEnd(nextTextNode, 0)
  133.         selection.removeAllRanges()
  134.         selection.addRange(range)
  135.       }
  136.     },
  137.     // 键盘抬起事件
  138.     handkeKeyUp () {
  139.       if (this.showAt()) {
  140.         const node = this.getRangeNode()
  141.         const endIndex = this.getCursorIndex()
  142.         this.node = node
  143.         this.endIndex = endIndex
  144.         this.position = this.getRangeRect()
  145.         this.queryString = this.getAtUser() || ''
  146.         this.showDialog = true
  147.       } else {
  148.         this.showDialog = false
  149.       }
  150.     },
  151.     // 键盘按下事件
  152.     handleKeyDown (e) {
  153.       if (this.showDialog) {
  154.         if (e.code === 'ArrowUp' ||
  155.           e.code === 'ArrowDown' ||
  156.           e.code === 'Enter') {
  157.           e.preventDefault()
  158.         }
  159.       }
  160.     },
  161.     // 插入标签后隐藏选择框
  162.     handlePickUser (user) {
  163.       this.replaceAtUser(user)
  164.       this.user = user
  165.       this.showDialog = false
  166.     },
  167.     // 隐藏选择框
  168.     handleHide () {
  169.       this.showDialog = false
  170.     },
  171.     // 显示选择框
  172.     handleShow () {
  173.       this.showDialog = true
  174.     }
  175.   }
  176. }
  177. ‹/script>
  178. ‹style scoped lang="scss">
  179.   .content {
  180.     font-family: sans-serif;
  181.     h1{
  182.       text-align: center;
  183.     }
  184.   }
  185.   .editor {
  186.     margin: 0 auto;
  187.     width: 600px;
  188.     height: 150px;
  189.     background: #fff;
  190.     border: 1px solid blue;
  191.     border-radius: 5px;
  192.     text-align: left;
  193.     padding: 10px;
  194.     overflow: auto;
  195.     line-height: 30px;
  196.     &:focus {
  197.       outline: none;
  198.     }
  199.   }
  200. ‹/style>
复制代码
  如果添加了点击事件,节点和光标位置获取,需要在【键盘抬起事件】中获取,并保存到data
  1. // 键盘抬起事件
  2.     handkeKeyUp () {
  3.       if (this.showAt()) {
  4.         const node = this.getRangeNode() // 获取节点
  5.         const endIndex = this.getCursorIndex() // 获取光标位置
  6.         this.node = node
  7.         this.endIndex = endIndex
  8.         this.position = this.getRangeRect()
  9.         this.queryString = this.getAtUser() || ''
  10.         this.showDialog = true
  11.       } else {
  12.         this.showDialog = false
  13.       }
  14.     },
复制代码
  新建一个组件,编辑弹窗选项 
  1. ‹template>
  2. ‹div
  3.   class="wrapper"
  4.   :style="{position:'fixed',top:position.y +'px',left:position.x+'px'}">
  5.   ‹div v-if="!mockList.length" class="empty">无搜索结果‹/div>
  6.   ‹div
  7.     v-for="(item,i) in mockList"
  8.     :key="item.id"
  9.     class="item"
  10.     :class="{'active': i === index}"
  11.     ref="usersRef"
  12.     @click="clickAt($event,item)"
  13.     @mouseenter="hoverAt(i)"
  14.   >
  15.     ‹div class="name">{{item.name}}‹/div>
  16.   ‹/div>
  17. ‹/div>
  18. ‹/template>
  19. ‹script>
  20. const mockData = [
  21.   { name: 'HTML', id: 'HTML' },
  22.   { name: 'CSS', id: 'CSS' },
  23.   { name: 'Java', id: 'Java' },
  24.   { name: 'JavaScript', id: 'JavaScript' }
  25. ]
  26. export default {
  27.   name: 'AtDialog',
  28.   props: {
  29.     visible: Boolean,
  30.     position: Object,
  31.     queryString: String
  32.   },
  33.   data () {
  34.     return {
  35.       users: [],
  36.       index: -1,
  37.       mockList: mockData
  38.     }
  39.   },
  40.   watch: {
  41.     queryString (val) {
  42.       val ? this.mockList = mockData.filter(({ name }) => name.startsWith(val)) : this.mockList = mockData.slice(0)
  43.     }
  44.   },
  45.   mounted () {
  46.     document.addEventListener('keyup', this.keyDownHandler)
  47.   },
  48.   destroyed () {
  49.     document.removeEventListener('keyup', this.keyDownHandler)
  50.   },
  51.   methods: {
  52.     keyDownHandler (e) {
  53.       if (e.code === 'Escape') {
  54.         this.$emit('onHide')
  55.         return
  56.       }
  57.       // 键盘按下 => ↓
  58.       if (e.code === 'ArrowDown') {
  59.         if (this.index >= this.mockList.length - 1) {
  60.           this.index = 0
  61.         } else {
  62.           this.index = this.index + 1
  63.         }
  64.       }
  65.       // 键盘按下 => ↑
  66.       if (e.code === 'ArrowUp') {
  67.         if (this.index ‹= 0) {
  68.           this.index = this.mockList.length - 1
  69.         } else {
  70.           this.index = this.index - 1
  71.         }
  72.       }
  73.       // 键盘按下 => 回车
  74.       if (e.code === 'Enter') {
  75.         if (this.mockList.length) {
  76.           const user = {
  77.             name: this.mockList[this.index].name,
  78.             id: this.mockList[this.index].id
  79.           }
  80.           this.$emit('onPickUser', user)
  81.           this.index = -1
  82.         }
  83.       }
  84.     },
  85.     clickAt (e, item) {
  86.       const user = {
  87.         name: item.name,
  88.         id: item.id
  89.       }
  90.       this.$emit('onPickUser', user)
  91.       this.index = -1
  92.     },
  93.     hoverAt (index) {
  94.       this.index = index
  95.     }
  96.   }
  97. }
  98. ‹/script>
  99. ‹style scoped lang="scss">
  100.   .wrapper {
  101.     width: 238px;
  102.     border: 1px solid #e4e7ed;
  103.     border-radius: 4px;
  104.     background-color: #fff;
  105.     box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
  106.     box-sizing: border-box;
  107.     padding: 6px 0;
  108.   }
  109.   .empty{
  110.     font-size: 14px;
  111.     padding: 0 20px;
  112.     color: #999;
  113.   }
  114.   .item {
  115.     font-size: 14px;
  116.     padding: 0 20px;
  117.     line-height: 34px;
  118.     cursor: pointer;
  119.     color: #606266;
  120.     &.active {
  121.       background: #f5f7fa;
  122.       color: blue;
  123.       .id {
  124.         color: blue;
  125.       }
  126.     }
  127.     &:first-child {
  128.       border-radius: 5px 5px 0 0;
  129.     }
  130.     &:last-child {
  131.       border-radius: 0 0 5px 5px;
  132.     }
  133.     .id {
  134.       font-size: 12px;
  135.       color: rgb(83, 81, 81);
  136.     }
  137.   }
  138. ‹/style>
复制代码
  以上就是如何通过Vue实现@人的功能的详细内容,更多关于Vue @人功能的资料请关注脚本之家其它相关文章!


回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|耀极客论坛 ( 粤ICP备2022052845号-2 )|网站地图

GMT+8, 2023-3-24 14:43 , Processed in 0.081860 second(s), 20 queries .

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表