voilab-pdf-table
Version:
PdfKit wrapper that helps to draw informations in simple tables.
908 lines (819 loc) • 29.1 kB
JavaScript
/*jslint node: true, unparam: true, nomen: true */
'use strict';
var lodash = require('lodash'),
EventEmitter = require('events').EventEmitter,
getPaddingValue = function (direction, p) {
var l = p && p.length;
if (direction === 'vertical' || direction === 'horizontal') {
if (l === 1) {
return p[0] * 2;
}
if (l === 2) {
return direction === 'vertical' ? p[0] * 2 : p[1] * 2;
}
if (l === 3) {
return direction === 'vertical' ? p[0] + p[2] : p[1] * 2;
}
if (l === 4) {
return direction === 'vertical' ? p[0] + p[2] : p[1] + p[3];
}
}
if (l === 1) {
return p[0];
}
if (l === 2) {
return direction === 'top' || direction === 'bottom' ? p[0] : p[1];
}
if (l === 3) {
return direction === 'top' ? p[0] : (direction === 'bottom' ? p[2] : p[1]);
}
if (l === 4) {
return direction === 'top' ? p[0] : (
direction === 'bottom' ? p[2] : (
direction === 'left' ? p[3] : p[1]
)
);
}
return 0;
},
addCellBackground = function (self, column, row, pos, index, isHeader) {
self.emitter.emit('cell-background-add', self, column, row, index, isHeader);
self.pdf
.rect(pos.x, pos.y, column.width, row._renderedContent.height)
.fill();
self.emitter.emit('cell-background-added', self, column, row, index, isHeader);
},
addCellBorder = function (self, column, row, pos, isHeader) {
self.emitter.emit('cell-border-add', self, column, row, isHeader);
var border = isHeader ? column.headerBorder : column.border,
bpos = {
x: pos.x + column.width,
y: pos.y + row._renderedContent.height
},
doStroke = function () {
var opacity = (!isHeader && column.borderOpacity) || (isHeader && column.headerBorderOpacity);
self.pdf.lineCap('square').opacity(opacity || 1).stroke().restore();
};
if (border.indexOf('L') !== -1) {
self.pdf.save().moveTo(pos.x, pos.y).lineTo(pos.x, bpos.y);
doStroke();
}
if (border.indexOf('T') !== -1) {
self.pdf.save().moveTo(pos.x, pos.y).lineTo(bpos.x, pos.y);
doStroke();
}
if (border.indexOf('B') !== -1) {
self.pdf.save().moveTo(pos.x, bpos.y).lineTo(bpos.x, bpos.y);
doStroke();
}
if (border.indexOf('R') !== -1) {
self.pdf.save().moveTo(bpos.x, pos.y).lineTo(bpos.x, bpos.y);
doStroke();
}
self.emitter.emit('cell-border-added', self, column, row, isHeader);
},
addCell = function (self, column, row, pos, isHeader) {
var width = column.width,
padding = {
left: 0,
top: 0
},
data = row._renderedContent.data[column.id] || row._renderedContent.data[column.id] === 0 ? row._renderedContent.data[column.id] : '',
renderer = isHeader ? column.headerRenderer : column.renderer,
cellAdded = isHeader ? column.headerCellAdded : column.cellAdded,
y = pos.y,
x = pos.x;
// Top and bottom padding (only if valign is not set)
if (!column.valign) {
if (isHeader) {
padding.top = getPaddingValue('top', column.headerPadding);
padding.bottom = getPaddingValue('bottom', column.headerPadding);
y += padding.top;
} else if (!isHeader) {
padding.top = getPaddingValue('top', column.padding);
padding.bottom = getPaddingValue('bottom', column.padding);
y += padding.top;
}
}
// Left and right padding
if (!isHeader) {
padding.left = getPaddingValue('left', column.padding);
padding.right = getPaddingValue('right', column.padding);
width -= getPaddingValue('horizontal', column.padding);
x += padding.left;
} else {
padding.left = getPaddingValue('left', column.headerPadding);
padding.right = getPaddingValue('right', column.headerPadding);
width -= getPaddingValue('horizontal', column.headerPadding);
x += padding.left;
}
// if specified, cache is not used and renderer is called one more time
if (renderer && column.cache === false) {
data = renderer(self, row, true, column, lodash.clone(pos), padding, isHeader);
}
// manage vertical alignement
if (column.valign === 'center') {
y += (row._renderedContent.height - row._renderedContent.contentHeight[column.id]) / 2;
} else if (column.valign === 'bottom') {
y += (row._renderedContent.height - row._renderedContent.contentHeight[column.id]);
}
self.pdf.text(data, x, y, lodash.assign({}, column, {
height: row._renderedContent.height,
width: width
}));
cellAdded && cellAdded(self, row, column, { x: x, y: self.pdf.y, baseY: y }, padding, isHeader);
pos.x += column.width;
},
addRow = function (self, row, index, isHeader) {
var pos = {
x: self.pos.x || self.pdf.page.margins.left,
y: self.pdf.y
},
ev = {
cancel: false
};
// the content might be higher than the remaining height on the page.
if (self.pdf.y + row._renderedContent.height > (self.pos.maxY || (self.pdf.page.height - self.pdf.page.margins.bottom) - self.bottomMargin)) {
self.emitter.emit('page-add', self, row, ev);
if (!ev.cancel) {
self.pdf.addPage();
// Reset Y position for next page
pos.y = self.pos.y || self.pdf.page.margins.top;
}
self.emitter.emit('page-added', self, row);
// Reset Y position if page-added drawn something
pos.y = self.pdf.y || self.pdf.page.margins.top;
}
lodash.forEach(self.getColumns(), function (column) {
if ((!isHeader && column.fill) || (isHeader && column.headerFill)) {
addCellBackground(self, column, row, pos, index, isHeader);
}
if ((!isHeader && column.border) || (isHeader && column.headerBorder)) {
addCellBorder(self, column, row, pos, isHeader);
}
addCell(self, column, row, pos, isHeader);
});
self.pdf.y = pos.y + row._renderedContent.height;
},
setRowHeight = function (self, row, isHeader) {
var max_height = 0;
row._renderedContent = {data: {}, dataHeight: {}, contentHeight: {}};
lodash.forEach(self.getColumns(), function (column) {
var renderer = isHeader ? column.headerRenderer : column.renderer,
content = renderer ? renderer(self, row, false, column) : row[column.id],
height = !content || column.ellipsis ? 1 : self.pdf.heightOfString(content, lodash.assign(lodash.clone(column), {
width: column.width - getPaddingValue('horizontal', column.padding)
})),
column_height = isHeader ? column.headerHeight : column.height;
// Setup the content height
row._renderedContent.contentHeight[column.id] = height;
// Continue with the row height
if (isHeader) {
height += getPaddingValue('vertical', column.headerPadding);
} else {
height += getPaddingValue('vertical', column.padding);
}
if (height < column_height) {
height = column_height;
}
// backup content so we don't need to call the renderer a second
// time when really rendering the column
row._renderedContent.data[column.id] = content;
row._renderedContent.dataHeight[column.id] = height;
// check max row height
max_height = height > max_height ? height : max_height;
});
row._renderedContent.height = max_height;
},
PdfTable = function (pdf, conf) {
lodash.merge(this, {
/**
* List of columns
* @var {Array}
*/
columns: [],
/**
* Defaults for all new columns
* @var {Object}
*/
columnsDefaults: {},
/**
* List of plugins (do not set it at construction time)
* @var {Array}
*/
plugins: [],
/**
* The number to put inside the pdfkit.moveDown() method
* @var {Number}
*/
minRowHeight: 1,
/**
* Height of the bottom margin, in point
* @var {Number}
*/
bottomMargin: 5,
/**
* Check if we want to show headers when {@link addBody()}
* @var {Boolean}
*/
showHeaders: true,
/**
* Pdf in which the table will be drawn
* @var {PdfDocument}
*/
pdf: pdf,
pos: {
x: pdf.x,
y: pdf.y,
},
/**
* Event emitter
* @var {EventEmitter}
*/
emitter: new EventEmitter()
}, lodash.cloneDeep(conf || {}));
};
lodash.assign(PdfTable.prototype, {
/**
* Add action before data rows are added
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Array</i> <b>data</b> complete set of data rows</li>
* </ul>
* @return {PdfTable}
*/
onBodyAdd: function (fn) {
this.emitter.on('body-add', fn);
return this;
},
/**
* Add action after data rows are added
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Array</i> <b>data</b> complete set of data rows</li>
* </ul>
* @return {PdfTable}
*/
onBodyAdded: function (fn) {
this.emitter.on('body-added', fn);
return this;
},
/**
* Add action before a row is added
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>row</b> the row to add</li>
* </ul>
* @return {PdfTable}
*/
onRowAdd: function (fn) {
this.emitter.on('row-add', fn);
return this;
},
/**
* Add action after a row is added
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>row</b> the added row</li>
* </ul>
* @return {PdfTable}
*/
onRowAdded: function (fn) {
this.emitter.on('row-added', fn);
return this;
},
/**
* Add action before a header is added
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>row</b> the header row to add</li>
* </ul>
* @return {PdfTable}
*/
onHeaderAdd: function (fn) {
this.emitter.on('header-add', fn);
return this;
},
/**
* Add action after a row is added
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>row</b> the added header row</li>
* </ul>
* @return {PdfTable}
*/
onHeaderAdded: function (fn) {
this.emitter.on('header-added', fn);
return this;
},
/**
* Add action before a page is added. You can use <em>ev.cancel = true</em>
* to cancel automatic page add, so you can do whatever you want to add
* a new page.
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>row</b> the current row</li>
* <li><i>Object</i> <b>ev</b> the event</li>
* </ul>
* @return {PdfTable}
*/
onPageAdd: function (fn) {
this.emitter.on('page-add', fn);
return this;
},
/**
* Add action after a page is added.
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>row</b> the current row</li>
* </ul>
* @return {PdfTable}
*/
onPageAdded: function (fn) {
this.emitter.on('page-added', fn);
return this;
},
/**
* Add action before height is calculated for every row
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Array</i> <b>data</b> complete set of data rows</li>
* </ul>
* @return {PdfTable}
*/
onRowHeightCalculate: function (fn) {
this.emitter.on('row-height-calculate', fn);
return this;
},
/**
* Add action after height is calculated for every row
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Array</i> <b>data</b> complete set of data rows</li>
* </ul>
* @return {PdfTable}
*/
onRowHeightCalculated: function (fn) {
this.emitter.on('row-height-calculated', fn);
return this;
},
/**
* Add action before height is calculated for the header
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Array</i> <b>data</b> complete set of data rows</li>
* </ul>
* @return {PdfTable}
*/
onHeaderHeightCalculate: function (fn) {
this.emitter.on('header-height-calculate', fn);
return this;
},
/**
* Add action after height is calculated for the header
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Array</i> <b>data</b> complete set of data rows</li>
* </ul>
* @return {PdfTable}
*/
onHeaderHeightCalculated: function (fn) {
this.emitter.on('header-height-calculated', fn);
return this;
},
/**
* Add action after a column is added
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>column</b> the added column</li>
* </ul>
* @return {PdfTable}
*/
onColumnAdded: function (fn) {
this.emitter.on('column-added', fn);
return this;
},
/**
* Add action before a cell background is added
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>column</b> the current column</li>
* <li><i>Object</i> <b>row</b> the current row</li>
* <li><i>Number</i> <b>index</b> the current row index</li>
* <li><i>Boolean</i> <b>isHeader</b> true if it's a header cell</li>
* </ul>
* @return {PdfTable}
*/
onCellBackgroundAdd: function (fn) {
this.emitter.on('cell-background-add', fn);
return this;
},
/**
* Add action after a cell background is added
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>column</b> the current column</li>
* <li><i>Object</i> <b>row</b> the current row</li>
* <li><i>Number</i> <b>index</b> the current row index</li>
* <li><i>Boolean</i> <b>isHeader</b> true if it's a header cell</li>
* </ul>
* @return {PdfTable}
*/
onCellBackgroundAdded: function (fn) {
this.emitter.on('cell-background-added', fn);
return this;
},
/**
* Add action before a cell border is added
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>column</b> the current column</li>
* <li><i>Object</i> <b>row</b> the current row</li>
* <li><i>Boolean</i> <b>isHeader</b> true if it's a header cell</li>
* </ul>
* @return {PdfTable}
*/
onCellBorderAdd: function (fn) {
this.emitter.on('cell-border-add', fn);
return this;
},
/**
* Add action after a cell border is added
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>column</b> the current column</li>
* <li><i>Object</i> <b>row</b> the current row</li>
* <li><i>Boolean</i> <b>isHeader</b> true if it's a header cell</li>
* </ul>
* @return {PdfTable}
*/
onCellBorderAdded: function (fn) {
this.emitter.on('cell-border-added', fn);
return this;
},
/**
* Add action after a column's property is changed
* <ul>
* <li><i>PdfTable</i> <b>table</b> PdfTable behind the event</li>
* <li><i>Object</i> <b>column</b> the column< that changed/li>
* <li><i>string</i> <b>prop</b> the property that changed</li>
* <li><i>mixed</i> <b>oldValue</b> the property value before change/li>
* </ul>
* @return {PdfTable}
*/
onColumnPropertyChanged: function (fn) {
this.emitter.on('column-property-changed', fn);
return this;
},
/**
* Add a plugin
*
* @param {Object} plugin the instanciated plugin
* @return {PdfTable}
*/
addPlugin: function (plugin) {
if (!plugin || !plugin.configure) {
var err = new Error('Plugin [' + (plugin && plugin.id) + '] must have a configure() method.');
err.module = 'pdftable';
err.code = 2;
throw err;
}
this.plugins.push(plugin);
plugin.configure(this);
return this;
},
/**
* Get a plugin
*
* @param {String} the plugin id
* @return {Object} the instanciated plugin
*/
getPlugin: function (id) {
return lodash.find(this.plugins, {id: id});
},
/**
* Remove a plugin and its events by the key
*
* @param {String} id the plugin id
* @return {PdfTable}
*/
removePlugin: function (id) {
lodash.remove(this.plugins, {id: id});
return this;
},
/**
* Determine if headers need to be displayed or not when .addBody
*
* @param {Boolean} show
* @return {PdfTable}
*/
setShowHeaders: function (show) {
this.showHeaders = !!show;
return this;
},
/**
* Define a column. Config array is mostly what we find in .text(), plus:
*
* <ul>
* <li><i>String</i> <b>id</b>: column id</li>
* <li><i>Function</i> <b>renderer</b>: renderer function for cell.
* Recieve (PdfTable table, row, draw).</li>
* <li><i>Function</i> <b>cellAdded</b>: renderer function for cell,
* after main data is drawn. Recieve (PdfTable table, row, draw).</li>
* <li><i>Boolean</i> <b>hidden</b>: True to define the column as
* hidden (default to false)</li>
* <li><i>String</i> <b>border</b>: cell border (LTBR)</li>
* <li><i>Number</i> <b>borderOpacity</b>: cell border opacity, from 0
* to 1</li>
* <li><i>Number</i> <b>width</b>: column width</li>
* <li><i>Number</i> <b>height</b>: min height for cell (default to
* standard linebreak)</li>
* <li><i>String</i> <b>align</b>: text horizontal align (left, center,
* right)</li>
* <li><i>String</i> <b>valign</b>: text vertical align (top, center,
* bottom)</li>
* <li><i>Boolean</i> <b>fill</b>: True to fill the cell with the
* predefined color (with pdf.fillColor(color))</li>
* <li><i>Boolean</i> <b>cache</b>: false to disable cache content. The
* renderer will be called twice (at height calculation time and when
* really rendering the content)</li>
* </ul>
*
* Specific to column header
* <ul>
* <li><i>String</i> <b>header</b>: column header text</li>
* <li><i>Function</i> <b>headerRenderer</b>: renderer function for
* header cell. Recieve (PdfTable table, row)</li>
* <li><i>Function</i> <b>haederCellAdded</b>: renderer function for
* cell, after main data is drawn. Recieve (PdfTable table, row,
* draw).</li>
* <li><i>String</i> <b>headerBorder</b>: cell border (LTBR)</li>
* <li><i>Number</i> <b>headerBorderOpacity</b>: cell border opacity,
* from 0 to 1</li>
* <li><i>Boolean</i> <b>headerFill</b>: True to fill the header with
* the predefined color (with pdf.fillColor(color))</li>
* <li><i>Number</i> <b>headerHeight</b>: min height for cell (default
* to standard linebreak)</li>
* </ul>
*
* Work in progress
* <ul>
* <li><i>Array</i> <b>padding</b>: padding for cell. Can be one number
* (same padding for LTBR), 2 numbers (same TB and LR) or 4 numbers</li>
* </ul>
*
* @param {Object} column
* @return {PdfTable}
*/
addColumn: function (column) {
this.columns.push(lodash.assign(lodash.clone(this.columnsDefaults || {}), column));
this.emitter.emit('column-added', this, column);
return this;
},
/**
* Set defaults for all new columns to add
*
* @see addColumn
* @param {Object} params
* @return {PdfTable}
*/
setColumnsDefaults: function (params) {
this.columnsDefaults = params;
return this;
},
/**
* Add many columns in one shot
*
* @see addColumn
* @param {Array} columns
* @return {PdfTable}
*/
addColumns: function (columns) {
return this.setColumns(columns, true);
},
/**
* Set columns in one shot
*
* @see addColumn
* @param {Array} columns
* @param {Boolean} [add] true to add these columns to existing columns
* @return {PdfTable}
*/
setColumns: function (columns, add) {
var self = this;
if (!add) {
this.columns = [];
}
lodash.forEach(columns, function (column) {
self.addColumn(column);
});
return this;
},
/**
* Get all table columns
*
* @param {Boolean} [withHidden] true to get hidden columns too
* @return {Array}
*/
getColumns: function (withHidden) {
return !withHidden
? lodash.filter(this.columns, function (column) {
return !column.hidden;
})
: this.columns;
},
/**
* Get a definition for a column
*
* @param {String} columnId
* @return {Object}
*/
getColumn: function (columnId) {
return lodash.find(this.columns, {id: columnId});
},
/**
* Get width between two columns. Widths of these columns are included in
* the sum.
*
* Example: table.getColumnWidthBetween('B', 'D');
* <pre>
* | A | B | C | D | E |
* | |-> | ->| ->| |
* </pre>
*
* If column A is empty, behave like {@link PdfTable.getColumnWidthUntil()}.
* If column B is empty, behave like {@link PdfTable.getColumnWidthFrom()}.
*
* @param {String} columnA start column
* @param {String} columnB last column
* @return {Number}
*/
getColumnWidthBetween: function (columnA, columnB) {
var width = 0,
check = false;
lodash.some(this.getColumns(), function (column) {
// begin sum either from start, or from column A
if (column.id === columnA || !columnA) {
check = true;
}
// stop sum if we want width from start to column B
if (!columnA && column.id === columnB) {
return true;
}
if (check) {
width += column.width;
}
if (column.id === columnB) {
return true;
}
});
return width;
},
/**
* Get width from start to the given column. Given width's column is not
* included in the sum.
*
* Example: table.getColumnWidthUntil('D');
* <pre>
* | A | B | C | D | E |
* |-> | ->| ->| | |
* </pre>
*
* @param {String} columnId column to stop sum
* @return {Number}
*/
getColumnWidthUntil: function (columnId) {
return this.getColumnWidthBetween(null, columnId);
},
/**
* Get width from a column to the end of the table. Given column's width is
* added to the sum.
*
* Example: table.getColumnWidthFrom('D');
* <pre>
* | A | B | C | D | E |
* | | | |-> | ->|
* </pre>
*
* @param {String} columnId the column from which we want to find the width
* @return {Number}
*/
getColumnWidthFrom: function (columnId) {
return this.getColumnWidthBetween(columnId, null);
},
/**
* Get table width (sum of all columns)
*
* @return {Number}
*/
getWidth: function () {
return this.getColumnWidthUntil(null, null);
},
/**
* Get column width
*
* @param {String} columnId
* @return {Number}
*/
getColumnWidth: function (columnId) {
return this.getColumnParam(columnId, 'width');
},
/**
* Set column width
*
* @param {String} columnId
* @param {Number} width
* @param {Boolean} [silent] True to prevent event to be emitted
* @return {PdfTable}
*/
setColumnWidth: function (columnId, width, silent) {
return this.setColumnParam(columnId, 'width', width, silent);
},
/**
* Get column param
*
* @param {String} columnId
* @param {String} param the desired param to fetch
* @return {mixed}
*/
getColumnParam: function (columnId, param) {
var column = this.getColumn(columnId);
return column && column[param];
},
/**
* Set a specific definition for a column
*
* @param {String} columnId column string index
* @param {String} key definition name (align, etc.)
* @param {mixed} value definition value
* @param {Boolean} [silent] True to prevent event to be emitted
* @return {PdfTable}
*/
setColumnParam: function (columnId, key, value, silent) {
var column = this.getColumn(columnId),
old_value;
if (column) {
old_value = column[key];
column[key] = value;
if (!silent) {
this.emitter.emit('column-property-changed', this, column, key, old_value);
}
}
return this;
},
/**
* Add content to the table
*
* @param {Array} data the complete set of data
* @return {PdfTable}
*/
addBody: function (data) {
var self = this,
index = 0;
this.emitter.emit('body-add', this, data);
if (!this.pdf.page) {
var err = new Error("No page available. Add a page to the PDF before calling addBody()");
err.module = 'pdftable';
err.code = 1;
throw err;
}
if (this.showHeaders) {
this.addHeader(index);
index++;
}
// calculate height for each row, depending on multiline contents
this.emitter.emit('row-height-calculate', this, data);
lodash.forEach(data, function (row) {
setRowHeight(self, row);
});
this.emitter.emit('row-height-calculated', this, data);
// really add rows, but now we know the exact height of each one
lodash.forEach(data, function (row, i) {
var rowIndex = i + index;
self.emitter.emit('row-add', self, row, rowIndex);
addRow(self, row, rowIndex);
self.emitter.emit('row-added', self, row, rowIndex);
});
this.emitter.emit('body-added', this, data);
return this;
},
/**
* Add table headers
*
* @return {PdfTable}
*/
addHeader: function (index) {
var row = lodash.reduce(this.getColumns(), function (acc, column) {
acc[column.id] = column.header;
return acc;
}, {});
this.emitter.emit('header-add', this, row);
this.emitter.emit('header-height-calculate', this, row);
setRowHeight(this, row, true);
this.emitter.emit('header-height-calculated', this, row);
addRow(this, row, index, true);
this.emitter.emit('header-added', this, row);
return this;
}
});
module.exports = PdfTable;