From 994df8531b171c48b9b6b645649026563dffefe0 Mon Sep 17 00:00:00 2001 From: wangfengming Date: Sat, 20 Jun 2020 19:47:46 +0800 Subject: [PATCH 1/6] :fix: `rowSpan` does not work correctly --- demo/32-merge-and-shade-table-cells.ts | 39 ++++++++++++++++++++++++++ src/file/table/table-row/table-row.ts | 4 +++ src/file/table/table.ts | 15 ++++------ 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/demo/32-merge-and-shade-table-cells.ts b/demo/32-merge-and-shade-table-cells.ts index db3174aeaf0..14667ffd788 100644 --- a/demo/32-merge-and-shade-table-cells.ts +++ b/demo/32-merge-and-shade-table-cells.ts @@ -209,6 +209,43 @@ const table5 = new Table({ }, }); +const table6 = new Table({ + rows: [ + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("11")], + }), + new TableCell({ + children: [new Paragraph("12")], + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("21"), new Paragraph("31")], + rowSpan: 2, + }), + new TableCell({ + children: [new Paragraph("22")], + }), + ], + }), + new TableRow({ + children: [ + new TableCell({ + children: [new Paragraph("32")], + }), + ], + }), + ], + width: { + size: 100, + type: WidthType.PERCENTAGE, + }, +}); + doc.addSection({ children: [ table, @@ -226,6 +263,8 @@ doc.addSection({ table4, new Paragraph("More Merging columns"), table5, + new Paragraph("Another Merging columns"), + table6, ], }); diff --git a/src/file/table/table-row/table-row.ts b/src/file/table/table-row/table-row.ts index 466b7eb320f..2a655a535dd 100644 --- a/src/file/table/table-row/table-row.ts +++ b/src/file/table/table-row/table-row.ts @@ -46,6 +46,10 @@ 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); diff --git a/src/file/table/table.ts b/src/file/table/table.ts index a0e6dab88f4..2cc408dc643 100644 --- a/src/file/table/table.ts +++ b/src/file/table/table.ts @@ -78,27 +78,24 @@ 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++) { + const endRowIndex = rowIndex + (cell.options.rowSpan - 1); + for (let i = rowIndex + 1; i <= endRowIndex; i++) { rows[i].addCellToIndex( new TableCell({ children: [], verticalMerge: VerticalMergeType.CONTINUE, }), - i, + cellIndex, ); } } }); - } + }); if (float) { this.properties.setTableFloatProperties(float); From 3977c8ab3bdea6d7b83b09723c71ad9c6ee47013 Mon Sep 17 00:00:00 2001 From: wangfengming Date: Sat, 20 Jun 2020 20:20:22 +0800 Subject: [PATCH 2/6] :fix: `rowSpan` continue cell should has the same border to the first row cell --- demo/32-merge-and-shade-table-cells.ts | 39 +++++++++++++++++++++----- src/file/table/table.ts | 1 + 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/demo/32-merge-and-shade-table-cells.ts b/demo/32-merge-and-shade-table-cells.ts index 14667ffd788..615f97c46bd 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(); @@ -209,14 +209,40 @@ const table5 = new Table({ }, }); +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({ - children: [new Paragraph("11")], + borders, + children: [new Paragraph("11"), new Paragraph("21")], + rowSpan: 2, }), new TableCell({ + borders, children: [new Paragraph("12")], }), ], @@ -224,18 +250,17 @@ const table6 = new Table({ new TableRow({ children: [ new TableCell({ - children: [new Paragraph("21"), new Paragraph("31")], + borders, + children: [new Paragraph("22"), new Paragraph("32")], rowSpan: 2, }), - new TableCell({ - children: [new Paragraph("22")], - }), ], }), new TableRow({ children: [ new TableCell({ - children: [new Paragraph("32")], + borders, + children: [new Paragraph("31")], }), ], }), diff --git a/src/file/table/table.ts b/src/file/table/table.ts index 2cc408dc643..2422f50161c 100644 --- a/src/file/table/table.ts +++ b/src/file/table/table.ts @@ -87,6 +87,7 @@ export class Table extends XmlComponent { for (let i = rowIndex + 1; i <= endRowIndex; i++) { rows[i].addCellToIndex( new TableCell({ + borders: cell.options.borders, children: [], verticalMerge: VerticalMergeType.CONTINUE, }), From fa7cb0bef1d998fab185f5a57c96d00b53ad589a Mon Sep 17 00:00:00 2001 From: wangfengming Date: Sat, 20 Jun 2020 21:01:23 +0800 Subject: [PATCH 3/6] :test: add test case for `columnSpan` and `rowSpan` --- src/file/table/table.spec.ts | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) 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: [ From 11e54b3e2cb5ea5721a8f1e4546e29edff35d2c1 Mon Sep 17 00:00:00 2001 From: wangfengming Date: Sat, 20 Jun 2020 21:36:35 +0800 Subject: [PATCH 4/6] :fix: handle cell that has both `columnSpan` and `rowSpan` --- src/file/table/table.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/file/table/table.ts b/src/file/table/table.ts index 2422f50161c..888f91dca1c 100644 --- a/src/file/table/table.ts +++ b/src/file/table/table.ts @@ -87,6 +87,7 @@ export class Table extends XmlComponent { for (let i = rowIndex + 1; i <= endRowIndex; i++) { rows[i].addCellToIndex( new TableCell({ + columnSpan: cell.options.columnSpan, borders: cell.options.borders, children: [], verticalMerge: VerticalMergeType.CONTINUE, From 8c9b61b37aa3d4acaa43d1e1a03741a88fa6bd4a Mon Sep 17 00:00:00 2001 From: wangfengming Date: Mon, 22 Jun 2020 12:25:51 +0800 Subject: [PATCH 5/6] :fix: handle `rowSpan` by convert between the virtual column index and the root index --- demo/32-merge-and-shade-table-cells.ts | 87 +++++++++++++++++++--- src/file/table/table-row/table-row.spec.ts | 87 ++++++++++++++++++++++ src/file/table/table-row/table-row.ts | 40 ++++++++++ src/file/table/table.ts | 8 +- 4 files changed, 209 insertions(+), 13 deletions(-) diff --git a/demo/32-merge-and-shade-table-cells.ts b/demo/32-merge-and-shade-table-cells.ts index 615f97c46bd..6fc7f867ec8 100644 --- a/demo/32-merge-and-shade-table-cells.ts +++ b/demo/32-merge-and-shade-table-cells.ts @@ -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,10 @@ const table5 = new Table({ new TableRow({ children: [ new TableCell({ - children: [], + children: [new Paragraph("2,0")], }), new TableCell({ - children: [], + children: [new Paragraph("2,1")], }), ], }), @@ -238,12 +238,12 @@ const table6 = new Table({ children: [ new TableCell({ borders, - children: [new Paragraph("11"), new Paragraph("21")], + children: [new Paragraph("0,0")], rowSpan: 2, }), new TableCell({ borders, - children: [new Paragraph("12")], + children: [new Paragraph("0,1")], }), ], }), @@ -251,7 +251,7 @@ const table6 = new Table({ children: [ new TableCell({ borders, - children: [new Paragraph("22"), new Paragraph("32")], + children: [new Paragraph("1,1")], rowSpan: 2, }), ], @@ -260,7 +260,72 @@ const table6 = new Table({ children: [ new TableCell({ borders, - children: [new Paragraph("31")], + 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")], }), ], }), @@ -284,12 +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("Another Merging columns"), + 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..0cdb0196b8a 100644 --- a/src/file/table/table-row/table-row.spec.ts +++ b/src/file/table/table-row/table-row.spec.ts @@ -182,4 +182,91 @@ 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); + }); + }); + + 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); + }); + + 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)).to.throw(`cell 'columnIndex' should not great than 7`); + 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 2a655a535dd..320616e8093 100644 --- a/src/file/table/table-row/table-row.ts +++ b/src/file/table/table-row/table-row.ts @@ -54,4 +54,44 @@ export class TableRow extends XmlComponent { // 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.ts b/src/file/table/table.ts index 888f91dca1c..f99b6fc9b1a 100644 --- a/src/file/table/table.ts +++ b/src/file/table/table.ts @@ -83,16 +83,18 @@ export class Table extends XmlComponent { // 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 columnIndex = row.rootIndexToColumnIndex(cellIndex + 1); + const startRowIndex = rowIndex + 1; const endRowIndex = rowIndex + (cell.options.rowSpan - 1); - for (let i = rowIndex + 1; i <= endRowIndex; i++) { - rows[i].addCellToIndex( + for (let i = startRowIndex; i <= endRowIndex; i++) { + rows[i].addCellToColumnIndex( new TableCell({ columnSpan: cell.options.columnSpan, borders: cell.options.borders, children: [], verticalMerge: VerticalMergeType.CONTINUE, }), - cellIndex, + columnIndex, ); } } From 057f41e35558bd687d8751d4439f69da3047c1ca Mon Sep 17 00:00:00 2001 From: wangfengming Date: Mon, 22 Jun 2020 12:34:08 +0800 Subject: [PATCH 6/6] :test: more test cases --- src/file/table/table-row/table-row.spec.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/file/table/table-row/table-row.spec.ts b/src/file/table/table-row/table-row.spec.ts index 0cdb0196b8a..03b8b2862e1 100644 --- a/src/file/table/table-row/table-row.spec.ts +++ b/src/file/table/table-row/table-row.spec.ts @@ -208,6 +208,9 @@ describe("TableRow", () => { 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`); }); }); @@ -242,6 +245,9 @@ describe("TableRow", () => { 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", () => { @@ -264,7 +270,6 @@ describe("TableRow", () => { ], }); - expect(() => tableRow.columnIndexToRootIndex(8)).to.throw(`cell 'columnIndex' should not great than 7`); expect(tableRow.columnIndexToRootIndex(8, true)).to.equal(5); expect(() => tableRow.columnIndexToRootIndex(9, true)).to.throw(`cell 'columnIndex' should not great than 8`); });