仿微博@人员功能实现

1,826 阅读3分钟

WechatIMG2的副本.jpeg

「好久好久没有更新了,差点把掘金给忘记了。总结了一下原因,自从去年入职了新公司之后负责了很久的低代码平台研发,后又开发了H5页面,直到遇到现在这个问题,我才想更新记录一下,没准就会有xdm和我遇到一样的需求呢,bingo~」

「前言」

最近手头H5项目总体是做一个公司的同事吧,这个很熟悉大部分公司都是有的,在同事吧分享自己一些事或者安利给别人一些好的东西,其中涉及到一个@人员功能,这个是必备的并且是移动端,接到这个需求我就考虑了如下几个问题:
1.@人员的输入必然会涉及富文本,是引入外部比较成熟的第三方插件还是自己手动写一个简易的?
2.如果自己研发,是考虑textarea还是contenteditable?
3.确实有很多关于contenteditable的介绍,那是否适用于移动端呢?

场景:正文部分输入@符号,唤起@人员全屏搜索框,点击人员之后变色

WechatIMG10.jpeg

「确定方案」

上文问题回答:
1.由于比较成熟的富文本第三方工具比较繁重,而我们只是一个@人员,使用反而大材小用,所以决定自己研发
2.div的contenteditable属性恰好支持本功能,而且用这个实例相对较多
3.目前参考都是pc端的运用,要具体实践才知移动端(所以在公司测试到了凌晨2点,才大概雏形出来[哭泣])

「研发思路」

  1. 监听用户输入,匹配用户以@开头的文字。
  2. 调用搜索弹窗,展示搜索出来的用户列表。
  3. 监听点击列表选择
  4. 选择需要@的用户,把对应的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换行或者输入@之后,经常出现莫名的标签 ] 这点做了很多处理,在最后传给服务端的时候把没用的标签都用正则替换掉 在输入删除了之后也有做过处理 截屏2022-02-14 下午7.29.37.png

「总结」

关于富文本的坑是很多,对于移动端的坑会更偏多一些。如果是过于复杂的富文本要求不建议用contenteditable进行研发,有什么问题欢迎随时评论~

「参考」

Twitter和微博都在用的 @ 人的功能是如何设计与实现的?
MDN