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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 04:15:25