@@ -46,19 +46,75 @@ export function getTextCursorPosition<
4646 }
4747 }
4848
49+ // Compute the offset of the cursor within the block’s inline content. We do
50+ // this by determining the type of content the block holds and then
51+ // calculating the difference between the selection’s anchor position and the
52+ // start of the block’s content.
53+ let offset = 0 ;
54+ try {
55+ const info = getBlockInfoFromTransaction ( tr ) ;
56+ const pmSchemaForOffset = getPmSchema ( tr . doc ) ;
57+ const schema = getBlockNoteSchema ( pmSchemaForOffset ) ;
58+ const contentType = schema . blockSchema [ info . blockNoteType ] ! . content ;
59+ let basePos : number | undefined ;
60+ if ( info . isBlockContainer ) {
61+ const blockContent = info . blockContent ;
62+ if ( contentType === "inline" ) {
63+ // Inline content starts immediately after the opening of the blockContent
64+ basePos = blockContent . beforePos + 1 ;
65+ } else if ( contentType === "table" ) {
66+ // Table content starts a few positions after blockContent to skip table wrappers
67+ basePos = blockContent . beforePos + 4 ;
68+ } else if ( contentType === "none" ) {
69+ // Blocks with no inline content have the cursor anchored at the blockContent itself
70+ basePos = blockContent . beforePos ;
71+ }
72+ } else {
73+ // For wrapper nodes (e.g. columns) there is no meaningful inline offset;
74+ // use the childContainer as a reference if available.
75+ // ChildContainer holds the block’s children; we assume the first child’s
76+ // content starts one position after the container.
77+ const childContainer = ( info as any ) . childContainer ;
78+ if ( childContainer && childContainer . beforePos !== undefined ) {
79+ basePos = childContainer . beforePos + 1 ;
80+ }
81+ }
82+ if ( basePos !== undefined ) {
83+ offset = tr . selection . anchor - basePos ;
84+ if ( offset < 0 ) {
85+ offset = 0 ;
86+ }
87+ }
88+ } catch ( e ) {
89+ // In case of any unexpected errors during offset computation, default to 0.
90+ offset = 0 ;
91+ }
92+
4993 return {
5094 block : nodeToBlock ( bnBlock . node , pmSchema ) ,
5195 prevBlock : prevNode === null ? undefined : nodeToBlock ( prevNode , pmSchema ) ,
5296 nextBlock : nextNode === null ? undefined : nodeToBlock ( nextNode , pmSchema ) ,
5397 parentBlock :
5498 parentNode === undefined ? undefined : nodeToBlock ( parentNode , pmSchema ) ,
99+ offset,
55100 } ;
56101}
57102
103+ /**
104+ * Places the text cursor within a block. By default you can specify
105+ * "start" or "end" to move the cursor to the beginning or end of the block.
106+ * Alternatively, pass a numeric offset to place the cursor that many
107+ * characters into the block’s inline content. Offsets beyond the block’s
108+ * length will be clamped to the end of the block.
109+ *
110+ * @param tr The ProseMirror transaction.
111+ * @param targetBlock Identifier of the block to move the cursor into.
112+ * @param placementOrOffset Either "start", "end", or a zero‑based offset.
113+ */
58114export function setTextCursorPosition (
59115 tr : Transaction ,
60116 targetBlock : BlockIdentifier ,
61- placement : "start" | "end" = "start" ,
117+ placementOrOffset : "start" | "end" | number = "start" ,
62118) {
63119 const id = typeof targetBlock === "string" ? targetBlock : targetBlock . id ;
64120 const pmSchema = getPmSchema ( tr . doc ) ;
@@ -76,43 +132,54 @@ export function setTextCursorPosition(
76132
77133 if ( info . isBlockContainer ) {
78134 const blockContent = info . blockContent ;
135+ // Handle blocks that have no inline content
79136 if ( contentType === "none" ) {
80137 tr . setSelection ( NodeSelection . create ( tr . doc , blockContent . beforePos ) ) ;
81138 return ;
82139 }
83140
141+ // Determine base position for inline or table content
142+ let basePos : number ;
84143 if ( contentType === "inline" ) {
85- if ( placement === "start" ) {
86- tr . setSelection (
87- TextSelection . create ( tr . doc , blockContent . beforePos + 1 ) ,
88- ) ;
89- } else {
90- tr . setSelection (
91- TextSelection . create ( tr . doc , blockContent . afterPos - 1 ) ,
92- ) ;
93- }
144+ basePos = blockContent . beforePos + 1 ;
94145 } else if ( contentType === "table" ) {
95- if ( placement === "start" ) {
96- // Need to offset the position as we have to get through the `tableRow`
97- // and `tableCell` nodes to get to the `tableParagraph` node we want to
98- // set the selection in.
99- tr . setSelection (
100- TextSelection . create ( tr . doc , blockContent . beforePos + 4 ) ,
101- ) ;
102- } else {
103- tr . setSelection (
104- TextSelection . create ( tr . doc , blockContent . afterPos - 4 ) ,
105- ) ;
106- }
146+ basePos = blockContent . beforePos + 4 ;
107147 } else {
108148 throw new UnreachableCaseError ( contentType ) ;
109149 }
110- } else {
111- const child =
112- placement === "start"
113- ? info . childContainer . node . firstChild !
114- : info . childContainer . node . lastChild ! ;
115150
116- setTextCursorPosition ( tr , child . attrs . id , placement ) ;
151+ // Determine target position
152+ let targetPos : number ;
153+ if ( typeof placementOrOffset === "number" ) {
154+ // Clamp the offset to the range of the block’s content
155+ const maxOffset = blockContent . afterPos - basePos - 1 ;
156+ const offset = Math . max ( 0 , Math . min ( placementOrOffset , maxOffset ) ) ;
157+ targetPos = basePos + offset ;
158+ } else if ( placementOrOffset === "start" ) {
159+ targetPos = basePos ;
160+ } else {
161+ // end
162+ if ( contentType === "inline" ) {
163+ targetPos = blockContent . afterPos - 1 ;
164+ } else if ( contentType === "table" ) {
165+ targetPos = blockContent . afterPos - 4 ;
166+ } else {
167+ throw new UnreachableCaseError ( contentType ) ;
168+ }
169+ }
170+ tr . setSelection ( TextSelection . create ( tr . doc , targetPos ) ) ;
171+ } else {
172+ // For wrapper nodes, delegate to the first or last child when using start/end.
173+ const isNumberPlacement = typeof placementOrOffset === "number" ;
174+ const child = ! isNumberPlacement && placementOrOffset === "end"
175+ ? info . childContainer . node . lastChild !
176+ : info . childContainer . node . firstChild ! ;
177+ if ( isNumberPlacement ) {
178+ // For numeric offsets inside wrapper nodes, we cannot determine a meaningful
179+ // character position at this level, so recurse into the first child with the same offset.
180+ setTextCursorPosition ( tr , child . attrs . id , placementOrOffset ) ;
181+ } else {
182+ setTextCursorPosition ( tr , child . attrs . id , placementOrOffset ) ;
183+ }
117184 }
118185}
0 commit comments