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
复现效果
- Chrome浏览器表现


- 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
相关产品推荐
相关产品推荐

