当前位置: 首页 > Web前端 > HTML5

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

时间:2023-04-05 12:57:18 HTML5

前言在我们日常的网络社交中,@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.tsximport 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 这个方法。当然也可以在其他情况调用 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 "&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/tributehttps://segmentfault.com/a/11...https://segmentfault.com/a/11...https://juejin.cn/post/698225...https://mp.weixin.qq.com/s/YP...