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

以上就是如何通过Vue实现@人的功能的详细内容,更多关于Vue @人功能的资料请关注我们其它相关文章!

(0)

相关推荐

  • vue.js实现h5机器人聊天(测试版)

    本文实例为大家分享了vue.js实现h5机器人聊天的具体代码,供大家参考,具体内容如下 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-sca

  • Vue+tracking.js 实现前端人脸检测功能

    项目中需要实现人脸登陆功能,实现思路为在前端检测人脸,把人脸照片发送到后端识别,返回用户token登陆成功 前端调用摄像头使用tracking.js检测视频流中的人脸,检测到人脸后拍照上传后端. 后端使用face_recognition人脸识别库,使用Flask提供restfulAP供前端调用 实现效果如下图: 登陆界面: 摄像头检测人脸界面: 前端代码如下: <template> <div id="facelogin"> <h1 class="

  • 使用vue3实现一个人喵交流小程序

    目录 前言 初始化项目 设计 代码实现 按需加载 播放音频 录音 长按事件 运行调试 总结 前言 相信很多养猫的人都很想跟自己的猫进行沟通,当猫咪发出各种不同声音的喵喵叫时,通常都会问猫咪怎么了啊,是不是饿了啊,还是怎样怎样的.我也曾经搜索过有没有宠物翻译的软件,现在自己来实现一个吧[手动狗头]. 微信小程序是不支持直接使用 vue 进行开发的,但目前行业中已经有不少解决方案去支持用各种开发框架开发跨端应用了. Taro 3.0版本开始支持使用 Vue3 进行开发,那就用 Taro 来实现我们的

  • vue+AI智能机器人回复功能实现

    本文实例为大家分享了vue+AI智能机器人回复的具体代码,供大家参考,具体内容如下 操作步骤 引入前端代码 前端代码是参考github上的一个开源项目,里面包括AI机器人回复和聊天室两个模块,这里只抽取出来一个AI机器人回复的前端,有兴趣的话,可以点击查看 封装好代理与请求 因为第三方API的请求是外网的,存在跨域问题,所以要配置代理,配置如下: 文件:vue.config.js const vueConfig = { //上面还有项目的其他配置 devServer: { port: 8000,

  • 基于vue和websocket的多人在线聊天室

    最近看到一些关于websocket的东西,就决定写一个在线聊天室尝试一下.最终决定配合vue来写,采用了官方的vue脚手架vue-cli和官方的router,在本例中呢,我是用了CHAT这个对象来存储app的数据的,但后来一想,虽然项目很小,但如果用官方的vuex会更好,方便以后扩展,比如可以加上私信功能,可以在对方不在线的时候缓存发送的消息,这些都是可以的.(现在比较尴尬的就是,我把聊天室写好放到公众号号redream里,但是很少有人会同时在线,所以你会经常发现你进去的时候只有你一个人,就导致

  • 基于Vue2实现的仿手机QQ单页面应用功能(接入聊天机器人 )

    概述 使用Vue2进行的仿手机QQ的webapp的制作,在ui上,参考了设计师kaokao的作品,作品由个人独立开发,源码中进行了详细的注释. 由于自己也是初学Vue2,所以注释写的不够精简,请见谅. 项目地址 https://github.com/jiangqizheng/vue-MiniQQ 项目已实现功能 对话功能--想着既然是QQ总要能进行对话交流,所以在项目中接入了图灵聊天机器人,可以与列表中的每个人物进行对话. 左滑删除--左滑删除相关消息. 搜索页面--点击右上角搜索按钮,能够进入

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

    本文采用vue,同时增加鼠标点击事件和一些页面小优化 基本结构 新建一个sandBox.vue文件编写功能的基本结构 <div class="content"> <!--文本框--> <div class="editor" ref="divRef" contenteditable @keyup="handkeKeyUp" @keydown="handleKeyDown" >

  • Vue实现微信支付功能遇到的坑

    微信支付功能相比较支付宝支付,有点点繁琐,整理记录下来,以便日后所需 项目用VUE+EL搭建而成,支付用EL的radio来做的 <el-radio v-model="radio" label="weixin" > <i class="iconfont icon-weixin"></i> <div class="list"> <h5>微信支付</h5> &l

  • Vue动态面包屑功能的实现方法

    面包屑应该是我们在项目中经常使用的一个功能,一般情况下它用来表示我们当前所处的站点位置,也可以帮助我们能够更快的回到上个层级. 今天我们就来聊聊如何在 Vue 的项目中实现面包屑功能.以下案例都是使用 Element-UI 进行实现. 最笨的方式 首先我们想到的最笨的方法就是在每个需要面包屑的页面中固定写好. <template> <div class="example-container"> <el-breadcrumb separator="

  • vue实现评论列表功能

    具体代码如下所示: <!DOCTYPE html> <html> <head> <title>简易评论列表</title> <meta charset="utf-8"> <link rel="stylesheet" href="node_modules\bootstrap\dist\css\bootstrap.css" rel="external nofoll

  • vue实现简易选项卡功能

    本文实例为大家分享了vue实现简易选项卡功能的具体代码,供大家参考,具体内容如下 1. 效果: 1. 实现发布评论功能 2. 实现评论列表的展示 3. 使用标签页切换的方式来实现 4. 高亮显示当前标签页栏对应的导航 2.HTML <div id="app">         <p>             <button @click="tab(0)" :class="current===0?'active':''"

  • Vue实现typeahead组件功能(非常靠谱)

     前言 之前那个typeahead写的太早,不满足当前的业务需求. 而且有些瑕疵,还有也不方便传入数据和响应数据.. 于是就推倒了重来,写了个V2的版本 看图,多了一些细节的考虑;精简了实现的逻辑代码 效果图 实现的功能 1: 鼠标点击下拉框之外的区域关闭下拉框 2: 支持键盘上下键选择,支持鼠标选择 3: 支持列表过滤搜索 4: 支持外部传入列表JSON格式的映射 5: 支持placeholder的传入 6: 选中对象的响应(.sync vue2.3的组件通讯的语法糖) 7: 箭头icon的映

  • Vue 仿百度搜索功能实现代码

    无上下选择 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>jsonp</title> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, us

  • Vue实现选择城市功能

    查看完整的代码请到   我的github地址  https://github.com/qianyinghuanmie/vue2.0-demos 一.结果展示 二.前期准备: 1.引入汉字转拼音的插件,利用NPM安装 代码指令为 npm install pinyin --save ,详细步骤请到pinyin 2.引入vue-resource,调用json文件,可以参考目录中的city.json,有条件的也可以自己去扒 三. 分析 所实现的功能点: 1.获取json数据展示城市列表 . 2.侧边字母

  • vue.js实现备忘录功能的方法

    这个vue实现备忘录的功能demo是K在github上找到的,K觉得这是一个用来对vue.js入门的一个非常简单的demo,所以拿在这里共享一下. (尊重他人劳动成果,从小事做起~  demo原github地址:https://github.com/vuejs/vue) 一.实现效果 二.代码展示 <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>备忘录&l

  • vue实现商城购物车功能

    本文实例为大家分享了vue实现商城购物车功能的具体代码,供大家参考,具体内容如下 首先,先上最终的效果图 效果并不是很好看,但是这不是重点. 首先,我们先看下布局: <template> <div class="shopcar" id="demo04"> <div class="header-title"> <h3>购物车</h3> </div> <div class=

随机推荐