@@ -11,12 +11,21 @@ import Registry from "../core/Registry";
1111import { AreaMixin } from "../mixins/AreaMixin" ;
1212import Utils from "../utils" ;
1313import { isDefined } from "../utils/utilities" ;
14- import { findRangeNative } from "../utils/selection-tools" ;
14+ import { findRangeNative , rangeToGlobalOffset } from "../utils/selection-tools" ;
1515
1616const GlobalOffsets = types . model ( "GlobalOffset" , {
1717 start : types . number ,
1818 end : types . number ,
19- } ) ;
19+ // distinguish loaded globalOffsets from user's annotation and internally calculated one;
20+ // we should rely only on calculated offsets to find ranges, see initRangeAndOffsets();
21+ // it should be in the model to avoid reinit on undo/redo.
22+ calculated : false ,
23+ } ) . views ( self => ( {
24+ get serialized ( ) {
25+ // should never get to serialized result
26+ return { start : self . start , end : self . end } ;
27+ } ,
28+ } ) ) ;
2029
2130const Model = types
2231 . model ( "RichTextRegionModel" , {
@@ -33,6 +42,7 @@ const Model = types
3342 } )
3443 . volatile ( ( ) => ( {
3544 hideable : true ,
45+ cachedRange : null ,
3646 } ) )
3747 . views ( self => ( {
3848 get parent ( ) {
@@ -77,7 +87,7 @@ const Model = types
7787
7888 Object . assign ( res . value , {
7989 ...xpathRange ,
80- globalOffsets : self . globalOffsets ?. toJSON ( ) ,
90+ globalOffsets : self . globalOffsets . serialized ,
8191 } ) ;
8292 } catch ( e ) {
8393 // regions may be broken, so they don't have globalOffsets
@@ -88,7 +98,7 @@ const Model = types
8898
8999 if ( self . globalOffsets ) {
90100 Object . assign ( res . value , {
91- globalOffsets : self . globalOffsets ?. toJSON ( ) ,
101+ globalOffsets : self . globalOffsets . serialized ,
92102 } ) ;
93103 }
94104 }
@@ -101,7 +111,8 @@ const Model = types
101111 return res ;
102112 } ,
103113
104- updateOffsets ( startOffset , endOffset ) {
114+ // text regions have only start/end, so we should update start/endOffsets with these values
115+ updateTextOffsets ( startOffset , endOffset ) {
105116 Object . assign ( self , { startOffset, endOffset } ) ;
106117 } ,
107118
@@ -112,42 +123,103 @@ const Model = types
112123 } ) ;
113124 } ,
114125
115- rangeFromGlobalOffset ( ) {
126+ getRangeToHighlight ( ) {
116127 const root = self . _getRootNode ( ) ;
117128
118- if ( self . globalOffsets && isDefined ( root ) ) {
119- return findRangeNative ( self . globalOffsets . start , self . globalOffsets . end , root ) ;
129+ if ( ! root || ! self . globalOffsets ) return undefined ;
130+
131+ const rangeIsMissing = ! self . cachedRange
132+ || self . cachedRange . collapsed
133+ // if this range is in detached iframe it'll look like a good one, check this
134+ || ! self . cachedRange . startContainer ?. ownerDocument ?. defaultView ;
135+
136+ if ( rangeIsMissing ) {
137+ const { start, end } = self . globalOffsets ;
138+
139+ self . cachedRange = findRangeNative ( start , end , root ) ;
120140 }
121141
122- return self . _getRange ( ) ;
142+ return self . cachedRange ;
123143 } ,
124144
125- // For external XPath updates
126- _fixXPaths ( ) {
127- if ( self . isText ) return ;
145+ /**
146+ * Main method to detect HTML range and its offsets for LSF region
147+ * globalOffsets are used for:
148+ * - internal use (get ranges to highlight quickly)
149+ * - end users convenience
150+ * - for emergencies (xpath invalid)
151+ */
152+ initRangeAndOffsets ( ) {
153+ if ( self . globalOffsets ?. calculated ) return ;
154+
155+ const root = self . _getRootNode ( ) ;
156+ let range ;
157+
158+ // 0. Text regions are simple — just get range by offsets
159+ if ( self . isText ) {
160+ const { startOffset : start , endOffset : end } = self ;
161+
162+ self . globalOffsets = { start, end, calculated : true } ;
163+ self . cachedRange = findRangeNative ( start , end , root ) ;
164+ return ;
165+ }
166+
167+ // 1. first try to find range by xpath in original document
168+ range = self . _getRange ( { useOriginalContent : true } ) ;
169+
170+ if ( range ) {
171+ // we need this range in the visible document, so find it by global offsets
172+ const originalRoot = self . _getRootNode ( true ) ;
173+ const [ start , end ] = rangeToGlobalOffset ( range , originalRoot ) ;
128174
129- const range = self . _getRange ( true ) ;
175+ self . globalOffsets = { start, end, calculated : true } ;
176+ self . cachedRange = findRangeNative ( start , end , root ) ;
177+
178+ return ;
179+ }
130180
131- if ( range && self . globalOffsets ) {
132- const root = self . _getRootNode ( true ) ;
181+ // 2. then try to find range on visible document
182+ // that's for old buggy annotations created over dirty document state
183+ range = self . _getRange ( { useOriginalContent : false } ) ;
133184
134- const rangeFromGlobal = findRangeNative (
135- self . globalOffsets . start ,
136- self . globalOffsets . end ,
137- root ,
138- ) ;
185+ if ( range ) {
186+ const [ start , end ] = rangeToGlobalOffset ( range , root ) ;
139187
140- if ( ! rangeFromGlobal ) return ;
188+ self . globalOffsets = { start, end, calculated : true } ;
189+ self . cachedRange = range ;
190+
191+ return ;
192+ }
193+
194+ // 3. if xpaths are broken use globalOffsets if given
195+ if ( self . globalOffsets && isDefined ( root ) ) {
196+ const { start, end } = self . globalOffsets ;
141197
142- const normedRange = xpath . fromRange ( rangeFromGlobal , root ) ;
198+ self . cachedRange = findRangeNative ( start , end , root ) ;
143199
144- if ( ! isDefined ( normedRange ) ) return ;
200+ if ( self . cachedRange ) {
201+ self . _fixXPaths ( self . cachedRange , root ) ;
202+ self . globalOffsets . calculated = true ;
203+ }
145204
146- self . start = normedRange . start ?? self . start ;
147- self . end = normedRange . end ?? self . end ;
148- self . startOffset = normedRange . startOffset ?? self . startOffset ;
149- self . endOffset = normedRange . endOffset ?? self . endOffset ;
205+ return ;
150206 }
207+
208+ // 4. out of options — region is broken
209+ // @todo show error in console and regions list
210+ return undefined ;
211+ } ,
212+
213+ // fix XPaths when we found region by globalOffsets
214+ _fixXPaths ( range , root ) {
215+ const normedRange = xpath . fromRange ( range , root ) ;
216+
217+ if ( ! isDefined ( normedRange ) ) return ;
218+
219+ self . start = normedRange . start ;
220+ self . end = normedRange . end ;
221+ self . startOffset = normedRange . startOffset ;
222+ self . endOffset = normedRange . endOffset ;
151223 } ,
152224
153225 _getRange ( { useOriginalContent = false , useCache = true } = { } ) {
@@ -168,9 +240,13 @@ const Model = types
168240 } ,
169241
170242 _getRootNode ( originalContent = false ) {
171- const ref = originalContent
172- ? self . parent . originalContentRef
173- : self . parent . rootNodeRef ;
243+ const parent = self . parent ;
244+ let ref ;
245+
246+ if ( originalContent ) ref = parent . originalContentRef ;
247+ else if ( parent . useWorkingNode ) ref = parent . workingNodeRef ;
248+ else ref = parent . visibleNodeRef ;
249+
174250 const node = ref . current ;
175251
176252 return node ?. contentDocument ?. body ?? node ;
@@ -183,29 +259,11 @@ const Model = types
183259
184260 const { start, startOffset, end, endOffset } = self ;
185261
186- try {
187- if ( self . isText ) {
188- const { startContainer, endContainer } = Utils . Selection . findRange ( startOffset , endOffset , rootNode ) ;
189- const range = document . createRange ( ) ;
190-
191- if ( ! startContainer || ! endContainer ) return ;
192-
193- range . setStart ( startContainer . node , startContainer . position ) ;
194- range . setEnd ( endContainer . node , endContainer . position ) ;
195-
196- self . text = range . toString ( ) ;
197-
198- return range ;
199- }
200- } catch ( err ) {
201- console . log ( "can't find range" , err ) ;
202- }
203-
204262 try {
205263 return xpath . toRange ( start , startOffset , end , endOffset , rootNode ) ;
206264 } catch ( err ) {
207265 // actually this happens when regions cannot be located by xpath for some reason
208- console . log ( "can't locate xpath" , err ) ;
266+ console . warn ( "can't locate xpath" , { start , end } , err ) ;
209267 }
210268
211269 return undefined ;
0 commit comments