抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

最近在仿浮墨笔记 flomo 学习,碰到在富文本编辑器中展示已有的标签 tag,苦恼于如何定位光标输入位置,直到看见大神的代码,恍然大悟。

预期目标

浮墨效果

CPT2203201538-640x116

鼠标点击 # 就会联想已经存在的标签,弹出框位置是根据现在的光标位置变化的

自己查到 textarea 标签定位光标位置的API只能查到是第几个字,而不能查到具体像素位置

CPT2203201548-465x413

代码是这样的:

const textarea = document.getElementById('textarea');
textarea.oninput = function () {
    console.log(textarea.selectionEnd)
}

下面看看大佬的实现方法吧!

实现方法

拷贝一个和输入框相同样式的容器 div,并且将内容也填充进去,然后在内容后面加一个 span 用来计算光标位置,光标位置即 span 距离容器 div 的 offsetTop, offsetLeft

// 获取光标位置 !!!核心代码
const getCursorPostion = (input: HTMLTextAreaElement) => {
  // 获取 “基于 textarea” 的坐标
  const { offsetLeft: inputX, offsetTop: inputY, selectionEnd: selectionPoint } = input;
  const div = document.createElement("div");
  //复制 input 所有样式并添加溢出换行,不显示在dom中
  const copyStyle = window.getComputedStyle(input);
  for (const item of copyStyle) {
    div.style.setProperty(item, copyStyle.getPropertyValue(item));
  }
  div.style.position = "fixed";
  div.style.visibility = "hidden";
  div.style.whiteSpace = "pre-wrap"
  //将 textarea 光标之前的内容拷贝到div上
  const swap = ".";
  const inputValue = input.tagName === "INPUT" ? input.value.replace(/ /g, swap) : input.value;
  const textContent = inputValue.substring(0, selectionPoint || 0);
  div.textContent = textContent;
  if (input.tagName === "TEXTAREA") {
    div.style.height = "auto";
  }
  //为 div 文字后面添加 span
  const span = document.createElement("span");
  span.textContent = inputValue.substring(selectionPoint || 0) || ".";
  div.appendChild(span);
  document.body.appendChild(div);
  //获取 span 距离顶部,左侧的距离
  const { offsetLeft: spanX, offsetTop: spanY } = span;
  document.body.removeChild(div);
  //标签联想框的位置 = 初始位置 + span 位置
  return {
    x: inputX + spanX,
    y: inputY + spanY,
  };
};

为了看的方便,将代码中的从 body 移出 div 代码先注释了

image-20220320160434471

image-20220320160502091

上面一堆属性全是复制 textarea 的,因为有一些默认样式也给复制过来了

文档中的 spandiv 的内容之后(因为行内元素),所以光标位置等于 span 的位置,然后因为 offsetTop 等属性是根据元素左上角定位的,还需要加点偏移,实际获取一下位置试一试

结果非常的 Amazing 啊,这正是输入光标的位置,不得不佩服大佬的思维

image-20220320143258976

这样最后添加点偏移,设置 联想输入框 的位置就可以了

这是整体的一点点代码

// 更新联想框位置
const updateTagSelectorPopupPosition = useCallback(() => {
    if (!editorRef.current || !tagSeletorRef.current) {
        return;
    }

    const seletorPopupWidth = 128;
    const editorWidth = editorRef.current.element.clientWidth;
    // 计算光标位置
    const { x, y } = getCursorPostion(editorRef.current.element);
    // 一些情况需要添加偏移
    const left = x + seletorPopupWidth + 16 > editorWidth ? editorWidth + 20 - seletorPopupWidth : x + 2;
    const top = y + 32 + 6;
	// 设置联想框位置
    tagSeletorRef.current.scroll(0, 0);
    tagSeletorRef.current.style.left = `${left}px`;
    tagSeletorRef.current.style.top = `${top}px`;
}, []);

总结

学习到了解决问题的新思路,之前还遇到过一些 JQuery 大佬的花式操作,越来越证明学框架仅仅只是框架而已,编程思维也要跟上来

评论