【vue2】留言@功能

键盘打出@,出现人员列表进行选择

 

主要是利用div或input的contenteditable属性(即可开启该元素的编辑模式)

然后利用键盘抬起和按下事件,进行监听判断是否是@,若是则显示人员列表进行选择,实现@功能的实现

主页面代码

 <!-- 留言区 -->
              <div
                style="
                  height: 100px;
                  background-color: #fff;
                  justify-content: space-between;
                  border: 1px solid #e6eaf0;
                "
              >
                <div style="display: flex">
                  <div
                    style="
                      width: 20px;
                      height: 20px;
                      border-radius: 10px;
                      overflow: hidden;
                      margin: auto 10px;
                    "
                  >
                    <img :src="info.img" style="width: 100%; height: 100%" />
                  </div>

                  <At ref="At" class="editor"></At>
                </div>
                <div
                  style="
                    width: 100%;
                    padding: 0 10px;
                    display: flex;
                    justify-content: space-between;
                  "
                >
                  <div style="margin: auto 0">
                    <img
                      src="@/assets/wenj.png"
                      style="width: 20px; height: 20px"
                    />
                    <!-- <img
                      src="@/assets/aite.png"
                      style="width: 20px; height: 20px; margin-left: 10px"
                    /> -->
                  </div>
                  <div>
                    <el-button type="text" @click="AddComments">发送</el-button>
                  </div>
                </div>
              </div>
// 发送留言
    async AddComments() {
      this.textarea = this.$refs.At.text;

      const res = await AddComments({
        userid: this.info.id,
        username: this.info.name,
        dataid: this.Form.dataid,
        text: this.textarea,
      });
      if (res.code === 200) {
        this.$parent.GetFromData(this.Form.dataid); //调用父组件的方法,重新渲染
        this.textarea = "";
      }
    },

组件At的代码

<template>
  <div id="At">
    <!-- 内容,主要是需要显示输入多少字数了 -->
    <el-input
      id="divRef"
      ref="divRef"
      type="textarea"
      :rows="2"
      maxlength="140"
      show-word-limit
      contenteditable
      resize="none"
      placeholder="留言"
      v-model="text"
      @keyup.native="handkeKeyUp"
      @keydown.native="handleKeyDown"
    >
    </el-input>
    <!-- 若是没有要求需要显示多少字数,就用这个 -->
    <!-- <div
      ref="divRef"
      contenteditable
      spellcheck="false"
      placeholder="留言"
      @keyup="handkeKeyUp"
      @keydown="handleKeyDown"
      @blur="blur"
    ></div> -->
    <!-- 人员选择 -->
    <AtDialog
      ref="AtDialog"
      v-if="showDialog"
      :visible="showDialog"
      :position="position"
      :queryString="queryString"
      :mockList="mockList"
      @onPickUser="handlePickUser"
      @onHide="handleHide"
      @onShow="handleShow"
    ></AtDialog>
  </div>
</template>
<script>
import { mapGetters } from "vuex";
import AtDialog from "./AtDialog.vue";
import { GetAllList } from "@/api/permission";

export default {
  name: "At",
  computed: {
    ...mapGetters(["userid", "name", "info", "btnsUrl"]),
  },
  components: {
    AtDialog,
  },

  data() {
    return {
      text: "", //编辑的留言
      node: "", // 获取到节点
      user: "", // 选中项的内容
      endIndex: "", // 光标最后停留位置
      queryString: "", // 搜索值
      showDialog: false, // 是否显示弹窗
      mockList: [],
      position: {
        x: 0,
        y: 0,
      }, // 弹窗显示位置
      cursorPosition: "",
    };
  },
  watch: {
    // 监听点击空白处,隐藏用户列表
    showDialog: {
      handler(newVal, olVal) {
        if (newVal) {
          setTimeout(() => {
            document.addEventListener("click", this.checkClick);
          }, 0);
        } else {
          document.removeEventListener("click", this.checkClick);
        }
      },
    },
  },
  //   mounted() {
  //     // 监听点击到@后面,出现用户列表
  //     window.addEventListener("mouseup", this.handleMousedown);
  //   },
  unmounted() {
    document.removeEventListener("click", checkClick);
    // document.removeEventListener("mouseup", this.handleMousedown);
  },
  created() {
    this.GetAllList();
  },
  methods: {
    GetAllList() {
      GetAllList().then((res) => {
        this.mockList = res.data.userlist;
      });
    },
    // 点击空白处,隐藏用户列表
    checkClick(event) {
      let dom = this.$refs.AtDialog; // 这里是你的下拉菜单元素
      if (dom) {
        this.showDialog = false;
        document.removeEventListener("click", this.checkClick);
      }
      //   if (!dom.contains(event.target)) {
      //     // 不在菜单范围,隐藏即可
      //     if (this.showDialog) {
      //       this.showDialog = false;
      //       document.removeEventListener("click", this.checkClick);
      //     }
      //   }
    },
    // // 监听点击到@后面,出现用户列表
    // handleMousedown(e) {
    //   if (e.target.id === "divRef") {
    //     const field = this.getTextSelection();
    //     var field_val = field.value; //文本
    //     const reg = /@([^@\s]*)$/;
    //     const mat = reg.exec(
    //       field_val.slice(field.selectionStart - 1, field.selectionEnd)
    //     );
    //     if (mat && mat.length === 2) {
    //       this.node = field.value; //文本内容
    //       this.endIndex = field.selectionEnd; //文本长度
    //       this.position = this.getRangeRect(e);
    //       this.queryString = this.getAtUser() || "";
    //       this.showDialog = true;
    //     }
    //   }
    // },
    getText() {
      this.$emit("getText", this.text);
    },
    // 失去焦点,获取内容(div的方法)
    blur() {
      this.text = this.setText(this.$refs.divRef.innerHTML);
    },
    // 去除标签,获取纯文本
    setText(html) {
      return html
        .replace(/<(p|div)[^>]*>(<br\/?>|&nbsp;)<\/\1>/gi, "\n")
        .replace(/<br\/?>/gi, "\n")
        .replace(/<[^>/]+>/g, "")
        .replace(/(\n)?<\/([^>]+)>/g, "")
        .replace(/\u00a0/g, " ")
        .replace(/&nbsp;/g, " ")
        .replace(/<img[^>]+src\\s*=\\s*['\"]([^'\"]+)['\"][^>]*>/g, "")
        .replace(/<\/?(img|table)[^>]*>/g, "") // 去除图片和表格
        .replace(/<\/?(a)[^>]*>/g, ""); //  去除a标签
    },
    // 获取光标位置
    getCursorIndex() {
      const selection = window.getSelection();
      return selection.focusOffset; // 选择开始处 focusNode 的偏移量
    },
    // 获取节点(div的方法)
    getRangeNode() {
      const selection = window.getSelection();
      return selection.focusNode; // 选择的结束节点
    },
    // 获取节点(输入框的方法)
    getTextSelection() {
      return document.getElementById("divRef");
    },
    // 弹窗出现的位置(输入框的方法)
    getRangeRect(e) {
      let p = e.target.getBoundingClientRect();
      const LINE_HEIGHT = 150;
      return {
        x: p.x,
        y: p.y - LINE_HEIGHT,
      };
    },
    // 弹窗出现的位置(div的方法)
    // getRangeRect() {
    //   const selection = window.getSelection();
    //   const range = selection.getRangeAt(0); // 是用于管理选择范围的通用对象
    //   const rect = range.getClientRects()[0]; // 择一些文本并将获得所选文本的范围
    //   const LINE_HEIGHT = 160;
    //   return {
    //     x: rect.x,
    //     y: rect.y - LINE_HEIGHT,
    //   };
    // },
    // 是否展示 @
    showAt() {
      // 输入框的方法
      const field = this.getTextSelection();
      //   var startPos = field.selectionStart; //光标开始的位置
      //   var endPos = field.selectionEnd; //结束
      var field_value = field.value; //文本
      const regx = /@([^@\s]*)$/;
      // 包含@
      //   const match = regx.exec(field_value.slice(0, field.selectionEnd));
      // 获取全部文字,然后判断光标结束位置的前面一个字符是不是@

      let match = "";
      //   判断是在整个文本的最后加还是中途加@
      if (field_value.length === field.selectionEnd) {
        match = regx.exec(
          field_value.slice(field_value.length - 1, field.selectionEnd)
        );
        return match && match.length === 2;
      } else {
        match = regx.exec(
          field_value.slice(field.selectionStart - 1, field.selectionEnd)
        );
        return match && match.length === 2;
      }
      //   div的方法
      //   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 || "";
        let seleText = "";
        // 判断是在整个文本的最后加还是中途加选择后的用户
        if (node.length === this.endIndex) {
          seleText = content + user.name;
          this.text = seleText;
          this.$nextTick(() => {
            this.$refs.divRef.focus();
          });
        } else {
          seleText =
            content.substring(0, this.endIndex) +
            user.name +
            content.substring(this.endIndex);
          this.text = seleText;
          this.$nextTick(() => {
            this.$refs.divRef.focus();
          });
        }
 
        //     // div的方法
        //     // 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(e) {
      if (this.showAt()) {
        // 输入框的方法
        const field = this.getTextSelection();
        this.node = field.value; //文本内容
        this.endIndex = field.selectionEnd; //文本长度
        this.position = this.getRangeRect(e);
        this.queryString = this.getAtUser() || "";
        this.showDialog = true;
        // // div的方法
        // 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 lang="scss" scoped></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"
      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,
    mockList: Array,
  },
  data() {
    return {
      users: [],
      index: -1,
      // mockList: mockData,
    };
  },
  watch: {
    queryString(val) {
      // console.log(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) {
      const field = this.$parent.getTextSelection();
      if (e.code === "Escape") {
        this.$emit("onHide");
        return;
      }
      if (e.code === "Backspace") {
        var field_value = field.value; //文本
        const regx = /@([^@\s]*)$/;
        // 获取全部文字,然后判断光标结束位置的前面一个字符是不是@
        const match = regx.exec(
          field_value.slice(field_value.length - 1, field.selectionEnd)
        );
        if (match && match.length === 2) {
          this.$emit("handleShow");
        } else {
          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;
  height: 150px;
  overflow: hidden;
  overflow-y: scroll;
  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;
    }
  }
  &:hover {
    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>

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值