• 前端web实现(@、At、艾特)选人或引用数据


    前言

    在我们日常的网络社交中,@XXX 功能可以说是一个比较常见的功能了。 本文将结合实践,介绍一种可以快速实现 @ 选人或引用数据的方式。

    功能需求

    简单的说一下需求:

    1、在输入框中输入 @ ,弹出浮窗,然后可以选择浮窗中相关的数据;
    2、在输入框中输入 # ,弹出浮窗,然后可以选择浮窗中相关的数据;
    3、@# 引用的数据要包含名称和id等,最终要传给后端;
    4、删除 @# 引用的数据时,需要整体删除;
    5、@# 引用的数据需要被标注成不同的颜色。

    大致就是这样。

    技术方案

    在网上参考了不少大佬的文章,也大致了解了一些社交平台的实现方式,有兴趣的朋友可以看看文末的参考。

    最终因为功能的契合度和时间原因,我选择了开源库: tributejs 。这个开源库有原生,Vue 等例子,就是没有 React 的例子,但是问题不大,使用方式都是大同小异。

    具体实现

    本文的 @XXX 功能是 tributejs + React实现的,所以 React 技术栈的同学可以直接参考后面的例子,其他技术栈的同学可以参考 tributejs 官方的实现。

    @功能实现

    首先当然是要下载 tributejs:

    yarn add tributejs
    或者
    npm install tributejs
    

    然后就是引入 tributejs,对想要的功能进行配置,具体各项配置的意义,可以直接到 tributejs 的 GitHub 上查看。最后可以给编辑器加一些自定义的样式:

    index.tsx

    import React, { useEffect, useState, useRef  } from 'react';
    import Tribute from "tributejs";
    import './index.less';
    
    const AtDemo = () => {
      const [atList, setAtList] = useState([
        {
          key: "1",
          value: "小明",
          position: "前端开发工程师"
        },
        {
          key: "2",
          value: "小李",
          position: "后端开发工程师"
        }
      ]);
      const [poundList, setpoundList] = useState([
        { name: "JavaScript", explain: "前端开发语言" },
        { name: "Java", explain: "后端开发语言之一" }
      ]);
    
      useEffect(() => {
        renderEditor(atList, poundList);
      }, [])
    
      const renderEditor = (_atList: any[], _poundList: any[]) => {
        let tributeMultipleTriggers = new Tribute({
          allowSpaces: true,
          noMatchTemplate: function () { return null; },
          collection: [
            {
              selectTemplate: function(item) {
                if (this.range.isContentEditable(this.current.element)) {
                  return (
                    `
                      
                        @${item.original.value}
                      
                    `
                  );
                }
    
                return "@" + item.original?.value;
              },
              values: _atList,
              menuItemTemplate: function (item) {
                return item.original.value;
              },
            },
            {
              trigger: "#",
              selectTemplate: function(item) {
                if (this.range.isContentEditable(this.current.element)) {
                  return (
                    `
                      
                        #${item.original.name}
                      
                    `
                  );
                }
    
                return "#" + item.original.name;
              },
              values: _poundList,
              lookup: "name",
              fillAttr: "name"
            }
          ]
        });
    
        tributeMultipleTriggers.attach(document.getElementById("editorMultiple") as HTMLElement);
      }
      
      return (
          
    ) } export default AtDemo;

    index.less

    .at-demo {
        background-color: #fff;
        padding: 24px;
        .at-item, .pound-item {
            color: #2ba6cb;
        }
    }
    .tribute-container {
        position: absolute;
        top: 0;
        left: 0;
        height: auto;
        overflow: auto;
        display: block;
        z-index: 999999;
      }
      .tribute-container ul {
        margin: 0;
        margin-top: 2px;
        padding: 0;
        list-style: none;
        background: #fff;
        border: 1px solid #3c98fa;
        border-radius: 4px;
      }
      .tribute-container li {
        padding: 5px 5px;
        cursor: pointer;
        border-radius: 4px;
      }
      .tribute-container li.highlight {
        background: #eee;
      }
      .tribute-container li span {
        font-weight: bold;
      }
      .tribute-container li.no-match {
        cursor: default;
      }
      .tribute-container .menu-highlighted {
        font-weight: bold;
    }
    .tribute-demo-input {
      outline: none;
      border: 1px solid #d9d9d9;
      padding: 4px 11px;
      border-radius: 2px;
      font-size: 15px;
      min-height: 100px;
      cursor: text;
    }
    .tribute-demo-input:hover {
      border-color: #3c98fa;
      transition: all 0.3s;
    }
    .tribute-demo-input:focus {
      border-color: #3c98fa;
    }
    [contenteditable="true"]:empty:before {
      content: attr(placeholder);
      display: block;
      color: #ccc;
    }
    #test-autocomplete-container {
      position: relative;
    }
    #test-autocomplete-textarea-container {
      position: relative;
    }
    .float-right {
      float: right;
    }
    
    
    

    我们可以看看效果,还是很不错的:

    在这里插入图片描述

    被引用的数据也是被整体删除的:

    在这里插入图片描述

    获取编辑器中的数据

    我们在编辑器中输入了我们想要的数据,那最终都是要获取其中的数据并且传递给后端的:

    
    ...
    
    import { Button } from 'antd';
    // 转义HTML
    const htmlEscape = (html: string) => {
      return html.replace(/[<>"&]/g,function(match,pos,originalText){
        switch(match){
          case "<":
              return "<";
          case ">":
              return ">"
          case "&":
              return "&";
          case "\"":
              return """;
          default:
            return match;
        }
      });
    }
    
    const AtDemo = () => {
    
        ...
        
        const getDataOfEditorMultiple = () => {
            const childrenData = document.getElementById('editorMultiple')?.innerHTML;
            console.log('childrenData', childrenData)
            const toServiceData = htmlEscape(childrenData);
            console.log('toServiceData', toServiceData)
        }
      
        return (
          
    ) }

    我们可以直接通过 getDataOfEditorMultiple 方法直接获取编辑器中的数据,并且转义之后发送给后端。

    实时获取编辑器中被引用的数据

    我们有时候可能需要实时的监听编辑器中所数据的数据,或者是被引用的数据。这时我们可以调用 oninput 这个方法。当然也可以在其他情况调用 onbluronfocus 这两个方法,顾名思义就是失去焦点时和获取焦点时。

    完整的代码如下:

    import React, { useEffect, useState, useRef  } from 'react';
    import './index.less';
    import Tribute from "tributejs";
    import { Button } from 'antd';
    
    const htmlEscape = (html: string) => {
      return html.replace(/[<>"&]/g,function(match,pos,originalText){
        switch(match){
          case "<":
              return "<";
          case ">":
              return ">"
          case "&":
              return "&";
          case "\"":
              return """;
          default:
            return match;
        }
      });
    }
    
    const AtDemo = () => {
      const [atList, setAtList] = useState([
        {
          key: "1",
          value: "小明",
          position: "前端开发工程师"
        },
        {
          key: "2",
          value: "小李",
          position: "后端开发工程师"
        }
      ]);
      const [poundList, setpoundList] = useState([
        { name: "JavaScript", explain: "前端开发语言" },
        { name: "Java", explain: "后端开发语言之一" }
      ]);
    
      useEffect(() => {
        renderEditor(atList, poundList);
      }, [])
    
      const renderEditor = (_atList: any[], _poundList: any[]) => {
        let tributeMultipleTriggers = new Tribute({
          allowSpaces: true,
          noMatchTemplate: function () { return null; },
          collection: [
            {
              selectTemplate: function(item) {
                if (this.range.isContentEditable(this.current.element)) {
                  return (
                    `
                      
                        @${item.original.value}
                      
                    `
                  );
                }
    
                return "@" + item.original?.value;
              },
              values: _atList,
              menuItemTemplate: function (item) {
                return item.original.value;
              },
            },
            {
              trigger: "#",
              selectTemplate: function(item) {
                if (this.range.isContentEditable(this.current.element)) {
                  return (
                    `
                      
                        #${item.original.name}
                      
                    `
                  );
                }
    
                return "#" + item.original.name;
              },
              values: _poundList,
              lookup: "name",
              fillAttr: "name"
            }
          ]
        });
    
        tributeMultipleTriggers.attach(document.getElementById("editorMultiple") as HTMLElement);
      }
      
      const getDataOfEditorMultiple = () => {
        const childrenData = document.getElementById('editorMultiple')?.innerHTML || '';
        console.log('childrenData', childrenData)
        const toServiceData = htmlEscape(childrenData);
        console.log('toServiceData', toServiceData)
      }
    
      const onInput = () => {
        const atItemList = document.getElementsByClassName('at-item');
        Array.prototype.forEach.call(atItemList, function(el) {
          console.log(el.dataset.atkey);
          console.log(el.dataset.atvalue);
        });
      }
      return (
          
    ) } export default AtDemo;

    几个关键点的实现

    这里提一下几个关键功能点的实现原理。

    • 编辑器的输入框利用的是普通的 div 标签,然后采用 contenteditable="true" 这个属性来实现的;
    • 引用数据的浮窗定位可以利用 Selection对象来获取;
    • 被 @ 或 # 引用的数据,想要被一次性删除,可以在被 @ 或 #的数据外包含一个 ,表示不可编辑的标签;
    • 把被引用的数据定义为特定的颜色,这个因为我们在输入框中插入引用数据时,被引用的数据是被HTML标签包裹着的,所以我们只需要对相关的HTML进行样式设置就好了;
    • 想要获取被引用数据中的多个属性的值,可以和上面的例子一样,利用HTML5的自定义属性 data-xxx 来保存我们想要的属性值,然后通过遍历标签 el.dataset.xxx 获取我们想要的属性的值。

    最后

    本文介绍了一种可以在前端快速实现 @xxx 选人或引用数据的功能,在部分情景下也算是比较好的解决方案了。有兴趣的同学可以看看文末参考文章中其他大佬们的实现方式。

  • 相关阅读:
    Tomcat配置SSL证书别名tomcat无法识别密钥项
    设计模式:代理模式(C#、JAVA、JavaScript、C++、Python、Go、PHP)
    倒计时37天
    算法练习题(涉外黄成老师)
    前端自动识别CAD图纸提取信息方法总结
    【深基16.例1】淘汰赛(下)
    PostgreSQL 认证方式
    OpenGLES:绘制一个颜色渐变的圆
    JAVA大学生活动中心场地管理系统计算机毕业设计Mybatis+系统+数据库+调试部署
    django 链接mysql数据库问题
  • 原文地址:https://blog.csdn.net/qq_42002487/article/details/127114994