Merge pull request #553 from wangfengming/master

Fix: `rowSpan` does not work correctly
This commit is contained in:
Dolan
2020-06-24 16:39:57 +01:00
committed by GitHub
5 changed files with 350 additions and 16 deletions

View File

@ -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,
],
});

View File

@ -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`);
});
});
});

View File

@ -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;
}
}

View File

@ -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: [

View File

@ -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);