@@ -27,6 +27,7 @@ import {
2727import { nodeToBlock } from "../../../nodeConversions/nodeToBlock.js" ;
2828import { getNodeById } from "../../../nodeUtil.js" ;
2929import { getPmSchema } from "../../../pmUtil.js" ;
30+ import { TableMap } from "prosemirror-tables" ;
3031
3132// for compatibility with tiptap. TODO: remove as we want to remove dependency on tiptap command interface
3233export const updateBlockCommand = <
@@ -64,30 +65,9 @@ export function updateBlockTr<
6465) {
6566 const blockInfo = getBlockInfoFromResolvedPos ( tr . doc . resolve ( posBeforeBlock ) ) ;
6667
67- const originalSelection = tr . selection ;
68- // Capturing current selection info for restoring it after the block is updated.
69- let anchorOffset : number | undefined = undefined ;
70- let headOffset : number | undefined = undefined ;
71-
72- if (
73- originalSelection instanceof TextSelection &&
74- blockInfo . isBlockContainer &&
75- blockInfo . blockContent
76- ) {
77- // Ensure both anchor and head are within the block's content before proceeding.
78- const isAnchorInContent =
79- originalSelection . anchor >= blockInfo . blockContent . beforePos + 1 &&
80- originalSelection . anchor <= blockInfo . blockContent . afterPos - 1 ;
81- const isHeadInContent =
82- originalSelection . head >= blockInfo . blockContent . beforePos + 1 &&
83- originalSelection . head <= blockInfo . blockContent . afterPos - 1 ;
84-
85- if ( isAnchorInContent && isHeadInContent ) {
86- anchorOffset =
87- originalSelection . anchor - ( blockInfo . blockContent . beforePos + 1 ) ;
88- headOffset =
89- originalSelection . head - ( blockInfo . blockContent . beforePos + 1 ) ;
90- }
68+ let cellAnchor : CellAnchor | null = null ;
69+ if ( blockInfo . blockNoteType === "table" ) {
70+ cellAnchor = captureCellAnchor ( tr ) ;
9171 }
9272
9373 const pmSchema = getPmSchema ( tr ) ;
@@ -170,32 +150,8 @@ export function updateBlockTr<
170150 ...block . props ,
171151 } ) ;
172152
173- // Restore selection
174- const newBlockInfo = getBlockInfoFromResolvedPos (
175- tr . doc . resolve ( tr . mapping . map ( posBeforeBlock ) ) ,
176- ) ;
177-
178- // If we captured relative offsets, try to restore the selection using them.
179- if (
180- anchorOffset !== undefined &&
181- headOffset !== undefined &&
182- newBlockInfo . isBlockContainer &&
183- newBlockInfo . blockContent
184- ) {
185- const contentNode = newBlockInfo . blockContent . node ;
186- const contentStartPos = newBlockInfo . blockContent . beforePos + 1 ;
187- const contentEndPos = contentStartPos + contentNode . content . size ;
188-
189- const newAnchorPos = Math . min (
190- contentStartPos + anchorOffset ,
191- contentEndPos ,
192- ) ;
193- const newHeadPos = Math . min ( contentStartPos + headOffset , contentEndPos ) ;
194-
195- tr . setSelection ( TextSelection . create ( tr . doc , newAnchorPos , newHeadPos ) ) ;
196- } else {
197- // Fallback to the default mapping if we couldn't use the offset method.
198- tr . setSelection ( originalSelection . map ( tr . doc , tr . mapping ) ) ;
153+ if ( cellAnchor ) {
154+ restoreCellAnchor ( tr , blockInfo , cellAnchor ) ;
199155 }
200156}
201157
@@ -383,3 +339,105 @@ export function updateBlock<
383339 const pmSchema = getPmSchema ( tr ) ;
384340 return nodeToBlock ( blockContainerNode , pmSchema ) ;
385341}
342+
343+ type CellAnchor = { row : number ; col : number ; offset : number } ;
344+
345+ /**
346+ * Captures the cell anchor from the current selection.
347+ * @param tr - The transaction to capture the cell anchor from.
348+ *
349+ * @returns The cell anchor, or null if no cell is selected.
350+ */
351+ export function captureCellAnchor ( tr : Transaction ) : CellAnchor | null {
352+ const sel = tr . selection ;
353+ if ( ! ( sel instanceof TextSelection ) ) return null ;
354+
355+ const $head = tr . doc . resolve ( sel . head ) ;
356+ // Find enclosing cell and table
357+ let cellDepth = - 1 ;
358+ let tableDepth = - 1 ;
359+ for ( let d = $head . depth ; d >= 0 ; d -- ) {
360+ const name = $head . node ( d ) . type . name ;
361+ if ( cellDepth < 0 && ( name === "tableCell" || name === "tableHeader" ) ) {
362+ cellDepth = d ;
363+ }
364+ if ( name === "table" ) {
365+ tableDepth = d ;
366+ break ;
367+ }
368+ }
369+ if ( cellDepth < 0 || tableDepth < 0 ) return null ;
370+
371+ // Absolute positions (before the cell)
372+ const cellPos = $head . before ( cellDepth ) ;
373+ const tablePos = $head . before ( tableDepth ) ;
374+ const table = tr . doc . nodeAt ( tablePos ) ;
375+ if ( ! table || table . type . name !== "table" ) return null ;
376+
377+ // Visual grid position via TableMap (handles spans)
378+ const map = TableMap . get ( table ) ;
379+ const rel = cellPos - ( tablePos + 1 ) ; // relative to inside table
380+ const idx = map . map . indexOf ( rel ) ;
381+ if ( idx < 0 ) return null ;
382+
383+ const row = Math . floor ( idx / map . width ) ;
384+ const col = idx % map . width ;
385+
386+ // Caret offset relative to the start of paragraph text
387+ const paraPos = cellPos + 1 ; // pos BEFORE tableParagraph
388+ const textStart = paraPos + 1 ; // start of paragraph text
389+ const offset = Math . max ( 0 , sel . head - textStart ) ;
390+
391+ return { row, col, offset } ;
392+ }
393+
394+ function restoreCellAnchor (
395+ tr : Transaction ,
396+ blockInfo : BlockInfo ,
397+ a : CellAnchor ,
398+ ) : boolean {
399+ if ( blockInfo . blockNoteType !== "table" ) return false ;
400+
401+ // 1) Resolve the table node in the current document
402+ let tablePos = - 1 ;
403+
404+ if ( blockInfo . isBlockContainer ) {
405+ // Prefer the blockContent position when available (points directly at the PM table node)
406+ tablePos = tr . mapping . map ( blockInfo . blockContent . beforePos ) ;
407+ } else {
408+ // Fallback: scan within the mapped bnBlock range to find the inner table node
409+ const start = tr . mapping . map ( blockInfo . bnBlock . beforePos ) ;
410+ const end = start + ( tr . doc . nodeAt ( start ) ?. nodeSize || 0 ) ;
411+ tr . doc . nodesBetween ( start , end , ( node , pos ) => {
412+ if ( node . type . name === "table" ) {
413+ tablePos = pos ;
414+ return false ;
415+ }
416+ return true ;
417+ } ) ;
418+ }
419+
420+ const table = tablePos >= 0 ? tr . doc . nodeAt ( tablePos ) : null ;
421+ if ( ! table || table . type . name !== "table" ) return false ;
422+
423+ // 2) Clamp row/col to the table’s current grid
424+ const map = TableMap . get ( table ) ;
425+ const row = Math . max ( 0 , Math . min ( a . row , map . height - 1 ) ) ;
426+ const col = Math . max ( 0 , Math . min ( a . col , map . width - 1 ) ) ;
427+
428+ // 3) Compute the absolute position of the target cell (pos BEFORE the cell)
429+ const cellIndex = row * map . width + col ;
430+ const relCellPos = map . map [ cellIndex ] ; // relative to (tablePos + 1)
431+ if ( relCellPos == null ) return false ;
432+ const cellPos = tablePos + 1 + relCellPos ;
433+
434+ // 4) Place the caret inside the cell, clamping the text offset
435+ const textPos = cellPos + 1 ;
436+ const textNode = tr . doc . nodeAt ( textPos ) ;
437+ const textStart = textPos + 1 ;
438+ const max = textNode ? textNode . content . size : 0 ;
439+ const head = textStart + Math . max ( 0 , Math . min ( a . offset , max ) ) ;
440+
441+ tr . setSelection ( TextSelection . create ( tr . doc , head ) ) ;
442+ return true ;
443+ }
0 commit comments