前言

在咱们日常的网络社交中,@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';// 本义HTMLconst 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...