@@ -2,190 +2,137 @@ import { RefObject, useCallback, useMemo, useRef } from "react";
22import { TextSelection } from "../models" ;
33import { 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- */
165export 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}
0 commit comments