跟着 ahooks 写 Hook 之 useTextSelection

340 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第16天,点击查看活动详情

💻文档地址:ahooks.js.org/zh-CN/

👨‍💻github地址:github.com/alibaba/hoo…


先来看一个 API

Window.getSelection:返回一个 Selection 对象,表示用户选择的文本范围或光标的当前位置。

const selection = window.getSelection() ;

selection 是一个 Selection 对象。 如果想要将 selection 转换为字符串,可通过连接一个空字符串("")或使用 String.toString() 方法。

Example

function foo() {
    let selObj = window.getSelection();
    console.log(selObj);
    let selRange = selObj.getRangeAt(0);
    let selectedText = selObj.toString();
    // 其他代码
}

浏览器兼容性

图片.png

useTextSelection

初始化 useTextSelection,返回的数据包括选中的文本text,选中区域的top, left, bottom, right, height, width

const initRect: Rect = {
  top: NaN,
  left: NaN,
  bottom: NaN,
  right: NaN,
  height: NaN,
  width: NaN,
};

const initState: State = {
  text: '',
  ...initRect,
};


function useTextSelection(target?: BasicTarget<Document | Element>): State {
  const [state, setState] = useState(initState);
  
  const stateRef = useRef(state);
  stateRef.current = state;

  return state;
},选中之前,如果有文本,则进行初始化。
export default useTextSelection;

useEffect 中,监听 mousedown 事件,任意点击都需要清空之前的信息,选中之前,如果有文本,则进行初始化。

function useTextSelection(target?: BasicTarget<Document | Element>): State {
  // ....

  useEffect(() => {
    // 任意点击都需要清空之前的 range
    const mousedownHandler = () => {
      if (!window.getSelection) return;
      // 选中之前,如果有文本,则进行初始化
      if (stateRef.current.text) {
        setState({ ...initState });
      }
      const selObj = window.getSelection();
      if (!selObj) return;
      selObj.removeAllRanges();
    };

    document.addEventListener('mousedown', mousedownHandler);

    return () => {
      document.removeEventListener('mousedown', mousedownHandler);
    };
  }, [])

  return state;
}

export default useTextSelection;

监听鼠标的 mouseup 事件,当鼠标松开的时候,调用 window.getSelection() API,获取 selection 对象。调用 selObj.toString() 获取文本。

selection.rangeCount:返回一个数字,表示该选区所包含的连续范围的数量。一般为1,因为通常情况下用户只能选择一个范围。

selection.getRangeAt(0):返回选区开始的节点(Node)。因为通常情况下用户只能选择一个范围,所以只有一个选区(range),此方法一般为getRangeAt(0)

Element.getBoundingClientRect():方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。

function getRectFromSelection(selection: Selection | null): Rect {
  if (!selection) {
    return initRect;
  }

  if (selection.rangeCount < 1) {
    return initRect;
  }

  const range = selection.getRangeAt(0);

  const { height, width, top, left, right, bottom } = range.getBoundingClientRect();
  return {
    height,
    width,
    top,
    left,
    right,
    bottom,
  };
}
// 鼠标松开
const mouseupHandler = () => {
  let selObj: Selection | null = null;
  let text = '';
  let rect = initRect;
  if (!window.getSelection) return;
  selObj = window.getSelection();
  text = selObj ? selObj.toString() : '';
  if (text) {
    rect = getRectFromSelection(selObj);
    setState({ ...state, text, ...rect });
  }
};

document.addEventListener('mouseup', mouseupHandler);

Example

import React from 'react';
import { useTextSelection } from 'ahooks';

export default () => {
  const { text } = useTextSelection();
  return (
    <div>
      <p>整个页面可以选择任何文本</p>
      <p>{text}</p>
    </div>
  );
};

Aug-09-2022 16-02-33.gif

监听特定区域文本选择

document.addEventListener('mouseup', mouseupHandler); 中的 document 替换为传入的 DOM 节点。没有传入 targetel -> document

function useTextSelection(target?: BasicTarget<Document | Element>): State {
  useEffect(() => {
    const el = getTargetElement(target, document);
    // 没有传入 target,el -> document
    if (!el) {
      return;
    }

    el.addEventListener('mouseup', mouseupHandler);
  }, [])
  return state;
}

export default useTextSelection;

ahooks 中使用的是 useEffectWithTarget,没有仔细去看,不过看名字,应该是根据传入的 Target 去变化的吧