You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React环境下如何让contentEditable div失焦时仍显示非闪烁光标

React实现contentEditable失焦时显示非闪烁光标(仿Google Docs)

核心思路

Google Docs的实现逻辑是避免编辑器真正失焦,或在失焦后立即恢复选区并通过CSS模拟静态光标。下面是具体的React实现方案:


步骤1:保存编辑器选区状态

通过监听全局selectionchange事件,实时保存编辑器内的选区位置,确保失焦后能精准恢复:

import { useRef, useState, useEffect } from 'react';

function EditableEditor() {
  const editorRef = useRef(null);
  const [savedRange, setSavedRange] = useState(null);

  useEffect(() => {
    const handleSelectionChange = () => {
      const selection = window.getSelection();
      // 仅保存编辑器内部的选区
      if (selection.rangeCount > 0 && editorRef.current?.contains(selection.anchorNode)) {
        setSavedRange(selection.getRangeAt(0).cloneRange());
      }
    };

    document.addEventListener('selectionchange', handleSelectionChange);
    return () => document.removeEventListener('selectionchange', handleSelectionChange);
  }, []);

步骤2:拦截页面点击,恢复选区并渲染静态光标

监听页面点击事件,当点击区域不在编辑器内时,强制编辑器重新聚焦并恢复之前的选区,同时通过CSS变量定位自定义光标:

useEffect(() => {
    const handleDocumentClick = (e) => {
      if (!editorRef.current || editorRef.current.contains(e.target)) return;

      // 恢复编辑器焦点与选区
      editorRef.current.focus();
      const selection = window.getSelection();
      selection.removeAllRanges();
      if (savedRange) {
        selection.addRange(savedRange);
        // 计算光标相对编辑器的位置,设置CSS变量
        const rect = savedRange.getBoundingClientRect();
        const editorRect = editorRef.current.getBoundingClientRect();
        editorRef.current.style.setProperty('--cursor-left', `${rect.left - editorRect.left}px`);
        editorRef.current.style.setProperty('--cursor-top', `${rect.top - editorRect.top}px`);
      }
      // 启用静态光标样式
      editorRef.current.classList.add('non-blinking-cursor');
    };

    document.addEventListener('click', handleDocumentClick);
    return () => document.removeEventListener('click', handleDocumentClick);
  }, [savedRange]);

步骤3:处理编辑器失焦边界情况

监听编辑器blur事件,针对Tab键切换、外部组件聚焦等场景,延迟恢复选区:

const handleBlur = () => {
    // 延迟执行,确保点击事件优先处理
    setTimeout(() => {
      const selection = window.getSelection();
      if (selection.rangeCount === 0 && savedRange) {
        editorRef.current.focus();
        selection.removeAllRanges();
        selection.addRange(savedRange);
        // 重新计算光标位置
        const rect = savedRange.getBoundingClientRect();
        const editorRect = editorRef.current.getBoundingClientRect();
        editorRef.current.style.setProperty('--cursor-left', `${rect.left - editorRect.left}px`);
        editorRef.current.style.setProperty('--cursor-top', `${rect.top - editorRect.top}px`);
        editorRef.current.classList.add('non-blinking-cursor');
      }
    }, 10);
  };

步骤4:CSS实现静态光标

隐藏默认闪烁光标,通过伪元素绘制静态光标:

.content-editable {
  position: relative;
  /* 隐藏系统默认光标 */
  caret-color: transparent;
  padding: 8px;
  min-height: 200px;
  border: 1px solid #ddd;
}

/* 静态光标样式 */
.content-editable.non-blinking-cursor::after {
  content: '';
  position: absolute;
  width: 2px;
  height: 1.2em;
  background-color: #222;
  left: var(--cursor-left, 0);
  top: var(--cursor-top, 0);
  pointer-events: none; /* 不干扰点击交互 */
}

完整组件返回部分

return (
    <div
      ref={editorRef}
      contentEditable
      className="content-editable"
      onBlur={handleBlur}
      onClick={() => editorRef.current.classList.remove('non-blinking-cursor')}
    >
      在这里输入内容...
    </div>
  );
}

export default EditableEditor;

注意事项

  • 若编辑器内容存在滚动,需在滚动事件中重新计算光标位置
  • 富文本场景下,需针对不同元素(如标题、列表)调整光标高度
  • 可通过修改CSS变量自定义光标的颜色、宽度和偏移量

内容的提问来源于stack exchange,提问作者Lorenz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 18:58:33