Skip to content

Commit 260a851

Browse files
committed
[WIP] useTextSelection hook
1 parent 2e6ce9b commit 260a851

10 files changed

Lines changed: 203 additions & 35 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useMemo, useRef } from "react";
2+
import { useTextSelection } from "../../../../../../packages/react-tools/src"
3+
export const UseTextSelection = () => {
4+
const ref = useRef<HTMLDivElement>(null);
5+
const selection = useTextSelection({ target: ref });
6+
const rectangles = useMemo(() => {
7+
if (!selection) {
8+
return null;
9+
} else {
10+
const rectangles = [];
11+
rectangles.push(
12+
<div
13+
key="outside-rectangle"
14+
style={{
15+
position: "absolute",
16+
border: "1px solid red",
17+
top: selection.outsideRectangle.top + "px",
18+
left: selection.outsideRectangle.left + "px",
19+
width: selection.outsideRectangle.width + "px",
20+
height: selection.outsideRectangle.height + "px",
21+
}}
22+
></div>
23+
);
24+
selection.innerRectangles.forEach((el, index) => {
25+
rectangles.push(<div
26+
key={"inner-rectangle-"+index}
27+
style={{
28+
position: "absolute",
29+
border: "1px solid darkcyan",
30+
top: el.top + "px",
31+
left: el.left + "px",
32+
width: el.width + "px",
33+
height: el.height + "px",
34+
}}
35+
></div>);
36+
})
37+
return rectangles;
38+
}
39+
}, [selection]);
40+
41+
return <div style={{ display: "grid", gridTemplateColumns: "50% 50%", columnGap: 15 }}>
42+
<div ref={ref} style={{ position: "relative", border: "1px solid lightgray" }}>
43+
<div>
44+
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Incidunt repudiandae fugit distinctio molestiae excepturi ex qui, impedit iste odit. Explicabo quis reprehenderit voluptates reiciendis nostrum minima autem temporibus sint doloribus</p>
45+
</div>
46+
<div>
47+
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Incidunt repudiandae fugit distinctio molestiae excepturi ex qui, impedit iste odit. Explicabo quis reprehenderit voluptates reiciendis nostrum minima autem temporibus sint doloribus</p>
48+
</div>
49+
{rectangles}
50+
</div>
51+
<div style={{textAlign: "left", padding: "0 1em", overflow: "auto", border: "1px solid lightgray"}}>
52+
<p><strong>Selection:</strong></p>
53+
<pre>{JSON.stringify(selection, null, 2)}</pre>
54+
</div>
55+
</div>
56+
}

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export const COMPONENTS = [
5353
"useThrottle",
5454
"useActiveElement",
5555
"useTimeout",
56-
"useInterval"
56+
"useInterval",
57+
"useTextSelection"
5758
]
5859
],
5960
//UTILS

apps/react-tools-demo/src/markdown/useStateGetReset.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,18 @@ Custom useState with get and reset state functions.
66
```tsx
77
const UseStateGetReset = () => {
88
const [stateG, setStateG, getState, resetState] = useStateGetReset({ id: "", name: "", eta: "" });
9-
const [state, setState] = useState({ id: "", name: "", eta:"" });
9+
const ref = useRef(getState);
10+
const [state, setState] = useState({ id: "", name: "", eta: "" });
11+
const update = useUpdate();
12+
if (!isShallowEqual(ref.current, getState)) {
13+
console.log("different");
14+
15+
}
16+
17+
useEffect(() => {
18+
const id = setInterval(() => update(), 1000);
19+
return () => clearInterval(id)
20+
}, [update])
1021

1122
const onChangeGetter = useCallback((e: BaseSyntheticEvent) => {
1223
const state = getState();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#
2+
3+
4+
## API
5+
6+
```tsx
7+
const range = document.createRange();
8+
```
9+
10+
> ### Params
11+
>
12+
>
13+
>
14+
15+
> ### Returns
16+
>
17+
>
18+
>
19+
>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ export { useDebounce } from './useDebounce';
3434
export { useThrottle } from './useThrottle';
3535
export { useActiveElement } from './useActiveElement';
3636
export { useTimeout } from './useTimeout';
37-
export { useInterval } from './useInterval';
37+
export { useInterval } from './useInterval';
38+
export { useTextSelection } from './useTextSelection';

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

Lines changed: 97 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,102 @@
1+
import { RefObject, useCallback, useMemo, useRef } from "react";
2+
import { TextSelection } from "../models";
3+
import { isDeepEqual, useSyncExternalStore } from "..";
4+
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.
9+
*/
10+
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 => {
11+
const selectionEnd = useCallback((evt: Event) => {
12+
const element = target
13+
? (target as RefObject<HTMLElement>).current
14+
? (target as RefObject<HTMLElement>).current
15+
: target as HTMLElement
16+
: document;
17+
onEnd && onEnd(evt);
18+
onChange && document.removeEventListener("selectionchange", onChange);
19+
element?.removeEventListener("pointerleave", selectionEndWrap.current!);
20+
document.removeEventListener("pointerup", selectionEndWrap.current!);
21+
}, [onEnd, onChange, target]);
22+
23+
const selectionEndWrap = useRef<EventListenerOrEventListenerObject>();
24+
25+
const selectionStart = useCallback((evt: Event, notif: () => void) => {
26+
const element = target
27+
? (target as RefObject<HTMLElement>).current
28+
? (target as RefObject<HTMLElement>).current
29+
: target as HTMLElement
30+
: document;
31+
if (!element?.contains(evt.target as HTMLElement)) {
32+
return;
33+
}
34+
onStart && onStart(evt);
35+
onChange && document.addEventListener("selectionchange", onChange);
36+
selectionEndWrap.current = (evt: Event) => {
37+
selectionEnd(evt);
38+
notif();
39+
}
40+
document.addEventListener("pointerup", selectionEndWrap.current);
41+
element?.addEventListener("pointerleave", selectionEndWrap.current);
42+
}, [onStart, onChange, selectionEnd, target]);
43+
44+
return useSyncExternalStore(
45+
useCallback(notif => {
46+
const listener = (evt: Event) => {
47+
selectionStart(evt, notif);
48+
}
49+
document.addEventListener("selectstart", listener);
50+
51+
return () => {
52+
document.removeEventListener("selectstart", listener)
53+
}
54+
}, [selectionStart]),
55+
useMemo(() => {
56+
let element = target
57+
? (target as RefObject<HTMLElement>).current
58+
? (target as RefObject<HTMLElement>).current
59+
: target as HTMLElement
60+
: document;
61+
let selection = getTextSelectionDataSet(element !== document ? element as HTMLElement : undefined);
62+
return () => {
63+
const currElement = target
64+
? (target as RefObject<HTMLElement>).current
65+
? (target as RefObject<HTMLElement>).current
66+
: target as HTMLElement
67+
: document;
68+
const currSelection = getTextSelectionDataSet(currElement !== document ? currElement as HTMLElement : undefined);
69+
if (element !== currElement || !isDeepEqual(currSelection, selection)) {
70+
element = currElement;
71+
selection = currSelection;
72+
}
73+
return selection;
74+
}
75+
}, [target])
76+
);
77+
}
78+
179
function getSelectedTextDirection(selection: Selection) {
280
const range = document.createRange();
381
range.setStart(selection.anchorNode!, selection.anchorOffset);
482
range.setEnd(selection.focusNode!, selection.focusOffset);
583
return range.collapsed ? 'backward' : 'forward';
684
}
7-
function getTextSelectionDataSet() {
85+
function getTextSelectionDataSet(parentElement?: HTMLElement): TextSelection | null {
886
const ws = window.getSelection();
987
if (ws === null || ws.toString().trim() === "") {
10-
return { text: "", outsideRectangle: null, innerRectangles: [] };
88+
return null;
1189
}
12-
const data: { text: string, outsideRectangle: DOMRect | null, innerRectangles: Omit<DOMRect, "toJSON">[] } = {
90+
const parentElementDim = (parentElement ?? document.body).getBoundingClientRect();
91+
const selectionDim = ws.getRangeAt(0).getBoundingClientRect();
92+
const data: TextSelection = {
1393
text: ws.toString(),
14-
outsideRectangle: ws.getRangeAt(0).getBoundingClientRect(),
94+
outsideRectangle: new DOMRect(
95+
selectionDim.x - parentElementDim.x,
96+
selectionDim.y - parentElementDim.y,
97+
selectionDim.width,
98+
selectionDim.height,
99+
),
15100
innerRectangles: []
16101
}
17102
const direction = getSelectedTextDirection(ws),
@@ -32,7 +117,7 @@ function getTextSelectionDataSet() {
32117

33118
const range = document.createRange();
34119
range.setStart(text, offset);
35-
selectedText = (text as Node & {data: string}).data.toString().substring(offset);
120+
selectedText = (text as Node & { data: string }).data.toString().substring(offset);
36121
if (allText.length <= selectedText.length) {
37122
range.setEnd(text, offset + allText.length);
38123
ranges.push(range);
@@ -72,15 +157,6 @@ function getTextSelectionDataSet() {
72157
text = text!.nextSibling;
73158
}
74159
}
75-
// if (spanText === null) {
76-
// container = container!.nextSibling;
77-
// for (let i = 0, size = (container! as HTMLElement).children.length; i < size; i++) {
78-
// if ((container! as HTMLElement).children[i].nodeName === "SPAN") {
79-
// spanText = (container! as HTMLElement).children[i];
80-
// break;
81-
// }
82-
// }
83-
// }
84160
const range = document.createRange();
85161
selectedText = (text as Node & { data: string }).data.toString();
86162
range.setStart(text, 0);
@@ -97,18 +173,13 @@ function getTextSelectionDataSet() {
97173

98174
data.innerRectangles = ranges.map(el => {
99175
const dim = el.getBoundingClientRect();
100-
return {
101-
x: dim.x - (data.outsideRectangle ? data.outsideRectangle.x : 0),
102-
y: dim.y - (data.outsideRectangle ? data.outsideRectangle.y : 0),
103-
top: dim.top - (data.outsideRectangle ? data.outsideRectangle.top : 0),
104-
left: dim.left - (data.outsideRectangle ? data.outsideRectangle.left : 0),
105-
right: dim.right - (data.outsideRectangle ? data.outsideRectangle.right : 0),
106-
bottom: dim.bottom - (data.outsideRectangle ? data.outsideRectangle.bottom : 0),
107-
width: dim.width,
108-
height: dim.height
109-
}
176+
return new DOMRect(
177+
dim.x - parentElementDim.x,
178+
dim.y - parentElementDim.y,
179+
dim.width,
180+
dim.height
181+
);
110182
});
111183

112-
window.getSelection()?.removeAllRanges();
113-
console.log("TEXT_SELECTION_RANGES", data);
184+
return data;
114185
}

packages/react-tools/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export {
4343
useThrottle,
4444
useActiveElement,
4545
useTimeout,
46-
useInterval
46+
useInterval,
47+
useTextSelection
4748
} from './hooks'
4849

4950
export {
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export type { DependencyListTyped, CompareFn } from "./common.model";
2-
export type { UseScriptProps, UseScript, UseScriptStatus } from './useScript.model';
2+
export type { UseScriptProps, UseScript, UseScriptStatus } from './useScript.model';
3+
export type { TextSelection } from './useTextSelection.model';
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface TextSelection {
2+
text: string;
3+
outsideRectangle: DOMRect;
4+
innerRectangles: DOMRect[];
5+
}

packages/react-tools/src/utils/isTouchEvent.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import { SyntheticEvent } from "react";
55
* @param {SyntheticEvent} event
66
* @returns {boolean} result
77
*/
8-
export const isTouchEvent = (event: SyntheticEvent): boolean => {
9-
return window.TouchEvent
10-
? event.nativeEvent instanceof TouchEvent
11-
: "touches" in event.nativeEvent;
8+
export const isTouchEvent = (event: SyntheticEvent | Event): boolean => {
9+
return (event as SyntheticEvent).nativeEvent
10+
? window.TouchEvent
11+
? (event as SyntheticEvent).nativeEvent instanceof TouchEvent
12+
: "touches" in (event as SyntheticEvent).nativeEvent
13+
: event instanceof TouchEvent || event instanceof PointerEvent;
1214
}

0 commit comments

Comments
 (0)