-
-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Expand file tree
/
Copy pathuseTextSelection.ts
More file actions
128 lines (109 loc) Β· 3.36 KB
/
useTextSelection.ts
File metadata and controls
128 lines (109 loc) Β· 3.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { useCallback, useLayoutEffect, useState } from "react";
type ClientRect = Record<keyof Omit<DOMRect, "toJSON">, number>;
function roundValues(_rect: ClientRect) {
const rect: ClientRect = { ..._rect };
for (const key of Object.keys(rect) as Array<keyof ClientRect>) {
rect[key] = Math.round(rect[key]);
}
return rect;
}
function shallowDiff(prev?: ClientRect, next?: ClientRect): boolean {
if (prev != null && next != null) {
for (const key of Object.keys(next) as Array<keyof ClientRect>) {
if (prev[key] !== next[key]) {
return true;
}
}
} else if (prev !== next) {
return true;
}
return false;
}
type TextSelectionState = {
clientRect?: ClientRect;
isCollapsed?: boolean;
textContent?: string;
};
const defaultState: TextSelectionState = {};
/**
* useTextSelection(ref)
*
* @description
* hook to get information about the current text selection
*
*/
export function useTextSelection(target?: HTMLElement) {
const [{ clientRect, isCollapsed, textContent }, setState] =
useState<TextSelectionState>(defaultState);
const handler = useCallback(() => {
setState((prev) => {
const selection = window.getSelection();
const nextState: TextSelectionState = {};
if (selection == null || !selection.rangeCount) {
return defaultState;
}
const range = selection.getRangeAt(0);
if (target != null && !target.contains(range.commonAncestorContainer)) {
return defaultState;
}
const contents = range.cloneContents();
if (contents.textContent != null) {
nextState.textContent = contents.textContent;
}
const rects = range.getClientRects();
let computedRect: ClientRect | undefined;
if (rects.length === 0 && range.commonAncestorContainer != null) {
const node = range.commonAncestorContainer;
const el =
node.nodeType === Node.ELEMENT_NODE
? (node as Element)
: node.parentElement ?? document.body;
const r = el.getBoundingClientRect();
computedRect = roundValues({
x: r.x,
y: r.y,
top: r.top,
right: r.right,
bottom: r.bottom,
left: r.left,
width: r.width,
height: r.height,
});
} else if (rects.length > 0) {
const r0 = rects[0];
computedRect = roundValues({
x: r0.x,
y: r0.y,
top: r0.top,
right: r0.right,
bottom: r0.bottom,
left: r0.left,
width: r0.width,
height: r0.height,
});
}
if (computedRect && shallowDiff(prev.clientRect, computedRect)) {
nextState.clientRect = computedRect;
}
nextState.isCollapsed = range.collapsed;
return nextState;
});
}, [target]);
useLayoutEffect(() => {
document.addEventListener("selectionchange", handler);
document.addEventListener("keydown", handler);
document.addEventListener("keyup", handler);
window.addEventListener("resize", handler);
return () => {
document.removeEventListener("selectionchange", handler);
document.removeEventListener("keydown", handler);
document.removeEventListener("keyup", handler);
window.removeEventListener("resize", handler);
};
}, [handler]);
return {
clientRect,
isCollapsed,
textContent,
};
}