Compare commits

...

14 Commits
5.2.0 ... 5.2.2

7 changed files with 398 additions and 27 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,10 @@ const table5 = new Table({
new TableRow({
children: [
new TableCell({
children: [],
children: [new Paragraph("2,0")],
}),
new TableCell({
children: [],
children: [new Paragraph("2,1")],
}),
],
}),
@ -209,6 +209,163 @@ 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({
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")],
rowSpan: 3,
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [new Paragraph("1,0")],
columnSpan: 2,
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [new Paragraph("2,0")],
columnSpan: 2,
}),
new TableCell({
children: [new Paragraph("2,2")],
rowSpan: 2,
}),
],
}),
new TableRow({
children: [
new TableCell({
children: [new Paragraph("3,0")],
}),
new TableCell({
children: [new Paragraph("3,1")],
}),
new TableCell({
children: [new Paragraph("3,3")],
}),
],
}),
],
width: {
size: 100,
type: WidthType.PERCENTAGE,
},
});
const table8 = new Table({
rows: [
new TableRow({
children: [
new TableCell({ children: [new Paragraph("1,1")] }),
new TableCell({ children: [new Paragraph("1,2")] }),
new TableCell({ children: [new Paragraph("1,3")] }),
new TableCell({ children: [new Paragraph("1,4")], rowSpan: 4, borders }),
],
}),
new TableRow({
children: [
new TableCell({ children: [new Paragraph("2,1")] }),
new TableCell({ children: [new Paragraph("2,2")] }),
new TableCell({ children: [new Paragraph("2,3")], rowSpan: 3 }),
],
}),
new TableRow({
children: [
new TableCell({ children: [new Paragraph("3,1")] }),
new TableCell({ children: [new Paragraph("3,2")], rowSpan: 2 }),
],
}),
new TableRow({
children: [
new TableCell({ children: [new Paragraph("4,1")] }),
],
}),
],
width: {
size: 100,
type: WidthType.PERCENTAGE,
},
});
doc.addSection({
children: [
table,
@ -222,10 +379,16 @@ 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,
new Paragraph("Merging columns 5"),
table8,
],
});

View File

@ -1,6 +1,6 @@
{
"name": "docx",
"version": "5.2.0",
"version": "5.2.2",
"description": "Easily generate .docx files with JS/TS with a nice declarative API. Works for Node and on the Browser.",
"main": "build/index.js",
"scripts": {

View File

@ -70,6 +70,9 @@ export class TableCell extends XmlComponent {
if (options.verticalMerge) {
this.properties.addVerticalMerge(options.verticalMerge);
} else if (options.rowSpan && options.rowSpan > 1) {
// if cell already have a `verticalMerge`, don't handle `rowSpan`
this.properties.addVerticalMerge(VerticalMergeType.RESTART);
}
if (options.margins) {
@ -84,10 +87,6 @@ export class TableCell extends XmlComponent {
this.properties.addGridSpan(options.columnSpan);
}
if (options.rowSpan && options.rowSpan > 1) {
this.properties.addVerticalMerge(VerticalMergeType.RESTART);
}
if (options.width) {
this.properties.setWidth(options.width.size, options.width.type);
}

View File

@ -182,4 +182,97 @@ 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);
// for column 10, just place the new cell at the end of row
expect(tableRow.columnIndexToRootIndex(10, true)).to.equal(5);
});
});
});

View File

@ -46,8 +46,56 @@ 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;
while (colIdx <= columnIndex) {
if (rootIdx >= this.root.length) {
if (allowEndNewCell) {
// for inserting verticalMerge CONTINUE cell at end of row
return this.root.length;
} else {
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,29 @@ 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) => {
if (rowIndex === rows.length - 1) {
// don't process the end row
return;
}
let columnIndex = 0;
row.cells.forEach((cell) => {
// 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(
new TableCell({
children: [],
verticalMerge: VerticalMergeType.CONTINUE,
}),
i,
);
}
const continueCell = new TableCell({
// the inserted CONTINUE cell has rowSpan, and will be handled when process the next row
rowSpan: cell.options.rowSpan - 1,
columnSpan: cell.options.columnSpan,
borders: cell.options.borders,
children: [],
verticalMerge: VerticalMergeType.CONTINUE,
});
rows[rowIndex + 1].addCellToColumnIndex(continueCell, columnIndex);
}
columnIndex += cell.options.columnSpan || 1;
});
}
});
if (float) {
this.properties.setTableFloatProperties(float);