前言
在我们日常的网络社交中,@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
选人或引用数据的功能,在部分情景下也算是比较好的解决方案了。有兴趣的同学可以看看文末参考文章中其他大佬们的实现方式。