乐趣区

关于前端:前端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 (
                `<span contenteditable="false">
                  <span
                    class="at-item"
                    title="${item.original.value}"
                  >
                    @${item.original.value}
                  </span>
                </span>`
              );
            }

            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 (
                `<span contenteditable="false">
                  <span
                    class="pound-item"
                  >
                    #${item.original.name}
                  </span>
                </span>`
              );
            }

            return "#" + item.original.name;
          },
          values: _poundList,
          lookup: "name",
          fillAttr: "name"
        }
      ]
    });

    tributeMultipleTriggers.attach(document.getElementById("editorMultiple") as HTMLElement);
  }
  
  return (
      <div className="at-demo">
          <div
            id="editorMultiple"
            className="tribute-demo-input"
            placeholder="请输出"
          ></div>
      </div>
  )
}

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 "&lt;";
      case ">":
          return "&gt;"
      case "&":
          return "&amp;";
      case "\"":
          return "&quot;";
      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 (
      <div className="at-demo">
          <div
            id="editorMultiple"
            className="tribute-demo-input"
            placeholder="请输出"
          ></div>
          <Button onClick={getDataOfEditorMultiple}> 获取输入框中所有元素 </Button>
      </div>
    )
}

咱们能够间接通过 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 "&lt;";
      case ">":
          return "&gt;"
      case "&":
          return "&amp;";
      case "\"":
          return "&quot;";
      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 (
                `<span contenteditable="false">
                  <span
                    class="at-item"
                    title="${item.original.value}"
                    data-atkey="${item.original.key}"
                    data-atvalue="${item.original.value}"
                  >
                    @${item.original.value}
                  </span>
                </span>`
              );
            }

            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 (
                `<span contenteditable="false">
                  <span
                    class="pound-item"
                    data-poundname="${item.original.name}"
                  >
                    #${item.original.name}
                  </span>
                </span>`
              );
            }

            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 (
      <div className="at-demo">
          <div
            id="editorMultiple"
            className="tribute-demo-input"
            placeholder="请输出"
            onInput={onInput}
          ></div>
          <Button onClick={getDataOfEditorMultiple}> 获取输入框中所有元素 </Button>
      </div>
  )
}

export default AtDemo;

几个关键点的实现

这里提一下几个要害性能点的实现原理。

  • 编辑器的输入框利用的是一般的 div 标签,而后采纳 contenteditable="true" 这个属性来实现的;
  • 援用数据的浮窗定位能够利用 Selection 对象来获取;
  • 被 @ 或 # 援用的数据,想要被一次性删除,能够在被 @ 或 #的数据外蕴含一个 <span contenteditable="false"></span>,示意不可编辑的标签;
  • 把被援用的数据定义为特定的色彩,这个因为咱们在输入框中插入援用数据时,被援用的数据是被 HTML 标签包裹着的,所以咱们只须要对相干的 HTML 进行款式设置就好了;
  • 想要获取被援用数据中的多个属性的值,能够和下面的例子一样,利用 HTML5 的自定义属性 data-xxx 来保留咱们想要的属性值,而后通过遍历标签 el.dataset.xxx 获取咱们想要的属性的值。

最初

本文介绍了一种能够在前端疾速实现 @xxx 选人或援用数据的性能,在局部情景下也算是比拟好的解决方案了。有趣味的同学能够看看文末参考文章中其余大佬们的实现形式。

参考

https://github.com/zurb/tribute
https://segmentfault.com/a/11…
https://segmentfault.com/a/11…
https://juejin.cn/post/698225…
https://mp.weixin.qq.com/s/YP…

退出移动版