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

Safari下window.getSelection()获取光标位置失效的解决方案

问题背景

当前项目基于React v17、emoji-mart@^3.0.1开发,遇到三个关联问题:

  • 选中emoji可正常在控制台打印输出,但无法在ContentEditable组件中插入、展示
  • 选中emoji在Chrome可正常展示,桌面端、移动端Safari完全无法显示
  • 排查确认Safari环境下window.getSelection()未正常生效,导致saveCaretPosition执行失败,最终insertEmoji无法向编辑器插入内容
    目标是确认Safari环境下window.getSelection()的可行兼容方案。

复现环境

  • macOS Big Sur 11.4:Safari 14.1.1
  • iPad iOS 13.1.3:Safari

复现效果

  1. Chrome浏览器表现
    chrome运行效果
    chrome控制台输出
  2. Safari浏览器表现
    safari运行效果
    safari控制台输出

原有问题代码

EmojiPicker.tsx

const EmojiPicker = ({ onSelectEmoji }: Props) => {
 const control = useVisibilityControl()

 const handleClickOnEmoji = (emoji: BaseEmoji) => {
   control.hide()
   onSelectEmoji(emoji)
   console.log(emoji) // -> 可正常打印emoji数据
 }

return (
  <div className='editor__header'>
    <Popover
      control={control}
      position='absolute'
      className='z-5'
      trigger={
        <div>
          <Icon
            name='emoji-button'
            size={23}
          />
        </div>
      }
      placement={'bottom-right'}
      disableScrollOnVisible>
      <Picker
        onClick={handleClickOnEmoji}
        showSkinTones={false}
        color='#F1B700'
        showPreview={false}
      />
    </Popover>
  </div>
)}

RangeUtils.ts

const getCaretPosition = () => {
  if (window.getSelection) {
    const sel = window.getSelection()
    if (sel.getRangeAt && sel.rangeCount) {
      return sel.getRangeAt(0)
    }
  }
  return null
}

Editor.tsx

let savedRange: Range

const Editor = ({ placeholders }: Props) => {

  const editorRef = useRef(null)
  const tributeRef = useRef<Tribute<TemplatePlaceholder>>()


  useEffect(() => {
    const editorElm = editorRef.current
    if (!editorElm) return undefined

    if (placeholders && !tributeRef.current) {
      initTribute(editorElm)
    }

    return () => {
      if (tributeRef.current) {
        tributeRef.current.detach(editorElm)
        tributeRef.current = null
      }
    }
  }, [placeholders, initTribute])

 
  const saveCaretPosition = () => {
    savedRange = RangeUtils.getCaretPosition()
  }

  const insertEmoji = (emoji: BaseEmoji) => {
    console.log(isValidCaret()) // -> Safari下打印undefined,Chrome下打印true
    if (!isValidCaret()) {
      console.log('bugsssss') // -> 仅Safari下走到该分支
      return
    }
    
    console.log('emoji selected successfully')
    RangeUtils.restoreCaretPosition(savedRange)
    setTimeout(() => {
      savedRange = RangeUtils.insertTextAtCaret(savedRange, emoji.native)
      onChange(editorRef.current.innerHTML)
    }, 0)
  }

  const isValidCaret = () => {
    const startContainer = savedRange?.startContainer as HTMLElement
    return startContainer?.isContentEditable || startContainer?.parentElement?.isContentEditable
  }

  return (
    <>
      <div className='editor__wrapper editor'>
      
        <EmojiPicker onSelectEmoji={insertEmoji} />

        <div className='editor__content'>
          <ContentEditable
            innerRef={editorRef}
            html={value || ''}
            onKeyDown={event => {
              if (event.key === 'Enter') {
                document.execCommand('insertLineBreak')
                event.preventDefault()
              }
            }}
            onChange={(event: ContentEditableEvent) => {
              onChange(event.target.value)
            }}
            onBlur={() => {
              saveCaretPosition()
              onBlur()
            }}
            onPaste={handlePaste}
            spellCheck={false}
          />
        </div>
      </div>
    </>
  )
}

解决方案

window.getSelection本身在Safari下是完全兼容的,不需要找替代方案,问题出在Safari的两个特有行为:一是点击外部元素触发编辑器失焦时,会先清空选区再触发blur事件,此时拿到的range本身无效;二是点击非编辑器的可交互元素时,Safari会主动转移焦点并丢弃原有编辑器选区。
按以下三点修改即可实现全浏览器兼容:

1. 全场景提前保存光标,不要仅在blur时存储

在编辑器获得焦点、鼠标抬起、键盘输入、选区变化时实时更新保存的range,避免blur时拿到空选区,在Editor组件中新增以下逻辑:

useEffect(() => {
  const editorElm = editorRef.current
  if (!editorElm) return
  const saveValidRange = () => {
    const sel = window.getSelection()
    if (sel?.rangeCount && sel.anchorNode && editorElm.contains(sel.anchorNode)) {
      savedRange = sel.getRangeAt(0)
    }
  }
  editorElm.addEventListener('keyup', saveValidRange)
  editorElm.addEventListener('mouseup', saveValidRange)
  editorElm.addEventListener('focus', saveValidRange)
  document.addEventListener('selectionchange', saveValidRange)
  return () => {
    editorElm.removeEventListener('keyup', saveValidRange)
    editorElm.removeEventListener('mouseup', saveValidRange)
    editorElm.removeEventListener('focus', saveValidRange)
    document.removeEventListener('selectionchange', saveValidRange)
  }
}, [])

2. 阻止emoji面板抢焦点

给emoji触发器、弹出面板的onMouseDown事件加阻止默认行为逻辑,避免点击时编辑器失焦、选区被清空,这是Safari兼容的核心步骤:

// EmojiPicker.tsx中新增
const preventFocusLoss = (e: React.MouseEvent) => {
  e.preventDefault()
}

// 绑定到触发器和弹出层
<Popover
  // 其他原有属性不变
  trigger={
    <div onMouseDown={preventFocusLoss}>
      <Icon name='emoji-button' size={23} />
    </div>
  }
  contentProps={{ onMouseDown: preventFocusLoss }}
>
 {/* Picker组件保持不变 */}
</Popover>

3. 补全RangeUtils的兼容方法

补全缺失的restoreCaretPosition和insertTextAtCaret方法,适配Safari的range操作逻辑:

// RangeUtils.ts中新增
const restoreCaretPosition = (range: Range) => {
  if (!range) return
  const sel = window.getSelection()
  sel?.removeAllRanges()
  sel?.addRange(range)
}

const insertTextAtCaret = (range: Range, text: string) => {
  if (!range) return null
  const sel = window.getSelection()
  range.deleteContents()
  const textNode = document.createTextNode(text)
  range.insertNode(textNode)
  range.setStartAfter(textNode)
  range.collapse(true)
  sel?.removeAllRanges()
  sel?.addRange(range)
  return range
}

修改后在列出的Safari环境、Chrome环境下均可正常插入、展示emoji,原有编辑器逻辑不受影响。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 06:01:01