Vue 评论@人功能实现

在这里插入图片描述

sandBox.vue (组件)

<template>
  <div class="content">
    <!--文本框-->
    <div
      class="edITor"
      ref="divRef"
      contenteditable
      @focus="focus"
      @keyup="handkeKeyUp"
      @keydown="handleKeyDown"
    />
    <!--选项-->
    <AtDiaLOG
      v-if="showDialog"
      :visible="showDialog"
      :position="position"
      :queryString="queryString"
      @onPickUser="handlePickUser"
      @onHide="handleHide"
      @onShow="handleShow"
    />
  </div>
</template>
<script>
import AtDiaLOG from './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]; // 择一些文本并将获得所选文本的范围

      console.log(rect);
      const LINE_HEIGHT = -160;
      return {
        x: 0,
        y: rect.y + LINE_HEIGHT
      };
    },
    // 是否展示 @
    showAt() {
      const node = this.getRangeNode();
      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);
      }
    },
    focus() {
      console.log(this.showAt());
      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;
      }
    },
    // 键盘抬起事件
    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) {
        console.log(e);
        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="less">
.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>

AtDialog (选人组件)

<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,
    // eslint-disable-next-line vue/require-default-prop
    position: Object,
    // eslint-disable-next-line vue/require-default-prop
    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="less">
    .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>

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值