共计 6837 个字符,预计需要花费 18 分钟才能阅读完成。
前言
在咱们日常的网络社交中,@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 "<";
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 (
<div className="at-demo">
<div
id="editorMultiple"
className="tribute-demo-input"
placeholder="请输出"
></div>
<Button onClick={getDataOfEditorMultiple}> 获取输入框中所有元素 </Button>
</div>
)
}
咱们能够间接通过 getDataOfEditorMultiple
办法间接获取编辑器中的数据,并且本义之后发送给后端。
实时获取编辑器中被援用的数据
咱们有时候可能须要实时的监听编辑器中所数据的数据,或者是被援用的数据。这时咱们能够调用 oninput
这个办法。当然也能够在其余状况调用 onblur
和 onfocus
这两个办法,顾名思义就是失去焦点时和获取焦点时。
残缺的代码如下:
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 (
`<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…