From ad3ee65de3557bafa80ba35083035d296731331a Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Fri, 6 Feb 2026 13:31:30 -0300 Subject: [PATCH 1/2] merge model --- .../lib/modelApi/editing/mergeModel.ts | 11 +- .../test/modelApi/editing/mergeModelTest.ts | 349 ++++++++++++++++++ 2 files changed, 357 insertions(+), 3 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts index ab02e56f741c..58ea69125f3b 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts @@ -198,7 +198,8 @@ function mergeTables( if (i == 0 && colIndex + j >= table.rows[0].cells.length) { for (let k = 0; k < table.rows.length; k++) { const leftCell = table.rows[k]?.cells[colIndex + j - 1]; - table.rows[k].cells[colIndex + j] = createTableCell( + const index = leftCell.spanLeft ? colIndex + j + 1 : colIndex + j; + table.rows[k].cells[index] = createTableCell( false /*spanLeft*/, false /*spanAbove*/, leftCell?.isHeader, @@ -218,7 +219,8 @@ function mergeTables( for (let k = 0; k < table.rows[rowIndex].cells.length; k++) { const aboveCell = table.rows[rowIndex + i - 1]?.cells[k]; - table.rows[rowIndex + i].cells[k] = createTableCell( + const index = aboveCell.spanAbove ? rowIndex + i + 1 : rowIndex + i; + table.rows[index].cells[k] = createTableCell( false /*spanLeft*/, false /*spanAbove*/, false /*isHeader*/, @@ -228,7 +230,10 @@ function mergeTables( } const oldCell = table.rows[rowIndex + i].cells[colIndex + j]; - table.rows[rowIndex + i].cells[colIndex + j] = newCell; + const cellIndex = oldCell.spanLeft ? colIndex + j + 1 : colIndex + j; + const index = oldCell.spanAbove ? rowIndex + i + 1 : rowIndex + i; + + table.rows[index].cells[cellIndex] = newCell; if (i == 0 && j == 0) { const newMarker = createSelectionMarker(marker.format); diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts index 6dfb579cac5d..faf76a229f38 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts @@ -6041,4 +6041,353 @@ describe('mergeModel', () => { }); // #endregion + + // #region Merge table with spanLeft and spanAbove + + it('table to table, merge table with spanLeft cell', () => { + const majorModel = createContentModelDocument(); + const sourceModel = createContentModelDocument(); + + const para1 = createParagraph(); + const text1 = createText('test1'); + const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); + const cell02 = createTableCell(true, false, false, { backgroundColor: '02' }); // spanLeft + const cell11 = createTableCell(false, false, false, { backgroundColor: '11' }); + const cell12 = createTableCell(true, false, false, { backgroundColor: '12' }); // spanLeft + const table1 = createTable(2); + + para1.segments.push(text1); + text1.isSelected = true; + cell12.blocks.push(para1); + table1.rows = [ + { format: {}, height: 0, cells: [cell01, cell02] }, + { format: {}, height: 0, cells: [cell11, cell12] }, + ]; + + majorModel.blocks.push(table1); + + const newPara1 = createParagraph(); + const newText1 = createText('newText1'); + const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); + const newCell12 = createTableCell(false, false, false, { backgroundColor: 'n12' }); + const newTable1 = createTable(1); + + newPara1.segments.push(newText1); + newCell11.blocks.push(newPara1); + newTable1.rows = [{ format: {}, height: 0, cells: [newCell11, newCell12] }]; + + sourceModel.blocks.push(newTable1); + + spyOn(applyTableFormat, 'applyTableFormat'); + spyOn(normalizeTable, 'normalizeTable'); + + const result = mergeModel( + majorModel, + sourceModel, + { newEntities: [], deletedEntities: [], newImages: [] }, + { + mergeTable: true, + } + ); + + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [marker], + format: {}, + isImplicit: true, + }; + const tableCell: ContentModelTableCell = { + blockGroupType: 'TableCell', + blocks: [paragraph], + format: { + backgroundColor: 'n11', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }; + + expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + marker, + paragraph, + path: [tableCell, majorModel], + tableContext: { + table: majorModel.blocks[0] as ContentModelTable, + rowIndex: 1, + colIndex: 1, + isWholeTableSelected: false, + }, + }); + }); + + it('table to table, merge table with spanAbove cell', () => { + const majorModel = createContentModelDocument(); + const sourceModel = createContentModelDocument(); + + const para1 = createParagraph(); + const text1 = createText('test1'); + const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); + const cell02 = createTableCell(false, false, false, { backgroundColor: '02' }); + const cell11 = createTableCell(false, true, false, { backgroundColor: '11' }); // spanAbove + const cell12 = createTableCell(false, true, false, { backgroundColor: '12' }); // spanAbove + const table1 = createTable(2); + + para1.segments.push(text1); + text1.isSelected = true; + cell12.blocks.push(para1); + table1.rows = [ + { format: {}, height: 0, cells: [cell01, cell02] }, + { format: {}, height: 0, cells: [cell11, cell12] }, + ]; + + majorModel.blocks.push(table1); + + const newPara1 = createParagraph(); + const newText1 = createText('newText1'); + const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); + const newCell21 = createTableCell(false, false, false, { backgroundColor: 'n21' }); + const newTable1 = createTable(2); + + newPara1.segments.push(newText1); + newCell11.blocks.push(newPara1); + newTable1.rows = [ + { format: {}, height: 0, cells: [newCell11] }, + { format: {}, height: 0, cells: [newCell21] }, + ]; + + sourceModel.blocks.push(newTable1); + + spyOn(applyTableFormat, 'applyTableFormat'); + spyOn(normalizeTable, 'normalizeTable'); + + const result = mergeModel( + majorModel, + sourceModel, + { newEntities: [], deletedEntities: [], newImages: [] }, + { + mergeTable: true, + } + ); + + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [marker], + format: {}, + isImplicit: true, + }; + const tableCell: ContentModelTableCell = { + blockGroupType: 'TableCell', + blocks: [paragraph], + format: { + backgroundColor: 'n11', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }; + + expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + marker, + paragraph, + path: [tableCell, majorModel], + tableContext: { + table: majorModel.blocks[0] as ContentModelTable, + rowIndex: 1, + colIndex: 1, + isWholeTableSelected: false, + }, + }); + }); + + it('table to table, merge table with spanLeft and spanAbove cells requiring expansion', () => { + const majorModel = createContentModelDocument(); + const sourceModel = createContentModelDocument(); + + const para1 = createParagraph(); + const text1 = createText('test1'); + const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); + const cell02 = createTableCell(true, false, false, { backgroundColor: '02' }); // spanLeft + const cell11 = createTableCell(false, false, false, { backgroundColor: '11' }); + const cell12 = createTableCell(true, false, false, { backgroundColor: '12' }); // spanLeft + const table1 = createTable(2); + + para1.segments.push(text1); + text1.isSelected = true; + cell12.blocks.push(para1); + table1.rows = [ + { format: {}, height: 0, cells: [cell01, cell02] }, + { format: {}, height: 0, cells: [cell11, cell12] }, + ]; + + majorModel.blocks.push(table1); + + const newPara1 = createParagraph(); + const newText1 = createText('newText1'); + const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); + const newCell12 = createTableCell(false, false, false, { backgroundColor: 'n12' }); + const newCell21 = createTableCell(false, false, false, { backgroundColor: 'n21' }); + const newCell22 = createTableCell(false, false, false, { backgroundColor: 'n22' }); + const newTable1 = createTable(2); + + newPara1.segments.push(newText1); + newCell11.blocks.push(newPara1); + newTable1.rows = [ + { format: {}, height: 0, cells: [newCell11, newCell12] }, + { format: {}, height: 0, cells: [newCell21, newCell22] }, + ]; + + sourceModel.blocks.push(newTable1); + + spyOn(applyTableFormat, 'applyTableFormat'); + spyOn(normalizeTable, 'normalizeTable'); + + const result = mergeModel( + majorModel, + sourceModel, + { newEntities: [], deletedEntities: [], newImages: [] }, + { + mergeTable: true, + } + ); + + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [marker], + format: {}, + isImplicit: true, + }; + const tableCell: ContentModelTableCell = { + blockGroupType: 'TableCell', + blocks: [paragraph], + format: { + backgroundColor: 'n11', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }; + + expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + marker, + paragraph, + path: [tableCell, majorModel], + tableContext: { + table: majorModel.blocks[0] as ContentModelTable, + rowIndex: 1, + colIndex: 1, + isWholeTableSelected: false, + }, + }); + }); + + it('table to table, merge table with spanAbove cells requiring row expansion', () => { + const majorModel = createContentModelDocument(); + const sourceModel = createContentModelDocument(); + + const para1 = createParagraph(); + const text1 = createText('test1'); + const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); + const cell02 = createTableCell(false, false, false, { backgroundColor: '02' }); + const cell11 = createTableCell(false, true, false, { backgroundColor: '11' }); // spanAbove + const cell12 = createTableCell(false, true, false, { backgroundColor: '12' }); // spanAbove + const table1 = createTable(2); + + para1.segments.push(text1); + text1.isSelected = true; + cell12.blocks.push(para1); + table1.rows = [ + { format: {}, height: 0, cells: [cell01, cell02] }, + { format: {}, height: 0, cells: [cell11, cell12] }, + ]; + + majorModel.blocks.push(table1); + + const newPara1 = createParagraph(); + const newText1 = createText('newText1'); + const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); + const newCell12 = createTableCell(false, false, false, { backgroundColor: 'n12' }); + const newCell21 = createTableCell(false, false, false, { backgroundColor: 'n21' }); + const newCell22 = createTableCell(false, false, false, { backgroundColor: 'n22' }); + const newTable1 = createTable(2); + + newPara1.segments.push(newText1); + newCell11.blocks.push(newPara1); + newTable1.rows = [ + { format: {}, height: 0, cells: [newCell11, newCell12] }, + { format: {}, height: 0, cells: [newCell21, newCell22] }, + ]; + + sourceModel.blocks.push(newTable1); + + spyOn(applyTableFormat, 'applyTableFormat'); + spyOn(normalizeTable, 'normalizeTable'); + + const result = mergeModel( + majorModel, + sourceModel, + { newEntities: [], deletedEntities: [], newImages: [] }, + { + mergeTable: true, + } + ); + + const marker: ContentModelSelectionMarker = { + segmentType: 'SelectionMarker', + isSelected: true, + format: {}, + }; + const paragraph: ContentModelParagraph = { + blockType: 'Paragraph', + segments: [marker], + format: {}, + isImplicit: true, + }; + const tableCell: ContentModelTableCell = { + blockGroupType: 'TableCell', + blocks: [paragraph], + format: { + backgroundColor: 'n11', + }, + spanLeft: false, + spanAbove: false, + isHeader: false, + dataset: {}, + }; + + expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); + expect(result).toEqual({ + marker, + paragraph, + path: [tableCell, majorModel], + tableContext: { + table: majorModel.blocks[0] as ContentModelTable, + rowIndex: 1, + colIndex: 1, + isWholeTableSelected: false, + }, + }); + }); + + // #endregion }); From 6bb06a39ab49783a347abd02cbc090cf672b154b Mon Sep 17 00:00:00 2001 From: "Julia Roldi (from Dev Box)" Date: Fri, 6 Feb 2026 15:25:11 -0300 Subject: [PATCH 2/2] merge model with spans --- .../lib/modelApi/editing/mergeModel.ts | 148 +++++++++---- .../test/modelApi/editing/mergeModelTest.ts | 203 +++++------------- 2 files changed, 168 insertions(+), 183 deletions(-) diff --git a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts index 58ea69125f3b..33cba790a24c 100644 --- a/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts +++ b/packages/roosterjs-content-model-dom/lib/modelApi/editing/mergeModel.ts @@ -27,6 +27,7 @@ import type { ReadonlyContentModelBlock, ReadonlyContentModelBlockGroup, ReadonlyContentModelDocument, + ReadonlyContentModelTable, ShallowMutableContentModelParagraph, } from 'roosterjs-content-model-types'; @@ -191,49 +192,64 @@ function mergeTables( const { table: readonlyTable, colIndex, rowIndex } = tableContext; const table = mutateBlock(readonlyTable); - for (let i = 0; i < newTable.rows.length; i++) { - for (let j = 0; j < newTable.rows[i].cells.length; j++) { - const newCell = newTable.rows[i].cells[j]; + const newTableColCount = newTable.rows[0]?.cells.length || 0; + const newTableRowCount = newTable.rows.length; + + const lastTargetColIndex = getTargetColIndex(table, rowIndex, colIndex, newTableColCount); + const extraColsNeeded = lastTargetColIndex - table.rows[0].cells.length; + + if (extraColsNeeded > 0) { + const currentColCount = table.rows[0].cells.length; + for (let col = 0; col < extraColsNeeded; col++) { + const newColIndex = currentColCount + col; + for (let k = 0; k < table.rows.length; k++) { + const leftCell = table.rows[k]?.cells[newColIndex - 1]; + table.rows[k].cells[newColIndex] = createTableCell( + false /*spanLeft*/, + false /*spanAbove*/, + leftCell?.isHeader, + leftCell?.format + ); + } + } + } - if (i == 0 && colIndex + j >= table.rows[0].cells.length) { - for (let k = 0; k < table.rows.length; k++) { - const leftCell = table.rows[k]?.cells[colIndex + j - 1]; - const index = leftCell.spanLeft ? colIndex + j + 1 : colIndex + j; - table.rows[k].cells[index] = createTableCell( - false /*spanLeft*/, - false /*spanAbove*/, - leftCell?.isHeader, - leftCell?.format - ); - } + const lastTargetRowIndex = getTargetRowIndex(table, rowIndex, newTableRowCount, colIndex); + const extraRowsNeeded = lastTargetRowIndex - table.rows.length; + + if (extraRowsNeeded > 0) { + const currentRowCount = table.rows.length; + const colCount = table.rows[0]?.cells.length || 0; + for (let row = 0; row < extraRowsNeeded; row++) { + const newRowIndex = currentRowCount + row; + table.rows[newRowIndex] = { + cells: [], + format: {}, + height: 0, + }; + for (let k = 0; k < colCount; k++) { + const aboveCell = table.rows[newRowIndex - 1]?.cells[k]; + table.rows[newRowIndex].cells[k] = createTableCell( + false /*spanLeft*/, + false /*spanAbove*/, + false /*isHeader*/, + aboveCell?.format + ); } + } + } - if (j == 0 && rowIndex + i >= table.rows.length) { - if (!table.rows[rowIndex + i]) { - table.rows[rowIndex + i] = { - cells: [], - format: {}, - height: 0, - }; - } + for (let i = 0; i < newTable.rows.length; i++) { + const targetRowIndex = getTargetRowIndex(table, rowIndex, i, colIndex); - for (let k = 0; k < table.rows[rowIndex].cells.length; k++) { - const aboveCell = table.rows[rowIndex + i - 1]?.cells[k]; - const index = aboveCell.spanAbove ? rowIndex + i + 1 : rowIndex + i; - table.rows[index].cells[k] = createTableCell( - false /*spanLeft*/, - false /*spanAbove*/, - false /*isHeader*/, - aboveCell?.format - ); - } - } + for (let j = 0; j < newTable.rows[i].cells.length; j++) { + const newCell = newTable.rows[i].cells[j]; - const oldCell = table.rows[rowIndex + i].cells[colIndex + j]; - const cellIndex = oldCell.spanLeft ? colIndex + j + 1 : colIndex + j; - const index = oldCell.spanAbove ? rowIndex + i + 1 : rowIndex + i; + const targetColIndex = getTargetColIndex(table, targetRowIndex, colIndex, j); - table.rows[index].cells[cellIndex] = newCell; + const oldCell = table.rows[targetRowIndex]?.cells[targetColIndex]; + + table.rows[targetRowIndex].cells[targetColIndex] = newCell; if (i == 0 && j == 0) { const newMarker = createSelectionMarker(marker.format); @@ -499,6 +515,7 @@ function getFormatWithoutSegmentFormat( KeysOfSegmentFormat.forEach(key => delete resultFormat[key]); return resultFormat; } + function getHyperlinkTextColor(sourceFormat: ContentModelHyperLinkFormat) { const result: ContentModelHyperLinkFormat = {}; if (sourceFormat.textColor) { @@ -507,3 +524,60 @@ function getHyperlinkTextColor(sourceFormat: ContentModelHyperLinkFormat) { return result; } + +function getTargetColIndex( + table: ReadonlyContentModelTable, + rowIndex: number, + startColIndex: number, + offset: number +): number { + const row = table.rows[rowIndex]; + if (!row) { + return startColIndex + offset; + } + + if (offset === 0) { + return startColIndex; + } + + let targetColIndex = startColIndex; + let logicalCellsToSkip = offset; + + while (logicalCellsToSkip > 0) { + targetColIndex++; + + if (targetColIndex >= row.cells.length) { + logicalCellsToSkip--; + } else if (!row.cells[targetColIndex].spanLeft) { + logicalCellsToSkip--; + } + } + + return targetColIndex; +} + +function getTargetRowIndex( + table: ReadonlyContentModelTable, + startRowIndex: number, + offset: number, + colIndex: number +): number { + if (offset === 0) { + return startRowIndex; + } + + let targetRowIndex = startRowIndex; + let logicalRowsToSkip = offset; + + while (logicalRowsToSkip > 0) { + targetRowIndex++; + + if (targetRowIndex >= table.rows.length) { + logicalRowsToSkip--; + } else if (!table.rows[targetRowIndex]?.cells[colIndex]?.spanAbove) { + logicalRowsToSkip--; + } + } + + return targetRowIndex; +} diff --git a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts index faf76a229f38..ed1af5dc629c 100644 --- a/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts +++ b/packages/roosterjs-content-model-dom/test/modelApi/editing/mergeModelTest.ts @@ -6044,28 +6044,33 @@ describe('mergeModel', () => { // #region Merge table with spanLeft and spanAbove - it('table to table, merge table with spanLeft cell', () => { + it('table to table, merge table with spanLeft cell - should skip span cells', () => { const majorModel = createContentModelDocument(); const sourceModel = createContentModelDocument(); + // Create a 2x3 table where columns 1-2 are merged (spanLeft) + // Selection is in cell12 (which is spanLeft, part of merged cell01-02) const para1 = createParagraph(); const text1 = createText('test1'); const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); const cell02 = createTableCell(true, false, false, { backgroundColor: '02' }); // spanLeft + const cell03 = createTableCell(false, false, false, { backgroundColor: '03' }); const cell11 = createTableCell(false, false, false, { backgroundColor: '11' }); const cell12 = createTableCell(true, false, false, { backgroundColor: '12' }); // spanLeft + const cell13 = createTableCell(false, false, false, { backgroundColor: '13' }); const table1 = createTable(2); para1.segments.push(text1); text1.isSelected = true; cell12.blocks.push(para1); table1.rows = [ - { format: {}, height: 0, cells: [cell01, cell02] }, - { format: {}, height: 0, cells: [cell11, cell12] }, + { format: {}, height: 0, cells: [cell01, cell02, cell03] }, + { format: {}, height: 0, cells: [cell11, cell12, cell13] }, ]; majorModel.blocks.push(table1); + // Source table has 2 cells - they should be placed at positions 1 and 2 (skipping span) const newPara1 = createParagraph(); const newText1 = createText('newText1'); const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); @@ -6081,7 +6086,7 @@ describe('mergeModel', () => { spyOn(applyTableFormat, 'applyTableFormat'); spyOn(normalizeTable, 'normalizeTable'); - const result = mergeModel( + mergeModel( majorModel, sourceModel, { newEntities: [], deletedEntities: [], newImages: [] }, @@ -6090,54 +6095,28 @@ describe('mergeModel', () => { } ); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }; - const paragraph: ContentModelParagraph = { - blockType: 'Paragraph', - segments: [marker], - format: {}, - isImplicit: true, - }; - const tableCell: ContentModelTableCell = { - blockGroupType: 'TableCell', - blocks: [paragraph], - format: { - backgroundColor: 'n11', - }, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - }; + const table = majorModel.blocks[0] as ContentModelTable; + // The first new cell should replace cell12 (at index 1), second should go to index 2 + expect(table.rows[1].cells[1]).toBe(newCell11); + expect(table.rows[1].cells[2]).toBe(newCell12); expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - marker, - paragraph, - path: [tableCell, majorModel], - tableContext: { - table: majorModel.blocks[0] as ContentModelTable, - rowIndex: 1, - colIndex: 1, - isWholeTableSelected: false, - }, - }); }); - it('table to table, merge table with spanAbove cell', () => { + it('table to table, merge table with spanAbove cell - should skip span cells', () => { const majorModel = createContentModelDocument(); const sourceModel = createContentModelDocument(); + // Create a 3x2 table where rows 0-1 are merged (spanAbove) at column 1 const para1 = createParagraph(); const text1 = createText('test1'); const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); const cell02 = createTableCell(false, false, false, { backgroundColor: '02' }); const cell11 = createTableCell(false, true, false, { backgroundColor: '11' }); // spanAbove const cell12 = createTableCell(false, true, false, { backgroundColor: '12' }); // spanAbove - const table1 = createTable(2); + const cell21 = createTableCell(false, false, false, { backgroundColor: '21' }); + const cell22 = createTableCell(false, false, false, { backgroundColor: '22' }); + const table1 = createTable(3); para1.segments.push(text1); text1.isSelected = true; @@ -6145,10 +6124,12 @@ describe('mergeModel', () => { table1.rows = [ { format: {}, height: 0, cells: [cell01, cell02] }, { format: {}, height: 0, cells: [cell11, cell12] }, + { format: {}, height: 0, cells: [cell21, cell22] }, ]; majorModel.blocks.push(table1); + // Source table has 2 rows - they should be placed at row 1 and 2 (skipping spanAbove) const newPara1 = createParagraph(); const newText1 = createText('newText1'); const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); @@ -6167,7 +6148,7 @@ describe('mergeModel', () => { spyOn(applyTableFormat, 'applyTableFormat'); spyOn(normalizeTable, 'normalizeTable'); - const result = mergeModel( + mergeModel( majorModel, sourceModel, { newEntities: [], deletedEntities: [], newImages: [] }, @@ -6176,65 +6157,40 @@ describe('mergeModel', () => { } ); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }; - const paragraph: ContentModelParagraph = { - blockType: 'Paragraph', - segments: [marker], - format: {}, - isImplicit: true, - }; - const tableCell: ContentModelTableCell = { - blockGroupType: 'TableCell', - blocks: [paragraph], - format: { - backgroundColor: 'n11', - }, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - }; + const table = majorModel.blocks[0] as ContentModelTable; + // First new cell should replace cell12 at row 1, second should go to row 2 + expect(table.rows[1].cells[1]).toBe(newCell11); + expect(table.rows[2].cells[1]).toBe(newCell21); expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - marker, - paragraph, - path: [tableCell, majorModel], - tableContext: { - table: majorModel.blocks[0] as ContentModelTable, - rowIndex: 1, - colIndex: 1, - isWholeTableSelected: false, - }, - }); }); - it('table to table, merge table with spanLeft and spanAbove cells requiring expansion', () => { + it('table to table, merge 2x2 table into cell with spanLeft - should expand and skip spans', () => { const majorModel = createContentModelDocument(); const sourceModel = createContentModelDocument(); + // Create a 2x3 table where columns 1-2 are merged (spanLeft) const para1 = createParagraph(); const text1 = createText('test1'); const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); const cell02 = createTableCell(true, false, false, { backgroundColor: '02' }); // spanLeft + const cell03 = createTableCell(false, false, false, { backgroundColor: '03' }); const cell11 = createTableCell(false, false, false, { backgroundColor: '11' }); const cell12 = createTableCell(true, false, false, { backgroundColor: '12' }); // spanLeft + const cell13 = createTableCell(false, false, false, { backgroundColor: '13' }); const table1 = createTable(2); para1.segments.push(text1); text1.isSelected = true; cell12.blocks.push(para1); table1.rows = [ - { format: {}, height: 0, cells: [cell01, cell02] }, - { format: {}, height: 0, cells: [cell11, cell12] }, + { format: {}, height: 0, cells: [cell01, cell02, cell03] }, + { format: {}, height: 0, cells: [cell11, cell12, cell13] }, ]; majorModel.blocks.push(table1); + // Source table is 2x2 const newPara1 = createParagraph(); const newText1 = createText('newText1'); const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); @@ -6255,7 +6211,7 @@ describe('mergeModel', () => { spyOn(applyTableFormat, 'applyTableFormat'); spyOn(normalizeTable, 'normalizeTable'); - const result = mergeModel( + mergeModel( majorModel, sourceModel, { newEntities: [], deletedEntities: [], newImages: [] }, @@ -6264,54 +6220,32 @@ describe('mergeModel', () => { } ); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }; - const paragraph: ContentModelParagraph = { - blockType: 'Paragraph', - segments: [marker], - format: {}, - isImplicit: true, - }; - const tableCell: ContentModelTableCell = { - blockGroupType: 'TableCell', - blocks: [paragraph], - format: { - backgroundColor: 'n11', - }, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - }; + const table = majorModel.blocks[0] as ContentModelTable; + // New cells should be placed correctly, skipping the spanLeft cell + expect(table.rows[1].cells[1]).toBe(newCell11); + expect(table.rows[1].cells[2]).toBe(newCell12); + // Second row of pasted table should go to row 2 + expect(table.rows.length).toBe(3); + expect(table.rows[2].cells[1]).toBe(newCell21); + expect(table.rows[2].cells[2]).toBe(newCell22); expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - marker, - paragraph, - path: [tableCell, majorModel], - tableContext: { - table: majorModel.blocks[0] as ContentModelTable, - rowIndex: 1, - colIndex: 1, - isWholeTableSelected: false, - }, - }); }); - it('table to table, merge table with spanAbove cells requiring row expansion', () => { + it('table to table, merge 2x2 table into cell with spanAbove - should expand and skip spans', () => { const majorModel = createContentModelDocument(); const sourceModel = createContentModelDocument(); + // Create a 3x2 table where rows 0-1 are merged (spanAbove) at column 1 const para1 = createParagraph(); const text1 = createText('test1'); const cell01 = createTableCell(false, false, false, { backgroundColor: '01' }); const cell02 = createTableCell(false, false, false, { backgroundColor: '02' }); const cell11 = createTableCell(false, true, false, { backgroundColor: '11' }); // spanAbove const cell12 = createTableCell(false, true, false, { backgroundColor: '12' }); // spanAbove - const table1 = createTable(2); + const cell21 = createTableCell(false, false, false, { backgroundColor: '21' }); + const cell22 = createTableCell(false, false, false, { backgroundColor: '22' }); + const table1 = createTable(3); para1.segments.push(text1); text1.isSelected = true; @@ -6319,10 +6253,12 @@ describe('mergeModel', () => { table1.rows = [ { format: {}, height: 0, cells: [cell01, cell02] }, { format: {}, height: 0, cells: [cell11, cell12] }, + { format: {}, height: 0, cells: [cell21, cell22] }, ]; majorModel.blocks.push(table1); + // Source table is 2x2 const newPara1 = createParagraph(); const newText1 = createText('newText1'); const newCell11 = createTableCell(false, false, false, { backgroundColor: 'n11' }); @@ -6343,7 +6279,7 @@ describe('mergeModel', () => { spyOn(applyTableFormat, 'applyTableFormat'); spyOn(normalizeTable, 'normalizeTable'); - const result = mergeModel( + mergeModel( majorModel, sourceModel, { newEntities: [], deletedEntities: [], newImages: [] }, @@ -6352,41 +6288,16 @@ describe('mergeModel', () => { } ); - const marker: ContentModelSelectionMarker = { - segmentType: 'SelectionMarker', - isSelected: true, - format: {}, - }; - const paragraph: ContentModelParagraph = { - blockType: 'Paragraph', - segments: [marker], - format: {}, - isImplicit: true, - }; - const tableCell: ContentModelTableCell = { - blockGroupType: 'TableCell', - blocks: [paragraph], - format: { - backgroundColor: 'n11', - }, - spanLeft: false, - spanAbove: false, - isHeader: false, - dataset: {}, - }; + const table = majorModel.blocks[0] as ContentModelTable; + // New cells should be placed correctly, skipping the spanAbove cell + // Table should have expanded to 3 columns + expect(table.rows[0].cells.length).toBe(3); + expect(table.rows[1].cells[1]).toBe(newCell11); + expect(table.rows[1].cells[2]).toBe(newCell12); + expect(table.rows[2].cells[1]).toBe(newCell21); + expect(table.rows[2].cells[2]).toBe(newCell22); expect(normalizeTable.normalizeTable).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - marker, - paragraph, - path: [tableCell, majorModel], - tableContext: { - table: majorModel.blocks[0] as ContentModelTable, - rowIndex: 1, - colIndex: 1, - isWholeTableSelected: false, - }, - }); }); // #endregion