本文采用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 @人功能的资料请关注www.887551.com其它相关文章!