转载声明:文章来源https://blog.csdn.net/xgangzai/article/details/129543384
简介
在接到这个需求的时候,我就纳闷,为啥要搞这么多花样,评论就评论吧,它还要你实现艾特某人的时候,还要调用后台的IM接口,给相关人员发送通知推送;说到这里,有点经验的JY应该就想到了,数据参数如何组装和传递这是个关键点,后面再细说。
评论区主要实现的功能点有:表情包选择,艾特符识别并弹出人员选择,还有就是图片选择(篇幅有限,这个放在第二篇文章述说),还有就是支持表情包,艾特人,文本组合显示的文本区域(这个是难点)
组合显示的文本区域,除了能正常显示三种元素节点,还需要将其关联的数据包裹起来,方便在点击发送的时候,将这些数据提取出来进行组装,然后发送给后台;
刚开始,毫无头绪,不知从何下手的时候,想到的就是去寻找一劳永逸的插件,然后发现,比较适合做这方面的插件,无非就是富文本编辑器吧,比如最常用的富文本编辑器Vue-Quill-Editor[1] ,但是研究了一番这开源插件的文档发现,开发的API并不能满足我实际的需求,比如艾特人的情况下数据存储,还有改写后光标的显示和跟踪问题,唉,一番折腾之后又陷入了苦思当中;但问题总归是要解决的,于是我开始萌生了不再依赖插件的想法;经过我一番的努力思索,还真给我整出来了,废话不多说,先上图:
接下来我会从以下几个方面分享我的开发思路喝实现方案
原型描述
思路分析
表情模块实现
艾特符模块实现
数据交互模块实现
代码实现
原型需求
具体的需求是这样子的,请看下方描述:
A.鼠标点击输入框即可开始输入内容
B.当在输入的过程发送了@消息时,被@的人会通过IM收到被@的消息。
输入@后弹出人员选择框,这里的人员包括“所有人”、“创建人”、“负责人”、“子任务负责人”、“参与人”。排序顺序按照此顺序进行展示。
排列在第一个的为“所有人”这里的所有人不包括自己。
下面的其他人员中同样不会显示出自己。
勾选人后,被勾选的人会显示到输入框中。
注意:如果@了所有人,又@了单个人,此时发送出去的消息针对个人只会发送一次。
C.通过@发送出去的消息除了会在当前页面进行展示外,还会通过IM将消息发送给对方:
@了几个人就会把消息单独发给那几个人。如果有相同的需要去重,只会发送一次。
如果@了所有人,那么就会单独发送给所有人(不包含自己)。
被@的人会在IM中收到消息。这条消息来自发起人与被@的人之间的私聊。如果聊天的内容中包含了附件,则附件不会发送到IM,只会发送本文内容和表情。做出了的效果如图:
IM聊天那边就会收到相关任务的评论信息
思路分析
表情模块实现
选择表情包弹层有两种方案,一种是直接图片链接展示,图片命名用中文,另一种是提交给后台的时候是要转译成【微笑】的形式进行保存,而不是图片链接
这里涉及到表情选择后,回显到文本框中。我们所知道的textarea标签只能传进文本内容,所要将div改写成可编辑的文本框,增加contenteditable="true" 属性,通过获取div的Dom,对其进行光标定位,让表情包进行插入,再操作光标移动;可能看到这里的JY有点懵,在代码实现那里会细说。
艾特符模块实现
当监听到用户输入@的时候我们弹出人员选择器,这时候我们需要记住现在光标所在的位置,当用户选择人员完毕之后,我们创建一个Dom在插入到我们刚刚记录光标的位置,并且把我们输入的@删除,将光标放在这个节点的最后;似乎过程描述得还蛮简单的,但不过实现起来还是有点难度;
一、使用到的JavaScript对象:
1.Range Range对象表示的包含节点或者文本节点一段片段
2.Selection Selection对象表示用户的光标开始位置到结束位置的选区
(以上是我的个人理解,具体的需要到MDN上查阅)
二、实现的原理
首先通过const selection = Window.getSelection(),
const range = selection.getrangeAt(0)获取光标的位置,我使用的是create-pos插件准确获取光标的位置
然后监听键盘事件,阻止输入@的默认行为,并且创建一个SPAN标签,内容为@,然后到光标处;
创建两个新的span标签,把@+选中的内容让放到其中一个新建的span标签中,另外一个span标签插入空格
创建一个fragment片段,把第四步中两个span标签一次插入fragment中
最后使用Range对象中的insertNode()方法插入富文本中
第6步完成之后,找到第4步创建并带有@的SPAN节点,然后移除
删除时,首先找到包含@+内容的节点,然后把整个节点一起删除
数据交互模块实现
比如我要评论这样一条信息
接口需要到的参数格式呢,比如IM推送,需要知道你艾特的是谁,还有表情包,到底要的是链接,还是标识符,这些需要分析如何存储和传递的, 而我这个项目的接口需要的数据格式如下:
其中imText是用于在IM聊天窗口中,发送消息的数据格式,
text部分是直接用于评论任务记录的显示,
users是用于存储艾特相关人员的用户id存储
显而易见,我们需要做的就是将users—id存储到艾特符元素里面,imText需要我们从内容框中得到的html,再做个正则匹配将其内容转换和替换成我们需要的数据格式;至于如何实现,待会在代码实现栏上详说
代码实现
表情包评论代码解析
选中表情
appendEmoji(imgSrc) {
// 拿到dom获取光标
const editor = this.$refs.jsEditorElement;
if (editor) {
this.isFocus()
console.log(editor.focus);
this.selectEmoji(imgSrc);
console.log('onChangeJsEditor')
this.onChangeJsEditor('emoji')
}
this.visibleEmoj = false;
}
将文本框的光标位置移动到添加表情后的位置
selectEmoji(url) {
const editor = this.$refs.jsEditorElement;
if (editor) {
editor.focus();
// this.editorRange.selection.collapseToEnd();
// 删掉草稿start
const editorRange = this.editorRange.range;
console.log(
"editorRange",
editorRange,
editorRange.startOffset,
editorRange.endOffset
);
if (!editorRange) {
return;
}
const textNode = editorRange.endContainer; // 拿到末尾文本节点
const endOffset = editorRange.endOffset; // 光标位置
// 找出光标前的at符号位置
// const textNodeValue = textNode.nodeValue
// const expRes = (/@([^@]*)$/).exec(textNodeValue)
// if (expRes && expRes.length > 1) {
// editorRange.setStart(textNode, expRes.index)
editorRange.setEnd(textNode, endOffset);
editorRange.deleteContents(); // 删除草稿end
const dom = this.createInsterImgData(url);
console.log(dom);
console.log(this.editorRange.selection);
console.log(this.editorRange.range);
this.insertHtmlImgAtCaret(
dom,
this.editorRange.selection,
this.editorRange.range
);
// }
}
}
将表情包以加装后img标签的形式累加到文本显示
createInsterImgData(url) {
const btn = document.createElement("img");
btn.setAttribute("src", url);
btn.setAttribute("class", "emo");
btn.setAttribute("style", "width: 26px;height:26px;");
return btn;
}
@艾特某人代码解析
一、选择@按钮事件
可以阅读源码中的selectPerson()方法,这里主要说下,保存@某人信息的问题
给每一个选择的人员,构造一个Dom,设置为a标签元素,然后将用户信息存放在dataset里面,并设置一个classs属性(用于提交的时候通过正则匹配出来提取用户信息), 如下:
this.insertCaret(
`<a data-id="${some.member_id}" data-mid="${some.im_user_id}" class="userSetClass"
data-name="${some.member_name}" contenteditable="false">@${some.member_name}</a> `
);
createInsterData(personArr) {
const temp = [];
for (const person of personArr) {
const btn = document.createElement("a");
btn.dataset.id = person.member_id;
btn.dataset.mid = person.im_user_id;
btn.dataset.name = person.member_name || person.name;
btn.contentEditable = false;
btn.setAttribute("href", "javascript:void(0)");
if(this.allowSelectMembers.length === personArr.length) {
btn.textContent = ` &${person.member_name} `;
btn.setAttribute('style', 'display:none')
btn.setAttribute("class", "userHiddenSetClass");
} else {
btn.textContent = ` @${person.member_name} `;
btn.setAttribute("class", "userSetClass");
}
btn.addEventListener(
"click",
() => {
return false;
},
false
);
btn.tabindex = "-1";
const bSpaceNode = document.createTextNode("\u200B"); // 不可见字符,为了放光标方便
temp.push(btn);
temp.push(bSpaceNode);
}
// 将所有添加进去@所有人
if(this.allowSelectMembers.length === personArr.length) {
const btn = document.createElement("a");
btn.type = "link";
btn.textContent = ` @所有人 `;
btn.contentEditable = false;
btn.setAttribute("class", "userSetAllClass");
btn.setAttribute("href", "javascript:void(0)");
btn.tabindex = "-1";
const bSpaceNode = document.createTextNode("\u200B"); // 不可见字符,为了放光标方便
temp.push(btn);
temp.push(bSpaceNode);
}
return temp;
}
二、输入@符号监听
这里要写个监听函数,当用户按 shift + @ 的时候会被检索到,然后执行获取光标事件,并同时更新人员弹层选择列表getAllNewMembers(),
onInputText(e) {
this.onChangeJsEditor(e.target.innerHTML)
console.log(e.target.innerHTML)
// 这是输入了@,那就直接弹选人浮层
this.doToggleDialog();
console.log(this.editorRange);
if (e.code === "Digit2" && e.shiftKey) {
this.mockInput = false;
console.log("输入@");
// 获取新的参与人alt列表
this.getAllNewMembers()
}
}
三、发布评论前的数据转换
刚才在前面两个模块做的数据组装就是为了最后一步,发表评论的数据提取和传递问题
首先提取用户信息,将其存放在一个数组中
let collect = editor.getElementsByClassName("userSetClass");
console.log(collect, Array.from(collect).length);
for (const child of collect) {
atidsss.push(child.dataset.id);
atnames.push(child.dataset.name);
atmid.push(child.dataset.mid);
}
// @所有人
let AltAll = editor.getElementsByClassName("userSetAllClass")
// if(AltAll.)
if(AltAll && Array.from(AltAll).length > 0) {
let userHiddenSetClass = editor.getElementsByClassName("userHiddenSetClass")
for (const child of userHiddenSetClass) {
atidsss.push(child.dataset.id);
atnames.push(child.dataset.name);
atmid.push(child.dataset.mid);
}
}
其次,将带有img标签的html文本,通过正则表达式将其提取出图片名称,用于IM的消息推送格式
params.imText = params.imText.replace(
/<img [^>]*src=['"]([^'"]+)[^>]*>/gi,
function (match, capture) {
console.log(capture);
let name = that.getLastFileName(capture)
if(name) {
return `[${name}]`
}
return ''
}
)
最后就是整理数据,将后端定义好的数据格式传递给他们
let params = {
task_id: this.activeTaskItemDetailID,
text: this.$refs.jsEditorElement.innerHTML,
imText: this.$refs.jsEditorElement.innerHTML,
images: this.imgList,
users: atidsss ? atidsss.filter((v) => v) : [],
attachment: this.fileList,
}
帖子还没人回复快来抢沙发