Skip to content

Commit 19bfecd

Browse files
committed
[WIP] useTextSelection hook
1 parent 93f6bd7 commit 19bfecd

3 files changed

Lines changed: 84 additions & 136 deletions

File tree

apps/react-tools-demo/src/components/hooks/useTextSelection/UseTextSelection.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useMemo, useRef } from "react";
22
import { useTextSelection } from "../../../../../../packages/react-tools/src"
33
export const UseTextSelection = () => {
44
const ref = useRef<HTMLDivElement>(null);
5-
const selection = useTextSelection({ target: ref });
5+
const selection = useTextSelection({ target: ref, onEnd: () => {getSelection()?.removeAllRanges()} });
66
const rectangles = useMemo(() => {
77
if (!selection) {
88
return null;
@@ -13,7 +13,7 @@ export const UseTextSelection = () => {
1313
key="outside-rectangle"
1414
style={{
1515
position: "absolute",
16-
border: "1px solid red",
16+
border: ".5px solid red",
1717
top: selection.outsideRectangle.top + "px",
1818
left: selection.outsideRectangle.left + "px",
1919
width: selection.outsideRectangle.width + "px",
@@ -26,7 +26,7 @@ export const UseTextSelection = () => {
2626
key={"inner-rectangle-"+index}
2727
style={{
2828
position: "absolute",
29-
border: "1px solid darkcyan",
29+
border: ".5px solid darkcyan",
3030
top: el.top + "px",
3131
left: el.left + "px",
3232
width: el.width + "px",

packages/react-tools/src/hooks/useTextSelection.ts

Lines changed: 80 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -2,190 +2,137 @@ import { RefObject, useCallback, useMemo, useRef } from "react";
22
import { TextSelection } from "../models";
33
import { isDeepEqual, useSyncExternalStore } from "..";
44

5-
/**
6-
* TODO
7-
* non funziona bene, vanno gestiti gli eventi per come qui https://excalidraw.com/#json=7nYktheXKBE2A3aBYxPVx,s3gx5SWbNG-GHk2_VsODRw
8-
* il getTextSelectionDataSet non ritorna i rettangoli se il testo selezionato è di uno stesso elemento ma va a capo. va fatta cosi per avere tutti i rettangoli senza far nessun calcolo:
9-
10-
const ws = getSelection()
11-
if(ws.toString()==="") return
12-
const range = ws.getRangeAt(0);
13-
const rects = range.getClientRects();
14-
15-
*/
165
export const useTextSelection = ({ target, onStart, onChange, onEnd }: { target?: RefObject<HTMLElement> | HTMLElement, onStart?: (evt: Event) => void, onChange?: (evt: Event) => void, onEnd?: (evt: Event) => void } = {}): TextSelection | null => {
6+
const selecting = useRef(false);
7+
const selectedText = useRef<string | null>(null);
8+
const notifRef = useRef<() => void>();
9+
const selection = useRef<TextSelection | null>(null);
10+
1711
const selectionEnd = useCallback((evt: Event) => {
1812
const element = target
19-
? (target as RefObject<HTMLElement>).current
2013
? (target as RefObject<HTMLElement>).current
21-
: target as HTMLElement
22-
: document;
14+
? (target as RefObject<HTMLElement>).current
15+
: target as HTMLElement
16+
: document.body;
17+
selection.current = getTextSelectionDataSet(element ?? document.body);
2318
onEnd && onEnd(evt);
24-
onChange && document.removeEventListener("selectionchange", onChange);
25-
element?.removeEventListener("pointerleave", selectionEndWrap.current!);
26-
document.removeEventListener("pointerup", selectionEndWrap.current!);
27-
}, [onEnd, onChange, target]);
19+
notifRef.current && notifRef.current();
20+
}, [onEnd, target]);
2821

29-
const selectionEndWrap = useRef<EventListenerOrEventListenerObject>();
22+
const pointerDownDocHandler = useCallback(() => {
23+
selecting.current = true;
24+
selectedText.current = null;
25+
}, []);
3026

31-
const selectionStart = useCallback((evt: Event, notif: () => void) => {
32-
const element = target
33-
? (target as RefObject<HTMLElement>).current
34-
? (target as RefObject<HTMLElement>).current
35-
: target as HTMLElement
36-
: document;
37-
if (!element?.contains(evt.target as HTMLElement)) {
38-
return;
27+
const pointerUpLeaveDocHandler = useCallback((evt: Event) => {
28+
selecting.current = false;
29+
if ((getSelection() ?? "").toString() === selectedText.current) {
30+
selectedText.current = null;
31+
selectionEnd(evt);
3932
}
33+
}, [selectionEnd]);
34+
35+
const pointerDownTargetHandler = useCallback((evt: Event) => {
36+
selecting.current = true;
4037
onStart && onStart(evt);
4138
onChange && document.addEventListener("selectionchange", onChange);
42-
selectionEndWrap.current = (evt: Event) => {
43-
selectionEnd(evt);
44-
notif();
39+
}, [onChange, onStart]);
40+
41+
const pointerUpLeaveTargetHandler = useCallback(() => {
42+
selecting.current = false;
43+
selectedText.current = (getSelection() ?? "").toString() || null;
44+
onChange && document.removeEventListener("selectionchange", onChange);
45+
}, [onChange]);
46+
47+
const pointerEnterTargetHandler = useCallback((evt: Event) => {
48+
if (selecting.current && (getSelection() ?? "").toString() === "") {
49+
onStart && onStart(evt);
50+
onChange && document.addEventListener("selectionchange", onChange);
4551
}
46-
document.addEventListener("pointerup", selectionEndWrap.current);
47-
element?.addEventListener("pointerleave", selectionEndWrap.current);
48-
}, [onStart, onChange, selectionEnd, target]);
52+
}, [onStart, onChange]);
4953

5054
return useSyncExternalStore(
5155
useCallback(notif => {
52-
const listener = (evt: Event) => {
53-
selectionStart(evt, notif);
54-
}
55-
document.addEventListener("selectstart", listener);
56+
notifRef.current = notif;
57+
const element = target
58+
? (target as RefObject<HTMLElement>).current
59+
? (target as RefObject<HTMLElement>).current
60+
: target as HTMLElement
61+
: document;
62+
document.addEventListener("pointerdown", pointerDownDocHandler);
63+
document.addEventListener("pointerup", pointerUpLeaveDocHandler);
64+
document.addEventListener("pointerleave", pointerUpLeaveDocHandler);
65+
66+
element && element.addEventListener("pointerdown", pointerDownTargetHandler);
67+
element && element.addEventListener("pointerup", pointerUpLeaveTargetHandler);
68+
element && element.addEventListener("pointerleave", pointerUpLeaveTargetHandler);
69+
element && element.addEventListener("pointerenter", pointerEnterTargetHandler);
5670

5771
return () => {
58-
document.removeEventListener("selectstart", listener)
72+
document.removeEventListener("pointerdown", pointerDownDocHandler);
73+
document.removeEventListener("pointerup", pointerUpLeaveDocHandler);
74+
document.removeEventListener("pointerleave", pointerUpLeaveDocHandler);
75+
76+
element && element.removeEventListener("pointerdown", pointerDownTargetHandler);
77+
element && element.removeEventListener("pointerup", pointerUpLeaveTargetHandler);
78+
element && element.removeEventListener("pointerleave", pointerUpLeaveTargetHandler);
79+
element && element.removeEventListener("pointerenter", pointerEnterTargetHandler);
5980
}
60-
}, [selectionStart]),
81+
}, [pointerDownDocHandler, pointerUpLeaveDocHandler, pointerDownTargetHandler, pointerUpLeaveTargetHandler, pointerEnterTargetHandler, target]),
6182
useMemo(() => {
6283
let element = target
6384
? (target as RefObject<HTMLElement>).current
6485
? (target as RefObject<HTMLElement>).current
6586
: target as HTMLElement
6687
: document;
67-
let selection = getTextSelectionDataSet(element !== document ? element as HTMLElement : undefined);
88+
let currSelection = selection.current;
6889
return () => {
6990
const currElement = target
7091
? (target as RefObject<HTMLElement>).current
7192
? (target as RefObject<HTMLElement>).current
7293
: target as HTMLElement
7394
: document;
74-
const currSelection = getTextSelectionDataSet(currElement !== document ? currElement as HTMLElement : undefined);
75-
if (element !== currElement || !isDeepEqual(currSelection, selection)) {
95+
if (element !== currElement || !isDeepEqual(currSelection, selection.current)) {
7696
element = currElement;
77-
selection = currSelection;
97+
currSelection = selection.current;
7898
}
79-
return selection;
99+
return currSelection;
80100
}
81101
}, [target])
82102
);
83103
}
84104

85-
function getSelectedTextDirection(selection: Selection) {
86-
const range = document.createRange();
87-
range.setStart(selection.anchorNode!, selection.anchorOffset);
88-
range.setEnd(selection.focusNode!, selection.focusOffset);
89-
return range.collapsed ? 'backward' : 'forward';
105+
function getSelectedTextDirection(selection: Selection): TextSelection["direction"] {
106+
const position = selection.anchorNode!.compareDocumentPosition(selection.focusNode!);
107+
let backward = false;
108+
// position == 0 if nodes are the same
109+
if (!position && selection.anchorOffset > selection.focusOffset || position === Node.DOCUMENT_POSITION_PRECEDING) {
110+
backward = true;
111+
}
112+
return backward ? "backward" : "forward"
90113
}
91-
function getTextSelectionDataSet(parentElement?: HTMLElement): TextSelection | null {
114+
function getTextSelectionDataSet(parentElement: HTMLElement): TextSelection | null {
92115
const ws = window.getSelection();
93116
if (ws === null || ws.toString().trim() === "") {
94117
return null;
95118
}
96-
const parentElementDim = (parentElement ?? document.body).getBoundingClientRect();
119+
const parentElementDim = parentElement.getBoundingClientRect();
97120
const selectionDim = ws.getRangeAt(0).getBoundingClientRect();
98121
const data: TextSelection = {
99122
text: ws.toString(),
123+
direction: getSelectedTextDirection(ws),
100124
outsideRectangle: new DOMRect(
101125
selectionDim.x - parentElementDim.x,
102126
selectionDim.y - parentElementDim.y,
103127
selectionDim.width,
104128
selectionDim.height,
105129
),
106-
innerRectangles: []
107-
}
108-
const direction = getSelectedTextDirection(ws),
109-
ranges = [];
110-
let allText = data.text;
111-
let container, element, text: Node | null, selectedText, offset;
112-
113-
if (direction === "backward") {
114-
text = ws.focusNode!;
115-
offset = ws.focusOffset;
116-
} else {
117-
text = ws.anchorNode!;
118-
offset = ws.anchorOffset;
119-
}
120-
121-
element = text!.parentNode!;
122-
container = element!.parentNode!;
123-
124-
const range = document.createRange();
125-
range.setStart(text, offset);
126-
selectedText = (text as Node & { data: string }).data.toString().substring(offset);
127-
if (allText.length <= selectedText.length) {
128-
range.setEnd(text, offset + allText.length);
129-
ranges.push(range);
130-
allText = "";
131-
} else {
132-
range.setEnd(text, offset + selectedText.length);
133-
ranges.push(range);
134-
allText = allText.substring(selectedText.length);
130+
innerRectangles: Array.from(ws.getRangeAt(0).getClientRects() || []).map(el => {
131+
el.x = el.x - parentElementDim.x;
132+
el.y = el.y - parentElementDim.y;
133+
return el;
134+
})
135135
}
136136

137-
while (allText !== "") {
138-
while (allText.charAt(0) === "\n") {
139-
allText = allText.substring(1);
140-
}
141-
if (allText === "") {
142-
break;
143-
}
144-
text = text!.nextSibling;
145-
while (text === null || text!.nodeName !== "#text") {
146-
if (text === null) {
147-
if (element === null || element!.nextSibling === null) {
148-
if (container === null || container!.nextSibling === null) {
149-
container = container!.parentNode;
150-
container = container?.nextSibling ?? null;
151-
element = container?.firstChild ?? null;
152-
text = element?.firstChild ?? null;
153-
} else {
154-
container = container!.nextSibling;
155-
element = container!.firstChild;
156-
text = element?.firstChild ?? null;
157-
}
158-
} else {
159-
element = element!.nextSibling;
160-
text = element!.firstChild;
161-
}
162-
} else {
163-
text = text!.nextSibling;
164-
}
165-
}
166-
const range = document.createRange();
167-
selectedText = (text as Node & { data: string }).data.toString();
168-
range.setStart(text, 0);
169-
if (allText.length <= selectedText.length) {
170-
range.setEnd(text, allText.length);
171-
ranges.push(range);
172-
allText = "";
173-
} else {
174-
range.setEnd(text, selectedText.length);
175-
ranges.push(range);
176-
allText = allText.substring(selectedText.length);
177-
}
178-
}
179-
180-
data.innerRectangles = ranges.map(el => {
181-
const dim = el.getBoundingClientRect();
182-
return new DOMRect(
183-
dim.x - parentElementDim.x,
184-
dim.y - parentElementDim.y,
185-
dim.width,
186-
dim.height
187-
);
188-
});
189-
190137
return data;
191138
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface TextSelection {
22
text: string;
3+
direction: "forward" | "backward";
34
outsideRectangle: DOMRect;
45
innerRectangles: DOMRect[];
56
}

0 commit comments

Comments
 (0)