@@ -123,6 +123,7 @@ export function computeCaretLayoutRectGeometry(
123123 includeDomFallback = true ,
124124) : CaretLayoutRect | null {
125125 if ( ! layout ) return null ;
126+ const originalPos = pos ;
126127
127128 // Geometry-based calculation from layout engine
128129 let effectivePos = pos ;
@@ -214,30 +215,116 @@ export function computeCaretLayoutRectGeometry(
214215 const pageEl = getPageElementByIndex ( painterHost ?? null , hit . pageIndex ) ;
215216 const pageRect = pageEl ?. getBoundingClientRect ( ) ;
216217
217- // Find span containing this pos and measure actual DOM position
218+ if ( includeDomFallback && pageRect ) {
219+ const selection = pageEl ?. ownerDocument ?. getSelection ( ) ;
220+ if ( selection ?. rangeCount && selection . isCollapsed ) {
221+ const nativeRange = selection . getRangeAt ( 0 ) ;
222+ if ( typeof nativeRange . getBoundingClientRect === 'function' ) {
223+ const nativeRect = nativeRange . getBoundingClientRect ( ) ;
224+ const inPageBounds =
225+ Number . isFinite ( nativeRect . left ) &&
226+ Number . isFinite ( nativeRect . top ) &&
227+ Number . isFinite ( nativeRect . height ) &&
228+ nativeRect . height > 0 &&
229+ nativeRect . left >= pageRect . left - 2 &&
230+ nativeRect . left <= pageRect . right + 2 &&
231+ nativeRect . top >= pageRect . top - 2 &&
232+ nativeRect . top <= pageRect . bottom + 2 ;
233+ const nativeX = ( nativeRect . left - pageRect . left ) / zoom ;
234+ const nativeY = ( nativeRect . top - pageRect . top ) / zoom ;
235+ const withinGeometrySanity = Math . abs ( nativeX - result . x ) <= 80 ;
236+ if ( inPageBounds && withinGeometrySanity ) {
237+ return {
238+ pageIndex : hit . pageIndex ,
239+ x : nativeX ,
240+ y : nativeY ,
241+ height : line . lineHeight ,
242+ } ;
243+ }
244+ }
245+ }
246+ }
247+
248+ // Find span containing this pos and measure actual DOM position.
249+ // Prefer a local line-scoped lookup first (fast path), then fall back to a
250+ // bounded page-level probe near the target PM position.
218251 let domCaretX : number | null = null ;
219252 let domCaretY : number | null = null ;
220- const spanEls = pageEl ?. querySelectorAll ( 'span[data-pm-start][data-pm-end]' ) ;
221- for ( const spanEl of Array . from ( spanEls ?? [ ] ) ) {
222- const pmStart = Number ( ( spanEl as HTMLElement ) . dataset . pmStart ) ;
223- const pmEnd = Number ( ( spanEl as HTMLElement ) . dataset . pmEnd ) ;
224- if ( effectivePos >= pmStart && effectivePos <= pmEnd && spanEl . firstChild ?. nodeType === Node . TEXT_NODE ) {
225- const textNode = spanEl . firstChild as Text ;
226- const charIndex = Math . min ( effectivePos - pmStart , textNode . length ) ;
227- const rangeObj = document . createRange ( ) ;
228- rangeObj . setStart ( textNode , charIndex ) ;
229- rangeObj . setEnd ( textNode , charIndex ) ;
230- if ( typeof rangeObj . getBoundingClientRect !== 'function' ) {
231- break ;
232- }
233- const rangeRect = rangeObj . getBoundingClientRect ( ) ;
234- if ( pageRect ) {
235- domCaretX = ( rangeRect . left - pageRect . left ) / zoom ;
236- domCaretY = ( rangeRect . top - pageRect . top ) / zoom ;
253+ const spanCandidates : HTMLElement [ ] = [ ] ;
254+ const pushUnique = ( el : HTMLElement ) => {
255+ if ( ! spanCandidates . includes ( el ) ) spanCandidates . push ( el ) ;
256+ } ;
257+ const collectSpans = ( root : ParentNode | null | undefined ) => {
258+ if ( ! root ) return ;
259+ const spans = root . querySelectorAll ( 'span[data-pm-start][data-pm-end]' ) ;
260+ for ( const span of Array . from ( spans ) ) {
261+ if ( span instanceof HTMLElement ) {
262+ pushUnique ( span ) ;
237263 }
264+ }
265+ } ;
266+
267+ const lineEls = pageEl ?. querySelectorAll ( '.superdoc-line[data-pm-start][data-pm-end]' ) ;
268+ let localLineEl : HTMLElement | null = null ;
269+ for ( const lineEl of Array . from ( lineEls ?? [ ] ) ) {
270+ if ( ! ( lineEl instanceof HTMLElement ) ) continue ;
271+ const lineStart = Number ( lineEl . dataset . pmStart ) ;
272+ const lineEnd = Number ( lineEl . dataset . pmEnd ) ;
273+ if ( ! Number . isFinite ( lineStart ) || ! Number . isFinite ( lineEnd ) ) continue ;
274+ if ( effectivePos >= lineStart && effectivePos <= lineEnd ) {
275+ localLineEl = lineEl ;
238276 break ;
239277 }
240278 }
279+ collectSpans ( localLineEl ) ;
280+
281+ if ( spanCandidates . length === 0 ) {
282+ const MAX_PM_DISTANCE = 8 ;
283+ const pageSpans = pageEl ?. querySelectorAll ( 'span[data-pm-start][data-pm-end]' ) ;
284+ for ( const span of Array . from ( pageSpans ?? [ ] ) ) {
285+ if ( ! ( span instanceof HTMLElement ) ) continue ;
286+ const pmStart = Number ( span . dataset . pmStart ) ;
287+ const pmEnd = Number ( span . dataset . pmEnd ) ;
288+ if ( ! Number . isFinite ( pmStart ) || ! Number . isFinite ( pmEnd ) ) continue ;
289+ if ( effectivePos >= pmStart && effectivePos <= pmEnd ) {
290+ pushUnique ( span ) ;
291+ continue ;
292+ }
293+ if ( Math . abs ( pmStart - effectivePos ) <= MAX_PM_DISTANCE || Math . abs ( pmEnd - effectivePos ) <= MAX_PM_DISTANCE ) {
294+ pushUnique ( span ) ;
295+ }
296+ }
297+ }
298+
299+ const domProbePositions = Array . from ( new Set ( [ originalPos , effectivePos , originalPos - 1 , originalPos + 1 ] ) ) . filter (
300+ ( candidate ) => candidate >= 0 ,
301+ ) ;
302+
303+ let resolved = false ;
304+ for ( const probePos of domProbePositions ) {
305+ for ( const spanEl of spanCandidates ) {
306+ const pmStart = Number ( ( spanEl as HTMLElement ) . dataset . pmStart ) ;
307+ const pmEnd = Number ( ( spanEl as HTMLElement ) . dataset . pmEnd ) ;
308+ if ( probePos >= pmStart && probePos <= pmEnd && spanEl . firstChild ?. nodeType === Node . TEXT_NODE ) {
309+ const textNode = spanEl . firstChild as Text ;
310+ const charIndex = Math . min ( probePos - pmStart , textNode . length ) ;
311+ const rangeObj = document . createRange ( ) ;
312+ rangeObj . setStart ( textNode , charIndex ) ;
313+ rangeObj . setEnd ( textNode , charIndex ) ;
314+ if ( typeof rangeObj . getBoundingClientRect !== 'function' ) {
315+ continue ;
316+ }
317+ const rangeRect = rangeObj . getBoundingClientRect ( ) ;
318+ if ( pageRect ) {
319+ domCaretX = ( rangeRect . left - pageRect . left ) / zoom ;
320+ domCaretY = ( rangeRect . top - pageRect . top ) / zoom ;
321+ resolved = true ;
322+ break ;
323+ }
324+ }
325+ }
326+ if ( resolved ) break ;
327+ }
241328
242329 // If we found a DOM caret position, prefer it to avoid residual drift
243330 if ( includeDomFallback && domCaretX != null && domCaretY != null ) {
0 commit comments