@@ -39,6 +39,7 @@ import {
3939} from '../tables/TableSelectionUtilities.js' ;
4040import { debugLog } from '../selection/SelectionDebug.js' ;
4141import { DOM_CLASS_NAMES , buildAnnotationSelector , DRAGGABLE_SELECTOR } from '@superdoc/dom-contract' ;
42+ import { applyEditableSlotAtInlineBoundary } from '@helpers/ensure-editable-slot-inline-boundary.js' ;
4243import { isSemanticFootnoteBlockId } from '../semantic-flow-constants.js' ;
4344import { CommentsPluginKey } from '@extensions/comment/comments-plugin.js' ;
4445
@@ -62,11 +63,6 @@ const COMMENT_THREAD_HIT_SAMPLE_OFFSETS: ReadonlyArray<readonly [number, number]
6263 [ 0 , - COMMENT_THREAD_HIT_TOLERANCE_PX ] ,
6364 [ 0 , COMMENT_THREAD_HIT_TOLERANCE_PX ] ,
6465] ;
65- // Boundary clicks are intentionally forgiving so near-edge clicks can place the
66- // caret before/after an inline SDT instead of selecting it.
67- const INLINE_SDT_BOUNDARY_OUTSIDE_SLOP_PX = 12 ;
68- const INLINE_SDT_BOUNDARY_INSIDE_SLOP_PX = 4 ;
69-
7066const clamp = ( value : number , min : number , max : number ) : number => Math . max ( min , Math . min ( max , value ) ) ;
7167
7268type CommentThreadHit = {
@@ -1221,18 +1217,10 @@ export class EditorInputManager {
12211217
12221218 // Track click depth for multi-click
12231219 const clickDepth = this . #registerPointerClick( event ) ;
1224- const hitPos = this . #normalizeInlineSdtBoundaryHitPosition(
1225- target ,
1226- event . clientX ,
1227- event . clientY ,
1228- doc ,
1229- hit . pos ,
1230- clickDepth ,
1231- ) ;
12321220
12331221 // Set up drag selection state
12341222 if ( clickDepth === 1 ) {
1235- this . #dragAnchor = hitPos ;
1223+ this . #dragAnchor = hit . pos ;
12361224 this . #dragAnchorPageIndex = hit . pageIndex ;
12371225 this . #pendingMarginClick = this . #callbacks. computePendingMarginClick ?.( event . pointerId , x , y ) ?? null ;
12381226
@@ -1302,33 +1290,33 @@ export class EditorInputManager {
13021290 if ( ! handledByDepth ) {
13031291 try {
13041292 // SD-1584: clicking inside a block SDT selects the node (NodeSelection).
1305- const sdtBlock = clickDepth === 1 ? this . #findStructuredContentBlockAtPos( doc , hitPos ) : null ;
1293+ const sdtBlock = clickDepth === 1 ? this . #findStructuredContentBlockAtPos( doc , hit . pos ) : null ;
13061294 let nextSelection : Selection ;
13071295 let inlineSdtBoundaryPos : number | null = null ;
13081296 let inlineSdtBoundaryDirection : 'before' | 'after' | null = null ;
13091297 if ( sdtBlock ) {
13101298 nextSelection = NodeSelection . create ( doc , sdtBlock . pos ) ;
13111299 } else {
1312- const inlineSdt = clickDepth === 1 ? this . #findStructuredContentInlineAtPos( doc , hitPos ) : null ;
1313- if ( inlineSdt && hitPos >= inlineSdt . end ) {
1300+ const inlineSdt = clickDepth === 1 ? this . #findStructuredContentInlineAtPos( doc , hit . pos ) : null ;
1301+ if ( inlineSdt && hit . pos >= inlineSdt . end ) {
13141302 const afterInlineSdt = inlineSdt . pos + inlineSdt . node . nodeSize ;
13151303 inlineSdtBoundaryPos = afterInlineSdt ;
13161304 inlineSdtBoundaryDirection = 'after' ;
13171305 nextSelection = TextSelection . create ( doc , afterInlineSdt ) ;
1318- } else if ( inlineSdt && hitPos <= inlineSdt . start ) {
1306+ } else if ( inlineSdt && hit . pos <= inlineSdt . start ) {
13191307 inlineSdtBoundaryPos = inlineSdt . pos ;
13201308 inlineSdtBoundaryDirection = 'before' ;
13211309 nextSelection = TextSelection . create ( doc , inlineSdt . pos ) ;
13221310 } else {
1323- nextSelection = TextSelection . create ( doc , hitPos ) ;
1311+ nextSelection = TextSelection . create ( doc , hit . pos ) ;
13241312 }
13251313 if ( ! nextSelection . $from . parent . inlineContent ) {
1326- nextSelection = Selection . near ( doc . resolve ( hitPos ) , 1 ) ;
1314+ nextSelection = Selection . near ( doc . resolve ( hit . pos ) , 1 ) ;
13271315 }
13281316 }
13291317 let tr = editor . state . tr . setSelection ( nextSelection ) ;
13301318 if ( inlineSdtBoundaryPos != null && inlineSdtBoundaryDirection ) {
1331- tr = this . #ensureEditableSlotAtInlineSdtBoundary ( tr , inlineSdtBoundaryPos , inlineSdtBoundaryDirection ) ;
1319+ tr = applyEditableSlotAtInlineBoundary ( tr , inlineSdtBoundaryPos , inlineSdtBoundaryDirection ) ;
13321320 nextSelection = tr . selection ;
13331321 }
13341322 // Preserve stored marks (e.g., formatting selected from toolbar before clicking)
@@ -1344,98 +1332,6 @@ export class EditorInputManager {
13441332 this . #callbacks. scheduleSelectionUpdate ?.( ) ;
13451333 }
13461334
1347- #normalizeInlineSdtBoundaryHitPosition(
1348- target : HTMLElement ,
1349- clientX : number ,
1350- clientY : number ,
1351- doc : ProseMirrorNode ,
1352- fallbackPos : number ,
1353- clickDepth : number ,
1354- ) : number {
1355- if ( clickDepth !== 1 ) return fallbackPos ;
1356-
1357- const line =
1358- target . closest ( `.${ DOM_CLASS_NAMES . LINE } ` ) ??
1359- ( typeof document . elementsFromPoint === 'function'
1360- ? ( document
1361- . elementsFromPoint ( clientX , clientY )
1362- . find ( ( element ) => element instanceof HTMLElement && element . closest ( `.${ DOM_CLASS_NAMES . LINE } ` ) )
1363- ?. closest ( `.${ DOM_CLASS_NAMES . LINE } ` ) as HTMLElement | null )
1364- : null ) ;
1365- if ( ! line ) return fallbackPos ;
1366-
1367- const wrappers = Array . from ( line . querySelectorAll < HTMLElement > ( `.${ DOM_CLASS_NAMES . INLINE_SDT_WRAPPER } ` ) ) ;
1368- const wrapper = wrappers . find ( ( candidate ) => {
1369- const rect = candidate . getBoundingClientRect ( ) ;
1370- const verticallyAligned = clientY >= rect . top - 2 && clientY <= rect . bottom + 2 ;
1371- if ( ! verticallyAligned ) return false ;
1372-
1373- const nearLeftEdge =
1374- clientX >= rect . left - INLINE_SDT_BOUNDARY_OUTSIDE_SLOP_PX &&
1375- clientX <= rect . left + INLINE_SDT_BOUNDARY_INSIDE_SLOP_PX ;
1376- const nearRightEdge =
1377- clientX >= rect . right - INLINE_SDT_BOUNDARY_INSIDE_SLOP_PX &&
1378- clientX <= rect . right + INLINE_SDT_BOUNDARY_OUTSIDE_SLOP_PX ;
1379- return nearLeftEdge || nearRightEdge ;
1380- } ) ;
1381- if ( ! wrapper ) return fallbackPos ;
1382-
1383- const rect = wrapper . getBoundingClientRect ( ) ;
1384- // Treat clicks near left edge as "before SDT", and right half as "after SDT" intent.
1385- const leftSideThreshold = rect . left + rect . width * 0.2 ;
1386- const rightSideThreshold = rect . left + rect . width * 0.5 ;
1387- if ( clientX <= leftSideThreshold ) {
1388- const pmStartRaw = wrapper . dataset . pmStart ;
1389- const pmStart = pmStartRaw != null ? Number ( pmStartRaw ) : NaN ;
1390- if ( ! Number . isFinite ( pmStart ) ) return fallbackPos ;
1391- return Math . max ( 0 , Math . min ( pmStart , doc . content . size ) ) ;
1392- }
1393- if ( clientX < rightSideThreshold ) return fallbackPos ;
1394-
1395- const pmEndRaw = wrapper . dataset . pmEnd ;
1396- const pmEnd = pmEndRaw != null ? Number ( pmEndRaw ) : NaN ;
1397- if ( ! Number . isFinite ( pmEnd ) ) return fallbackPos ;
1398- return Math . max ( 0 , Math . min ( pmEnd + 1 , doc . content . size ) ) ;
1399- }
1400-
1401- #ensureEditableSlotAtInlineSdtBoundary<
1402- T extends {
1403- doc : ProseMirrorNode ;
1404- insertText : ( text : string , from ?: number , to ?: number ) => unknown ;
1405- setSelection : ( selection : Selection ) => unknown ;
1406- selection : Selection ;
1407- } ,
1408- > ( tr : T , pos : number , direction : 'before' | 'after' ) : T {
1409- const clampedPos = Math . max ( 0 , Math . min ( pos , tr . doc . content . size ) ) ;
1410- const needsEditableSlot = ( node : ProseMirrorNode | null | undefined , side : 'before' | 'after' ) =>
1411- ! node ||
1412- node . type ?. name === 'hardBreak' ||
1413- node . type ?. name === 'lineBreak' ||
1414- node . type ?. name === 'structuredContent' ||
1415- ( node . type ?. name === 'run' && ! ( side === 'before' ? node . lastChild ?. isText : node . firstChild ?. isText ) ) ;
1416-
1417- if ( direction === 'before' ) {
1418- const $pos = tr . doc . resolve ( clampedPos ) ;
1419- const nodeBefore = $pos . nodeBefore ;
1420- const shouldInsertBefore = needsEditableSlot ( nodeBefore , 'before' ) ;
1421-
1422- if ( ! shouldInsertBefore ) return tr ;
1423-
1424- tr . insertText ( '\u200B' , clampedPos ) ;
1425- tr . setSelection ( TextSelection . create ( tr . doc , clampedPos + 1 ) ) ;
1426- return tr ;
1427- }
1428-
1429- const nodeAfter = tr . doc . nodeAt ( clampedPos ) ;
1430- const shouldInsertAfter = needsEditableSlot ( nodeAfter , 'after' ) ;
1431-
1432- if ( ! shouldInsertAfter ) return tr ;
1433-
1434- tr . insertText ( '\u200B' , clampedPos ) ;
1435- tr . setSelection ( TextSelection . create ( tr . doc , clampedPos + 1 ) ) ;
1436- return tr ;
1437- }
1438-
14391335 #handlePointerMove( event : PointerEvent ) : void {
14401336 if ( ! this . #deps) return ;
14411337
0 commit comments