「好久好久没有更新了,差点把掘金给忘记了。总结了一下原因,自从去年入职了新公司之后负责了很久的低代码平台研发,后又开发了H5页面,直到遇到现在这个问题,我才想更新记录一下,没准就会有xdm和我遇到一样的需求呢,bingo~」
「前言」
最近手头H5项目总体是做一个公司的同事吧,这个很熟悉大部分公司都是有的,在同事吧分享自己一些事或者安利给别人一些好的东西,其中涉及到一个@人员功能,这个是必备的并且是移动端,接到这个需求我就考虑了如下几个问题:
1.@人员的输入必然会涉及富文本,是引入外部比较成熟的第三方插件还是自己手动写一个简易的?
2.如果自己研发,是考虑textarea还是contenteditable?
3.确实有很多关于contenteditable的介绍,那是否适用于移动端呢?
场景:正文部分输入@符号,唤起@人员全屏搜索框,点击人员之后变色
「确定方案」
上文问题回答:
1.由于比较成熟的富文本第三方工具比较繁重,而我们只是一个@人员,使用反而大材小用,所以决定自己研发
2.div的contenteditable属性恰好支持本功能,而且用这个实例相对较多
3.目前参考都是pc端的运用,要具体实践才知移动端(所以在公司测试到了凌晨2点,才大概雏形出来[哭泣])
「研发思路」
- 监听用户输入,匹配用户以@开头的文字。
- 调用搜索弹窗,展示搜索出来的用户列表。
- 监听点击列表选择
- 选择需要@的用户,把对应的HTML文本替换到原文本上。在HTML文本上添加用户的元数据。
》获取光标位置
Range 是用于管理选择范围的通用对象。
文档选择是由 Selection 对象表示的,可通过window.getSelection() 或document.getSelection()过来获取。
// 获取光标位置
const getCursorIndex = () => {
const selection = window.getSelection();
return selection?.focusOffset;
};
// 获取节点
const getRangeNode = () => {
const selection = window.getSelection();
return selection?.focusNode;
};
》插入@用户
弹窗是否展示的逻辑
// 是否展示@
const showAt = () => {
const node = getRangeNode();
if (!node || node.nodeType !== Node.TEXT_NODE)
return false;
const content = node.textContent || "";
const regx = /@([^@\s]*)$/;
const match = regx.exec(content.slice(0, getCursorIndex()));
return match && match.length === 2;
};
然后我们去创建@标签,根据自己的需求来定
//创建@标签
const createAtButton = (user: User(弹窗选中后返回来的值)) => {
const btn = document.createElement('at');
btn.dataset.user = JSON.stringify(user);
btn.className = 'at-button';
btn.contentEditable = 'false';
btn.textContent = `@${user.nickName}`;
return btn;
};
插入@用户,重置光标
//输入@之后,光标后的内容插入
const replaceAtUser = (user: User, state: any) => {
const selectionData = window.selection;
const node = selectionData?.focusNode;
if (node) {
const content = node?.textContent || '';
//获取当前光标位置
const endIndex = selectionData?.focusOffset;
const preSlice = replaceString(content.slice(0, endIndex), '');
const restSlice = content.slice(endIndex);
const parentNode = node?.parentNode!;
const nextNode = node?.nextSibling;
const previousTextNode = new Text(preSlice + '\u200b');
const nextTextNode = new Text('\u200b' + restSlice);
const atButton = createAtButton(user);
parentNode.removeChild(node);
if (nextNode) {
parentNode.insertBefore(previousTextNode, nextNode);
parentNode.insertBefore(atButton, nextNode);
parentNode.insertBefore(nextTextNode, nextNode);
} else {
parentNode.appendChild(previousTextNode);
parentNode.appendChild(atButton);
parentNode.appendChild(nextTextNode);
}
//重置光标
const range = new Range();
range.setStart(nextTextNode, 1);
range.setEnd(nextTextNode, 1);
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
}
};
》Dom部分
<div
placeholder="请输入内容(支持@人员哦)"
className="editor"
contentEditable
onInput={() => handkeKeyUp(false)}
//粘贴处理
onPaste={(e) => {
const text = e.clipboardData.getData('Text');
document.execCommand('insertText', false, text);
e.preventDefault();
}}
></div>
//@的人员弹窗
<ComponentFullScreenDialog isShow={visible}>
<Aite from="edit" getAiteChildren={getAiteChildren} closeCallback={closeCallback} />
</ComponentFullScreenDialog>
</div>
到这里比较核心的点就完毕了,但是还有很多问题
「移动端和contenteditable问题」
- [ ios不能整体进行删除,所以删除@人员只能单个删除 ]
//css部分
-webkit-user-select: text;
user-select: text;
-
[ 如果还有点击按钮可以输入@的,要特殊处理 ] 之后参考源码部分吧,如果有需要
-
[ contenteditable换行或者输入@之后,经常出现莫名的标签 ] 这点做了很多处理,在最后传给服务端的时候把没用的标签都用正则替换掉 在输入删除了之后也有做过处理
「总结」
关于富文本的坑是很多,对于移动端的坑会更偏多一些。如果是过于复杂的富文本要求不建议用contenteditable进行研发,有什么问题欢迎随时评论~