@@ -10,6 +10,7 @@ import {
1010 tablesInsertCellAdapter ,
1111 tablesSetBorderAdapter ,
1212 tablesSetShadingAdapter ,
13+ tablesSplitCellAdapter ,
1314 tablesSplitAdapter ,
1415} from './tables-adapter.js' ;
1516
@@ -38,6 +39,11 @@ type NodeOptions = {
3839 nodeSize ?: number ;
3940} ;
4041
42+ type TableEditorOptions = {
43+ firstRowAsHeaders ?: boolean ;
44+ firstRowBorders ?: Record < string , unknown > | null ;
45+ } ;
46+
4147function createNode ( typeName : string , children : ProseMirrorNode [ ] = [ ] , options : NodeOptions = { } ) : ProseMirrorNode {
4248 const attrs = options . attrs ?? { } ;
4349 const text = options . text ?? '' ;
@@ -122,7 +128,15 @@ function createNode(typeName: string, children: ProseMirrorNode[] = [], options:
122128 return node as unknown as ProseMirrorNode ;
123129}
124130
125- function makeTableEditor ( ) : Editor {
131+ function makeTableEditor ( options : TableEditorOptions = { } ) : Editor {
132+ const firstRowAsHeaders = options . firstRowAsHeaders ?? false ;
133+ const firstRowType = firstRowAsHeaders ? 'tableHeader' : 'tableCell' ;
134+ const firstRowAttrs =
135+ options . firstRowBorders === undefined
136+ ? { }
137+ : {
138+ borders : options . firstRowBorders ,
139+ } ;
126140 const paragraph1 = createNode ( 'paragraph' , [ createNode ( 'text' , [ ] , { text : 'Hello' } ) ] , {
127141 attrs : { sdBlockId : 'p1' , paraId : 'p1' , paragraphProperties : { } } ,
128142 isBlock : true ,
@@ -144,13 +158,13 @@ function makeTableEditor(): Editor {
144158 inlineContent : true ,
145159 } ) ;
146160
147- const cell1 = createNode ( 'tableCell' , [ paragraph1 ] , {
148- attrs : { sdBlockId : 'cell-1' , colspan : 1 , rowspan : 1 , colwidth : [ 100 ] } ,
161+ const cell1 = createNode ( firstRowType , [ paragraph1 ] , {
162+ attrs : { sdBlockId : 'cell-1' , colspan : 1 , rowspan : 1 , colwidth : [ 100 ] , ... firstRowAttrs } ,
149163 isBlock : true ,
150164 inlineContent : false ,
151165 } ) ;
152- const cell2 = createNode ( 'tableCell' , [ paragraph2 ] , {
153- attrs : { sdBlockId : 'cell-2' , colspan : 1 , rowspan : 1 , colwidth : [ 200 ] } ,
166+ const cell2 = createNode ( firstRowType , [ paragraph2 ] , {
167+ attrs : { sdBlockId : 'cell-2' , colspan : 1 , rowspan : 1 , colwidth : [ 200 ] , ... firstRowAttrs } ,
154168 isBlock : true ,
155169 inlineContent : false ,
156170 } ) ;
@@ -199,6 +213,8 @@ function makeTableEditor(): Editor {
199213 insert : vi . fn ( ) . mockReturnThis ( ) ,
200214 replaceWith : vi . fn ( ) . mockReturnThis ( ) ,
201215 setNodeMarkup : vi . fn ( ) . mockReturnThis ( ) ,
216+ setSelection : vi . fn ( ) . mockReturnThis ( ) ,
217+ setStoredMarks : vi . fn ( ) . mockReturnThis ( ) ,
202218 setMeta : vi . fn ( ) . mockReturnThis ( ) ,
203219 mapping : {
204220 maps : [ ] as unknown [ ] ,
@@ -213,6 +229,7 @@ function makeTableEditor(): Editor {
213229 doc,
214230 tr,
215231 schema : {
232+ text : ( text : string ) => createNode ( 'text' , [ ] , { text } ) ,
216233 nodes : {
217234 paragraph : {
218235 createAndFill : vi . fn ( ( attrs : Record < string , unknown > = { } , content ?: unknown ) => {
@@ -554,6 +571,75 @@ describe('tables-adapter regressions', () => {
554571 } ) ;
555572 } ) ;
556573
574+ it ( 'splits a cell by structural row/column expansion without deleting neighboring cells' , ( ) => {
575+ const editor = makeTableEditor ( ) ;
576+ const tr = editor . state . tr as unknown as {
577+ delete : ReturnType < typeof vi . fn > ;
578+ insert : ReturnType < typeof vi . fn > ;
579+ setNodeMarkup : ReturnType < typeof vi . fn > ;
580+ } ;
581+
582+ const result = tablesSplitCellAdapter ( editor , {
583+ nodeId : 'cell-1' ,
584+ rows : 2 ,
585+ columns : 2 ,
586+ } ) ;
587+
588+ expect ( result . success ) . toBe ( true ) ;
589+ expect ( tr . delete ) . not . toHaveBeenCalled ( ) ;
590+ expect ( tr . insert ) . toHaveBeenCalled ( ) ;
591+ expect ( getTableGridUpdateAttrs ( tr ) ) . toMatchObject ( {
592+ userEdited : true ,
593+ grid : [ { col : 1200 } , { col : 3000 } , { col : 3000 } ] ,
594+ } ) ;
595+ } ) ;
596+
597+ it ( 'does not copy header-only null borders when split inserts a body row from a header source row' , ( ) => {
598+ const editor = makeTableEditor ( { firstRowAsHeaders : true , firstRowBorders : null } ) ;
599+ const tr = editor . state . tr as unknown as { insert : ReturnType < typeof vi . fn > } ;
600+
601+ const result = tablesSplitCellAdapter ( editor , {
602+ nodeId : 'cell-1' ,
603+ rows : 2 ,
604+ columns : 1 ,
605+ } ) ;
606+
607+ expect ( result . success ) . toBe ( true ) ;
608+
609+ const insertedRow = tr . insert . mock . calls . find ( ( [ , node ] ) => node ?. type ?. name === 'tableRow' ) ?. [ 1 ] as
610+ | ProseMirrorNode
611+ | undefined ;
612+ expect ( insertedRow ) . toBeDefined ( ) ;
613+
614+ const insertedCells = ( ( insertedRow as unknown as { _children ?: ProseMirrorNode [ ] } ) . _children ?? [ ] ) . filter (
615+ ( node ) => node . type . name === 'tableCell' ,
616+ ) ;
617+ expect ( insertedCells . length ) . toBeGreaterThan ( 0 ) ;
618+ for ( const cell of insertedCells ) {
619+ expect ( ( cell . attrs as Record < string , unknown > ) . borders ) . toBeUndefined ( ) ;
620+ }
621+ } ) ;
622+
623+ it ( 'preserves non-target rows when split inserts columns by widening adjacent cells' , ( ) => {
624+ const editor = makeTableEditor ( ) ;
625+ const tr = editor . state . tr as unknown as { setNodeMarkup : ReturnType < typeof vi . fn > } ;
626+
627+ const result = tablesSplitCellAdapter ( editor , {
628+ nodeId : 'cell-1' ,
629+ rows : 1 ,
630+ columns : 2 ,
631+ } ) ;
632+
633+ expect ( result . success ) . toBe ( true ) ;
634+ expect ( tr . setNodeMarkup ) . toHaveBeenCalledWith (
635+ expect . any ( Number ) ,
636+ null ,
637+ expect . objectContaining ( {
638+ colspan : 2 ,
639+ } ) ,
640+ ) ;
641+ } ) ;
642+
557643 it ( 'rejects paragraph targets for tables.setBorder' , ( ) => {
558644 const editor = makeTableEditor ( ) ;
559645 const result = tablesSetBorderAdapter ( editor , {
0 commit comments