Electron/网页应用:键盘选文本时获取Range/Selection对象的方法
问题:键盘选中文本时无法获取Selection信息导致注释功能报错
问题描述
我正在开发一款基于React的Electron EPUB阅读器,主打可访问性。目前遇到一个问题:用键盘选中文本后,触发添加注释的快捷键时会抛出错误Uncaught TypeError: Cannot read properties of undefined (reading 'cleanText'),因为this.state.currentLocation.selectionInfo为空,没法获取选中内容。想请教如何在键盘选文本时正确获取Selection/Range对象的相关信息。
补充说明
这是个开源项目,我已经实现了键盘处理器,预期流程是用户选中文本后按快捷键弹出注释输入框,但键盘选文本时selectionInfo相关参数为空,导致报错。
相关代码
触发注释的调用代码
this.addTextInputForAnnotation(annotationType, this.state.currentLocation.selectionInfo.cleanText, this.state.currentLocation.selectionInfo.rangeInfo.startContainerElementCssSelector, this.state.currentLocation.selectionInfo.rangeInfo.startOffset, this.state.currentLocation.selectionInfo.rangeInfo.endContainerElementCssSelector, this.state.currentLocation.selectionInfo.rangeInfo.endOffset);
Reader类声明
class Reader extends React.Component<IProps, IState>
初始state
this.state = { ... mediaOverlaysState: MediaOverlaysStateEnum.STOPPED, mediaOverlaysPlaybackRate: "1", visibleBookmarkList: [], currentLocation: undefined, divinaNumberOfPages: 0, divinaReadingModeSupported: [], ... };
解决方案
1. 实时获取原生Selection对象(推荐)
不管是鼠标还是键盘选中文本,都可以直接通过window.getSelection()获取当前选中状态,不需要依赖state里的缓存值。修改快捷键处理函数:
const handleAnnotationShortcut = () => { const selection = window.getSelection(); // 无选中内容直接返回 if (!selection || selection.isCollapsed) { // 可选:提示用户先选中文本 return; } const selectedText = selection.toString(); const range = selection.getRangeAt(0); // 传入注释函数前,先获取容器元素的CSS选择器和偏移量 this.addTextInputForAnnotation( annotationType, selectedText, getCssSelector(range.startContainer), range.startOffset, getCssSelector(range.endContainer), range.endOffset ); };
2. 实现CSS选择器生成工具函数
上面用到的getCssSelector用来把DOM元素转换成唯一的CSS选择器,方便后续定位文本位置:
function getCssSelector(element) { // 优先用ID,最快最唯一 if (element.id) return `#${element.id}`; // 根元素直接返回 if (element.tagName === 'BODY') return 'body'; let selector = element.tagName.toLowerCase(); // 加上类名 if (element.className) { selector += `.${element.className.trim().split(/\s+/).join('.')}`; } // 处理同类型兄弟元素,生成:nth-of-type选择器 let siblingIndex = 1; let prevSibling = element.previousElementSibling; while (prevSibling) { if (prevSibling.tagName.toLowerCase() === selector.split('.')[0]) { siblingIndex++; } prevSibling = prevSibling.previousElementSibling; } if (siblingIndex > 1) { selector += `:nth-of-type(${siblingIndex})`; } // 递归拼接父元素选择器 return `${getCssSelector(element.parentElement)} > ${selector}`; }
3. 实时更新state中的选中信息(可选)
如果还是希望通过state管理选中状态,可以监听selectionchange事件,实时更新currentLocation.selectionInfo:
componentDidMount() { // 监听全局选中变化事件 document.addEventListener('selectionchange', this.handleSelectionUpdate); } componentWillUnmount() { // 组件卸载时移除监听 document.removeEventListener('selectionchange', this.handleSelectionUpdate); } handleSelectionUpdate = () => { const selection = window.getSelection(); if (!selection || selection.isCollapsed) { // 无选中内容时清空selectionInfo this.setState(prevState => ({ currentLocation: { ...prevState.currentLocation, selectionInfo: null } })); return; } const range = selection.getRangeAt(0); // 更新state中的选中信息 this.setState(prevState => ({ currentLocation: { ...prevState.currentLocation, selectionInfo: { cleanText: selection.toString(), rangeInfo: { startContainerElementCssSelector: getCssSelector(range.startContainer), startOffset: range.startOffset, endContainerElementCssSelector: getCssSelector(range.endContainer), endOffset: range.endOffset } } } })); };
4. 增加空值防御判断
无论用哪种方式,调用注释函数前必须做空值判断,避免报错:
const { currentLocation } = this.state; // 可选链判断,确保selectionInfo存在 if (!currentLocation?.selectionInfo?.cleanText) { return; } const { cleanText, rangeInfo } = currentLocation.selectionInfo; this.addTextInputForAnnotation( annotationType, cleanText, rangeInfo.startContainerElementCssSelector, rangeInfo.startOffset, rangeInfo.endContainerElementCssSelector, rangeInfo.endOffset );
内容的提问来源于stack exchange,提问作者Alex
相关产品推荐
相关产品推荐

