@@ -46,19 +46,75 @@ export function getTextCursorPosition<
46
46
}
47
47
}
48
48
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
+
49
93
return {
50
94
block : nodeToBlock ( bnBlock . node , pmSchema ) ,
51
95
prevBlock : prevNode === null ? undefined : nodeToBlock ( prevNode , pmSchema ) ,
52
96
nextBlock : nextNode === null ? undefined : nodeToBlock ( nextNode , pmSchema ) ,
53
97
parentBlock :
54
98
parentNode === undefined ? undefined : nodeToBlock ( parentNode , pmSchema ) ,
99
+ offset,
55
100
} ;
56
101
}
57
102
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
+ */
58
114
export function setTextCursorPosition (
59
115
tr : Transaction ,
60
116
targetBlock : BlockIdentifier ,
61
- placement : "start" | "end" = "start" ,
117
+ placementOrOffset : "start" | "end" | number = "start" ,
62
118
) {
63
119
const id = typeof targetBlock === "string" ? targetBlock : targetBlock . id ;
64
120
const pmSchema = getPmSchema ( tr . doc ) ;
@@ -76,43 +132,54 @@ export function setTextCursorPosition(
76
132
77
133
if ( info . isBlockContainer ) {
78
134
const blockContent = info . blockContent ;
135
+ // Handle blocks that have no inline content
79
136
if ( contentType === "none" ) {
80
137
tr . setSelection ( NodeSelection . create ( tr . doc , blockContent . beforePos ) ) ;
81
138
return ;
82
139
}
83
140
141
+ // Determine base position for inline or table content
142
+ let basePos : number ;
84
143
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 ;
94
145
} 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 ;
107
147
} else {
108
148
throw new UnreachableCaseError ( contentType ) ;
109
149
}
110
- } else {
111
- const child =
112
- placement === "start"
113
- ? info . childContainer . node . firstChild !
114
- : info . childContainer . node . lastChild ! ;
115
150
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
+ }
117
184
}
118
185
}
0 commit comments