vue实现(@、At、艾特)demo

852 阅读1分钟

image.png

sandBox.vue
<template>
  <div class="content">
    <!--文本框-->
    <div
      ref="divRef"
      class="editor"
      contenteditable
      @keyup="handkeKeyUp"
      @keydown="handleKeyDown"
    />
    <!--选项-->
    <sandText
      v-if="showDialog"
      :visible="showDialog"
      :position="position"
      :query-string="queryString"
      @onPickUser="handlePickUser"
      @onHide="handleHide"
      @onShow="handleShow"
    />
    <el-checkbox v-model="checked">附订单号</el-checkbox>
    <el-button type="primary" @click="logA">主要按钮</el-button>
    <el-button ref="btn" type="text" @click="handleIn">@</el-button>
  </div>
</template>
<script>
import sandText from './sandText'
export default {
  name: 'SandBox',
  components: { sandText },
  data() {
    return {
      userList: [],
      checked: '',
      node: '', // 获取到节点
      user: '', // 选中项的内容
      endIndex: '', // 光标最后停留位置
      queryString: '', // 搜索值
      showDialog: false, // 是否显示弹窗
      position: {
        x: 0,
        y: 0
      } // 弹窗显示位置
    }
  },
  mounted() {
    this.handleIn()
  },
  methods: {
  // 数组去重
    distinct1(arr, key) {
      var newobj = {}
      var newArr = []
      for (var i = 0; i < arr.length; i++) {
        var item = arr[i]
        if (!newobj[item[key]]) {
          newobj[item[key]] = newArr.push(item)
        }
      }
      return newArr
    },
    handleIn() {
      this.$refs.divRef.focus()
      document.execCommand('selectAll', false, null)
      document.getSelection().collapseToEnd()
      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
    },
    logA() {
      const text = this.$refs.divRef.innerHTML
      const text2 = this.escape2Html(text)
      let list = []

      for (let index = 0; index < this.userList.length; index++) {
        if (text2.includes(`"id":"${this.userList[index].id}"`)) {
          list.push(this.userList[index])
        }
      }
      list = this.distinct1(list, 'id')
      console.log(this.$refs.divRef.innerText, list)
      // console.log(this.node, this.user)
    },
    escape2Html(str) {
    // 格式化
      var arrEntities = { lt: '<', gt: '>', nbsp: ' ', amp: '&', quot: '"' }
      return str.replace(/&(lt|gt|nbsp|amp|quot);/gi, function(all, t) {
        return arrEntities[t]
      })
    },
    // 获取光标位置
    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 rect = range.getBoundingClientRect()
      const LINE_HEIGHT = 30
      if (rect === undefined) {
        this.$refs.divRef.innerHTML = '\u200b'
        const rect = range.getClientRects()[0]
        // rect = {
        //   'x': this.$refs.divRef.offsetTop,
        //   'y': this.$refs.divRef.offsetLeft
        // }
        // return {
        //   x: 0,
        //   y: 0 + LINE_HEIGHT
        // }

        console.log(rect)
      }
      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
      }
      // 限制长度
      if (this.$refs.divRef.innerText.length > 100) {
        this.$refs.divRef.innerText = this.$refs.divRef.innerText.substr(
          0,
          100
        )
        // 光标移动到最后
        document.execCommand('selectAll', false, null)
        document.getSelection().collapseToEnd()
      }
    },
    // 键盘按下事件
    handleKeyDown(e) {
      if (this.showDialog) {
        if (
          e.code === 'ArrowUp' ||
          e.code === 'ArrowDown' ||
          e.code === 'Enter'
        ) {
          e.preventDefault()
        }
      }
    },
    // 插入标签后隐藏选择框
    handlePickUser(user) {
      this.userList.push(user)
      this.replaceAtUser(user)
      this.user = user
      this.showDialog = false
    },
    // 隐藏选择框
    handleHide() {
      this.showDialog = false
    },
    // 显示选择框
    handleShow() {
      this.showDialog = true
    }
  }
}
</script>

<style scoped lang="scss">
.content {
  width: 100%;
  font-family: sans-serif;
  h1 {
    text-align: center;
  }
}
.editor {
  margin: 0 auto;
  width: 100%;
  height: 150px;
  background: #fff;
  border: 1px solid #ccc;
  border-radius: 5px;
  text-align: left;
  padding: 10px;
  overflow: auto;
  line-height: 30px;
  &:focus {
    outline: none;
  }
}
</style>

<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"
      ref="usersRef"
      class="item"
      :class="{'active': i === index}"
      @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: 'CSS1' },
  { name: 'Java', id: 'Java1' },
  { name: 'JavaScript', id: 'JavaScript1' }
]
export default {
  name: 'SandText',
  props: {
    visible: {
      type: Boolean,
      default: true
    },
    queryString: {
      type: String,
      default: ''
    },
    position: {
      type: Object,
      default: () => {
        return {}
      }
    }
  },
  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;
    z-index: 100;
  }
  .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>