diff --git a/demo/32-merge-and-shade-table-cells.ts b/demo/32-merge-and-shade-table-cells.ts index db3174aeaf0..6fc7f867ec8 100644 --- a/demo/32-merge-and-shade-table-cells.ts +++ b/demo/32-merge-and-shade-table-cells.ts @@ -2,7 +2,7 @@ // Also includes an example on how to center tables // Import from 'docx' rather than '../build' if you install from npm import * as fs from "fs"; -import { AlignmentType, Document, HeadingLevel, Packer, Paragraph, ShadingType, Table, TableCell, TableRow, WidthType } from "../build"; +import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, ShadingType, Table, TableCell, TableRow, WidthType } from "../build"; const doc = new Document(); @@ -184,7 +184,7 @@ const table5 = new Table({ new TableRow({ children: [ new TableCell({ - children: [], + children: [new Paragraph("1,0")], }), new TableCell({ children: [new Paragraph("1,2")], @@ -195,10 +195,137 @@ const table5 = new Table({ new TableRow({ children: [ new TableCell({ - children: [], + children: [new Paragraph("2,0")], }), new TableCell({ - children: [], + children: [new Paragraph("2,1")], + }), + ], + }), + ], + width: { + size: 100, + type: WidthType.PERCENTAGE, + }, +}); + +const borders = { + top: { + style: BorderStyle.DASH_SMALL_GAP, + size: 1, + color: "red", + }, + bottom: { + style: BorderStyle.DASH_SMALL_GAP, + size: 1, + color: "red", + }, + left: { + style: BorderStyle.DASH_SMALL_GAP, + size: 1, + color: "red", + }, + right: { + style: BorderStyle.DASH_SMALL_GAP, + size: 1, + color: "red", + }, +}; + +const table6 = new Table({ + rows: [ + new TableRow({ + children: [ + new TableCell({ + borders, + children: [new Paragraph("0,0")], + rowSpan: 2, + }), + new TableCell({ + borders, + children: [new Paragraph("0,1")], + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + borders, + children: [new Paragraph("1,1")], + rowSpan: 2, + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + borders, + children: [new Paragraph("2,0")], + }), + ], + }), + ], + width: { + size: 100, + type: WidthType.PERCENTAGE, + }, +}); + +const table7 = new Table({ + rows: [ + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("0,0")], + }), + new TableCell({ + children: [new Paragraph("0,1")], + }), + new TableCell({ + children: [new Paragraph("0,2")], + rowSpan: 2, + }), + new TableCell({ + children: [new Paragraph("0,3")], + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("1,0")], + columnSpan: 2, + }), + new TableCell({ + children: [new Paragraph("1,3")], + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("2,0")], + columnSpan: 2, + }), + new TableCell({ + children: [new Paragraph("2,2")], + rowSpan: 2, + }), + new TableCell({ + children: [new Paragraph("2,3")], + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("3,0")], + }), + new TableCell({ + children: [new Paragraph("3,1")], + }), + new TableCell({ + children: [new Paragraph("3,3")], }), ], }), @@ -222,10 +349,14 @@ doc.addSection({ heading: HeadingLevel.HEADING_2, }), table3, - new Paragraph("Merging columns"), + new Paragraph("Merging columns 1"), table4, - new Paragraph("More Merging columns"), + new Paragraph("Merging columns 2"), table5, + new Paragraph("Merging columns 3"), + table6, + new Paragraph("Merging columns 4"), + table7, ], }); diff --git a/src/file/table/table-row/table-row.spec.ts b/src/file/table/table-row/table-row.spec.ts index e013153cd8f..03b8b2862e1 100644 --- a/src/file/table/table-row/table-row.spec.ts +++ b/src/file/table/table-row/table-row.spec.ts @@ -182,4 +182,96 @@ describe("TableRow", () => { }); }); }); + + describe("#rootIndexToColumnIndex", () => { + it("should get the correct virtual column index by root index", () => { + const tableRow = new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("test")], + columnSpan: 3, + }), + new TableCell({ + children: [new Paragraph("test")], + }), + new TableCell({ + children: [new Paragraph("test")], + }), + new TableCell({ + children: [new Paragraph("test")], + columnSpan: 3, + }), + ], + }); + + expect(tableRow.rootIndexToColumnIndex(1)).to.equal(0); + expect(tableRow.rootIndexToColumnIndex(2)).to.equal(3); + expect(tableRow.rootIndexToColumnIndex(3)).to.equal(4); + expect(tableRow.rootIndexToColumnIndex(4)).to.equal(5); + + expect(() => tableRow.rootIndexToColumnIndex(0)).to.throw(`cell 'rootIndex' should between 1 to 4`); + expect(() => tableRow.rootIndexToColumnIndex(5)).to.throw(`cell 'rootIndex' should between 1 to 4`); + }); + }); + + describe("#columnIndexToRootIndex", () => { + it("should get the correct root index by virtual column index", () => { + const tableRow = new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("test")], + columnSpan: 3, + }), + new TableCell({ + children: [new Paragraph("test")], + }), + new TableCell({ + children: [new Paragraph("test")], + }), + new TableCell({ + children: [new Paragraph("test")], + columnSpan: 3, + }), + ], + }); + + expect(tableRow.columnIndexToRootIndex(0)).to.equal(1); + expect(tableRow.columnIndexToRootIndex(1)).to.equal(1); + expect(tableRow.columnIndexToRootIndex(2)).to.equal(1); + + expect(tableRow.columnIndexToRootIndex(3)).to.equal(2); + expect(tableRow.columnIndexToRootIndex(4)).to.equal(3); + + expect(tableRow.columnIndexToRootIndex(5)).to.equal(4); + expect(tableRow.columnIndexToRootIndex(6)).to.equal(4); + expect(tableRow.columnIndexToRootIndex(7)).to.equal(4); + + expect(() => tableRow.columnIndexToRootIndex(-1)).to.throw(`cell 'columnIndex' should not less than zero`); + expect(() => tableRow.columnIndexToRootIndex(8)).to.throw(`cell 'columnIndex' should not great than 7`); + }); + + it("should allow end new cell index", () => { + const tableRow = new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("test")], + columnSpan: 3, + }), + new TableCell({ + children: [new Paragraph("test")], + }), + new TableCell({ + children: [new Paragraph("test")], + }), + new TableCell({ + children: [new Paragraph("test")], + columnSpan: 3, + }), + ], + }); + + expect(tableRow.columnIndexToRootIndex(8, true)).to.equal(5); + expect(() => tableRow.columnIndexToRootIndex(9, true)).to.throw(`cell 'columnIndex' should not great than 8`); + }); + }); }); diff --git a/src/file/table/table-row/table-row.ts b/src/file/table/table-row/table-row.ts index 466b7eb320f..320616e8093 100644 --- a/src/file/table/table-row/table-row.ts +++ b/src/file/table/table-row/table-row.ts @@ -46,8 +46,52 @@ export class TableRow extends XmlComponent { return this.options.children; } + public get cells(): TableCell[] { + return this.root.filter((xmlComponent) => xmlComponent instanceof TableCell); + } + public addCellToIndex(cell: TableCell, index: number): void { // Offset because properties is also in root. this.root.splice(index + 1, 0, cell); } + + public addCellToColumnIndex(cell: TableCell, columnIndex: number): void { + const rootIndex = this.columnIndexToRootIndex(columnIndex, true); + this.addCellToIndex(cell, rootIndex - 1); + } + + public rootIndexToColumnIndex(rootIndex: number): number { + // convert the root index to the virtual column index + if (rootIndex < 1 || rootIndex >= this.root.length) { + throw new Error(`cell 'rootIndex' should between 1 to ${this.root.length - 1}`); + } + let colIdx = 0; + // Offset because properties is also in root. + for (let rootIdx = 1; rootIdx < rootIndex; rootIdx++) { + const cell = this.root[rootIdx] as TableCell; + colIdx += cell.options.columnSpan || 1; + } + return colIdx; + } + + public columnIndexToRootIndex(columnIndex: number, allowEndNewCell: boolean = false): number { + // convert the virtual column index to the root index + // `allowEndNewCell` for get index to inert new cell + if (columnIndex < 0) { + throw new Error(`cell 'columnIndex' should not less than zero`); + } + let colIdx = 0; + // Offset because properties is also in root. + let rootIdx = 1; + const endRootIndex = allowEndNewCell ? this.root.length : this.root.length - 1; + while (colIdx <= columnIndex) { + if (rootIdx > endRootIndex) { + throw new Error(`cell 'columnIndex' should not great than ${colIdx - 1}`); + } + const cell = this.root[rootIdx] as TableCell; + rootIdx += 1; + colIdx += (cell && cell.options.columnSpan) || 1; + } + return rootIdx - 1; + } } diff --git a/src/file/table/table.spec.ts b/src/file/table/table.spec.ts index d2098fb1e4c..49eb7a3f463 100644 --- a/src/file/table/table.spec.ts +++ b/src/file/table/table.spec.ts @@ -188,6 +188,72 @@ describe("Table", () => { }); }); + it("creates a table with the correct columnSpan and rowSpan", () => { + const table = new Table({ + rows: [ + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("hello")], + columnSpan: 2, + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("hello")], + rowSpan: 2, + }), + new TableCell({ + children: [new Paragraph("hello")], + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("hello")], + }), + ], + }), + ], + }); + const tree = new Formatter().format(table); + const cellP = { "w:p": [{ "w:r": [{ "w:t": [{ _attr: { "xml:space": "preserve" } }, "hello"] }] }] }; + expect(tree).to.deep.equal({ + "w:tbl": [ + { "w:tblPr": [DEFAULT_TABLE_PROPERTIES, BORDERS, WIDTHS] }, + { + "w:tblGrid": [{ "w:gridCol": { _attr: { "w:w": 100 } } }, { "w:gridCol": { _attr: { "w:w": 100 } } }], + }, + { + "w:tr": [ + { + "w:tc": [{ "w:tcPr": [{ "w:gridSpan": { _attr: { "w:val": 2 } } }] }, cellP], + }, + ], + }, + { + "w:tr": [ + { + "w:tc": [{ "w:tcPr": [{ "w:vMerge": { _attr: { "w:val": "restart" } } }] }, cellP], + }, + { "w:tc": [cellP] }, + ], + }, + { + "w:tr": [ + { + "w:tc": [{ "w:tcPr": [{ "w:vMerge": { _attr: { "w:val": "continue" } } }] }, { "w:p": {} }], + }, + { "w:tc": [cellP] }, + ], + }, + ], + }); + }); + it("sets the table to fixed width layout", () => { const table = new Table({ rows: [ diff --git a/src/file/table/table.ts b/src/file/table/table.ts index a0e6dab88f4..f99b6fc9b1a 100644 --- a/src/file/table/table.ts +++ b/src/file/table/table.ts @@ -78,27 +78,28 @@ export class Table extends XmlComponent { this.root.push(row); } - for (const row of rows) { - row.Children.forEach((cell, cellIndex) => { - const column = rows.map((r) => r.Children[cellIndex]); + rows.forEach((row, rowIndex) => { + row.cells.forEach((cell, cellIndex) => { // Row Span has to be added in this method and not the constructor because it needs to know information about the column which happens after Table Cell construction // Row Span of 1 will crash word as it will add RESTART and not a corresponding CONTINUE if (cell.options.rowSpan && cell.options.rowSpan > 1) { - const thisCellsColumnIndex = column.indexOf(cell); - const endColumnIndex = thisCellsColumnIndex + (cell.options.rowSpan - 1); - - for (let i = thisCellsColumnIndex + 1; i <= endColumnIndex; i++) { - rows[i].addCellToIndex( + const columnIndex = row.rootIndexToColumnIndex(cellIndex + 1); + const startRowIndex = rowIndex + 1; + const endRowIndex = rowIndex + (cell.options.rowSpan - 1); + for (let i = startRowIndex; i <= endRowIndex; i++) { + rows[i].addCellToColumnIndex( new TableCell({ + columnSpan: cell.options.columnSpan, + borders: cell.options.borders, children: [], verticalMerge: VerticalMergeType.CONTINUE, }), - i, + columnIndex, ); } } }); - } + }); if (float) { this.properties.setTableFloatProperties(float);