聊天评论时自定义mention(@)与插入表情功能

在项目中,如果有聊天页面或者待办页面,往往涉及到艾特(@)功能,外面的插件往往不然自定义来满足产品的需求,这时我们需要自定义mention组件来完成功能!!!

下图涉及到了几个功能,一个@功能,插入表情功能,OSS文件前端直传,发送等 ,本人使用的vue页面写的,该文章只讲mention功能与表情插入功能,其它的在其他文章中讲述,具体看下文

聊天评论时自定义mention(@)与插入表情功能_第1张图片

 样式部分



 在App.vue中设置mention的样式,可能该条信息在其他页面也要展示,该条信息的记录弹窗页面,所以,在全局设样式比较好点,具体看个人

/**
 * App.vue中
 * 网址、@功能
 **/
.mention-link, .mention-at {
	color: #46A8FF;
	margin-right: 5px;
}

表情库:

// emote.js中

export var emoteData = [
    ...
    "",
    "",
    "",
    "",
    "",
    "",
    "☠",
    "",
    "",
    "",
    "",
    "",
    "",
    "",
    "",
    "",
    "",
    "",
    "",
    "",
    ...
  ]

逻辑部分:


export default {
	name: "tagDetail",
	components: { VuePdf },
	data() {
		return {
		  ignoreUserDataList:[],  // @功能获取存储用户的数据
		  ignoreUserIdList:[],  // @功能存储用户的选中id集合
          filterUserList: [],  // @列表
          isShowBox: false, // 是否显示@列表
          mentionBottom:0, // @列表位置 bottom
          mentionLeft:20, // @列表位置 left
          savedSelectionMention: null,  // 保存选中位置
          savedSelectionRange: null,  // 保存选中位置
          selectionName: false,  // @列表筛选用户
          isRecording: false,  // 是否正在记录
          // ... 其他功能的变量 ...
        }
    },

    computed: {
        rangeLength() {
          return this.savedSelectionRange.length > 0 && this.savedSelectionRange[0].startOffset - this.savedSelectionMention[0].startOffset
        }
      },
    methods:{
    /**
	 * @函数描述: 输入框输入变化时的回调函数
	 **/
    handleInput(event) {
      const commentBox = document.querySelector('.comment-input-data');
      // 显示隐藏评论按钮
      if(commentBox.innerHTML.length > 0){
        this.showSendBtn = true
        if(this.showDrawerDown && this.showEmoteOrFile === 2){
          this.showDrawerDown = false
        }
      }else {
        this.showSendBtn = false
      }
      if (this.isRecording) {
        this.savedSelectionRange = this.saveSelection();
      }
      // 获取当前选区的范围对象
      const selection = window.getSelection();
      if (selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);
        // 折叠range以确保它的start和end在相同位置
        range.collapse(true);

        // 如果不在编辑div的起始位置,创建一个新的range来查找前一个字符
        if (range.startOffset > 0) {
          // 复制当前选区的范围
          const rangeStart = range.cloneRange();
          // 将这个新range的起始点向前移动一个字符
          rangeStart.setStart(rangeStart.startContainer, range.startOffset - 1);
          // 选择这个字符
          rangeStart.setEnd(rangeStart.startContainer, range.startOffset);

          // 获取这个字符并检查它是否是 '@'
          const charBeforeCursor = rangeStart.toString();
          if (charBeforeCursor === '@') {
            this.ignoreUserIdList = this.mentionCreateUserIdList(commentBox.innerHTML)
            this.savedSelectionMention = this.saveSelection();

            const rect = range.getClientRects()[0];
            this.mentionBottom = rect.top - event.target.getBoundingClientRect().top + 30
            this.mentionLeft = rect.left - event.target.getBoundingClientRect().left + 20
            if(event.target.clientWidth - this.mentionLeft < 85){
              this.mentionLeft = this.mentionLeft - 200
            }
            this.isRecording = true
            let data = {
              ignoreUserIdList: this.ignoreUserIdList,
              tagId: this.tagId,
            }
            get_interaction_mentioning(data).then(res => {
              this.ignoreUserDataList = res.data.data
              this.filterUserList = res.data.data
              this.isShowBox = true
            })
          }
        }
      }
    },

    /**
     * @函数描述: 键盘输入时的回调函数 - 【弹起】
     **/
    handleKeyup() {
      const commentBox = document.querySelector('.comment-input-data');
      this.savedSelectionRange = this.saveSelection();
      if(commentBox.innerHTML === ''){
        this.isRecording = false
      }
      if (this.isRecording) {
        if (this.rangeLength < 0) {
          this.isShowBox = false
        } else {
          this.isShowBox = true
          this.filterSelectionName()
        }
      }
    },

    /**
     * @函数描述: 键盘输入时的回调函数 - 【按下】
     * @param: {object} event
     **/
    handleKeydown(event) {
      const BACKSPACE_KEY = 'Backspace';
      const selection = window.getSelection();
      if (selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);
        // 如果按下的是返回键
        if (event.key === BACKSPACE_KEY) {
          // 检查并且消除选区
          if (!range.collapsed) {
            // 用户有一个激活的选区,可能会删除多个字符
            return;
          }
          // 如果不在内容的开头位置
          if (range.startOffset > 0) {
            // 创建一个新的range来查找要删除的字符
            const rangeToDelete = range.cloneRange();
            rangeToDelete.setStart(rangeToDelete.startContainer, range.startOffset - 1);
            rangeToDelete.setEnd(rangeToDelete.startContainer, range.startOffset);
            const charBeforeCursor = rangeToDelete.toString();
            if (charBeforeCursor === '@') {
              this.isShowBox = false
              this.savedSelectionMention = null;
              this.savedSelectionRange = null;
              this.isRecording = false
            }
          }else {
            this.isRecording = false
            this.savedSelectionMention = null;
          }
            this.showSendBtn = false;
        }
      }
      if(event.keyCode === 13) {
        //回车执行查询
        event.preventDefault()
        this.enterSendCommentOrReply(1)
      }
    },

    /**
     * @函数描述: 过滤输入框内的姓名
     **/
    filterSelectionName() {
      const commentBox = document.querySelector('.comment-input-data');
      this.selectionName = commentBox.innerHTML.replace(/<[^>]*>[^<]*<\/[^>]*>/g, function (match) {
        return match.replace(/[^]*/g, ''); // 使用正则表达式替换标签的内容
      }).split('@')[1].substr(0, this.rangeLength);

      this.filterUserList = this.ignoreUserDataList.filter(item => item.userName.indexOf(this.selectionName) > -1)
    },

    /**
     * 选择用户
     * @param row
     */
    selectUser(row) {
      const span = document.createElement('span');
      span.className = 'mention-at';
      span.id = row.userId;
      span.textContent = `@${row.userName}`;
      span.setAttribute('contenteditable', 'false');

      const commentBox = document.querySelector('.comment-input-data');

      // 重新聚焦到评论框并恢复之前的选择
      commentBox.focus();
      const selection = window.getSelection();
      if (this.savedSelectionMention && this.savedSelectionMention.length > 0) {
        selection.removeAllRanges();
        selection.addRange(this.savedSelectionMention[0]);
      }

      if (selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);
        range.setStart(range.startContainer, range.startOffset - 1);
        range.setEnd(range.startContainer, range.endOffset + this.rangeLength);
        range.deleteContents(); // 删除 "@"

        const newRange = document.createRange();

        range.insertNode(span);
        newRange.setStartAfter(span);
        newRange.collapse(true);

        // APPLY THE NEW RANGE
        selection.removeAllRanges();
        selection.addRange(newRange);
      }

      this.isRecording = false
      this.isShowBox = false;
      // 清除savedSelection状态,因为已经不再需要了
      this.savedSelectionMention = null;
      // this.savedSelectionRange = null;
      this.savedSelectionRange = this.saveSelection()
    },

    /**
     * 这里是之前提到的saveSelection函数的示例实现
     * @returns {*[]}
     */
    saveSelection() {
      const ranges = [];
      const selection = window.getSelection();
      for (let i = 0; i < selection.rangeCount; i++) {
        ranges.push(selection.getRangeAt(i));
      }
      return ranges;
    },

    /**
     * @函数描述: 模糊搜索下拉框精确匹配文字高亮
     * @param {object} content 下拉项的内容
     */
    searchHeightLight(content, title) {
      let reg = ''
      let dataToReplace = `${title}`
      let roleName = `(${content.roleName})`
      if (content.userName.indexOf(title) !== -1) {
        reg = content.userName.replace(title, dataToReplace) + roleName;
      } else {
        reg = content.userName + roleName;
      }
      return reg
    },

	/**
	 * @函数描述: 点击表情添加到输入框
	 * @param: {Object} event 点击的对象
	**/
    insertEmoji(emoji) {
      const commentBox = document.querySelector('.comment-input-data');
      // 重新聚焦到评论框并恢复之前的选择
      commentBox.focus();
      const selection = window.getSelection();
      if (this.savedSelectionRange && this.savedSelectionRange.length > 0) {
        selection.removeAllRanges();
        selection.addRange(this.savedSelectionRange[0]);
      }
      if (selection.rangeCount > 0) {
        const range = selection.getRangeAt(0);
        range.setStart(range.startContainer, range.startOffset);
        range.setEnd(range.startContainer, range.endOffset);
        range.deleteContents();

        const newRange = document.createRange();
        const emojiNode = document.createTextNode(emoji)
        range.insertNode(emojiNode);
        newRange.setStartAfter(emojiNode);
        newRange.collapse(true);

        // APPLY THE NEW RANGE
        selection.removeAllRanges();
        selection.addRange(newRange);
        this.savedSelectionRange = this.saveSelection()
      }

      this.showSendBtn = true;
    },

    /**
	 * @函数描述: 获取@提及功能的用户id集合
     * @param: {string} text
   	 **/
	 mentionCreateUserIdList(string) {
		const parser = new DOMParser();
		const doc = parser.parseFromString(string, "text/html");
		const spanElements = doc.querySelectorAll("span");
		const idValues = Array.from(spanElements).map(span => span.id);
		const filterValues = idValues.filter(item => item !== "")
		return filterValues;
	},
   }
}

 功能是简单,主要是光标的处理问题比较绕

结果:

 聊天评论时自定义mention(@)与插入表情功能_第2张图片

代码持续优化中,如果有更好的方法,欢迎评论区交流!

你可能感兴趣的:(项目中的疑难解惑,vue.js,javascript,ecmascript,前端,web,笔记,经验分享)