/** | |
* jsPDF AutoTable plugin | |
* Copyright (c) 2014 Simon Bengtsson, https://github.com/someatoms/jsPDF-AutoTable | |
* | |
* Licensed under the MIT License. | |
* http://opensource.org/licenses/mit-license | |
*/ | |
(function (API) { | |
'use strict'; | |
// On every new jsPDF object, clear variables | |
API.events.push(['initialized', function () { | |
doc = undefined; | |
cellPos = undefined; | |
pageCount = 1; | |
settings = undefined; | |
}], false); | |
var MIN_COLUMN_WIDTH = 25; | |
var doc, cellPos, pageCount = 1, settings; | |
// See README.md or examples for documentation of the options | |
// return a new instance every time to avoid references issues | |
var defaultOptions = function () { | |
return { | |
padding: 5, | |
fontSize: 10, | |
lineHeight: 20, | |
renderHeader: function (doc, pageNumber, settings) { | |
}, | |
renderFooter: function (doc, lastCellPos, pageNumber, settings) { | |
}, | |
renderHeaderCell: function (x, y, width, height, key, value, settings) { | |
doc.setFillColor(52, 73, 94); // Asphalt | |
doc.setTextColor(255, 255, 255); | |
doc.setFontStyle('bold'); | |
doc.rect(x, y, width, height, 'F'); | |
y += settings.lineHeight / 2 + doc.internal.getLineHeight() / 2; | |
doc.text(value, x + settings.padding, y); | |
}, | |
renderCell: function (x, y, width, height, key, value, row, settings) { | |
doc.setFillColor(row % 2 === 0 ? 245 : 255); | |
doc.setTextColor(50); | |
doc.rect(x, y, width, height, 'F'); | |
y += settings.lineHeight / 2 + doc.internal.getLineHeight() / 2 - 2.5; | |
doc.text(value, x + settings.padding, y); | |
}, | |
margins: {right: 40, left: 40, top: 50, bottom: 40}, | |
startY: false, | |
overflow: 'ellipsize', // false, ellipsize or linebreak (false passes the raw text to renderCell) | |
overflowColumns: false, // Specify which colums that gets subjected to the overflow method chosen. false indicates all | |
avoidPageSplit: false, | |
extendWidth: true | |
} | |
}; | |
/** | |
* Create a table from a set of rows and columns. | |
* | |
* @param {Object[]|String[]} columns Either as an array of objects or array of strings | |
* @param {Object[][]|String[][]} data Either as an array of objects or array of strings | |
* @param {Object} [options={}] Options that will override the default ones (above) | |
*/ | |
API.autoTable = function (columns, data, options) { | |
options = options || {}; | |
columns = columns || []; | |
doc = this; | |
var userFontSize = doc.internal.getFontSize(); | |
initData({columns: columns, data: data}); | |
initOptions(options); | |
cellPos = { | |
x: settings.margins.left, | |
y: settings.startY === false ? settings.margins.top : settings.startY | |
}; | |
var tableHeight = settings.margins.bottom + settings.margins.top + settings.lineHeight * (data.length + 1) + 5 + settings.startY; | |
if (settings.startY !== false && settings.avoidPageSplit && tableHeight > doc.internal.pageSize.height) { | |
pageCount++; | |
doc.addPage(); | |
cellPos.y = settings.margins.top; | |
} | |
settings.renderHeader(doc, pageCount, settings); | |
var columnWidths = calculateColumnWidths(data, columns); | |
printHeader(columns, columnWidths); | |
printRows(columns, data, columnWidths); | |
settings.renderFooter(doc, cellPos, pageCount, settings); | |
doc.setFontSize(userFontSize); | |
return this; | |
}; | |
/** | |
* Returns the Y position of the last drawn cell | |
* @returns int | |
*/ | |
API.autoTableEndPosY = function () { | |
// If cellPos is not set, autoTable() has probably not been called | |
return cellPos ? cellPos.y : false; | |
}; | |
/** | |
* @deprecated Use autoTableEndPosY() | |
*/ | |
API.autoTableEndPos = function () { | |
return cellPos; | |
}; | |
/** | |
* Parses an html table. To draw a table, use it like this: | |
* `doc.autoTable(false, doc.autoTableHtmlToJson(tableDomElem))` | |
* | |
* @param table Html table element | |
* @param indexBased Boolean flag if result should be returned as seperate cols and data | |
* @returns []|{} Array of objects with object keys as headers or based on indexes if indexBased is set to true | |
*/ | |
API.autoTableHtmlToJson = function (table, indexBased) { | |
var data = [], headers = {}, header = table.rows[0], i, tableRow, rowData, j; | |
if (indexBased) { | |
headers = []; | |
for (i = 0; i < header.cells.length; i++) { | |
headers.push(header.cells[i] ? header.cells[i].textContent : ''); | |
} | |
for (i = 1; i < table.rows.length; i++) { | |
tableRow = table.rows[i]; | |
rowData = []; | |
for (j = 0; j < header.cells.length; j++) { | |
rowData.push(tableRow.cells[j] ? tableRow.cells[j].textContent : ''); | |
} | |
data.push(rowData); | |
} | |
return {columns: headers, data: data}; | |
} else { | |
for (i = 0; i < header.cells.length; i++) { | |
headers[i] = header.cells[i] ? header.cells[i].textContent : ''; | |
} | |
for (i = 1; i < table.rows.length; i++) { | |
tableRow = table.rows[i]; | |
rowData = {}; | |
for (j = 0; j < header.cells.length; j++) { | |
rowData[headers[j]] = tableRow.cells[j] ? tableRow.cells[j].textContent : ''; | |
} | |
data.push(rowData); | |
} | |
return data; | |
} | |
}; | |
/** | |
* Transform all to the object initialization form | |
* @param params | |
*/ | |
function initData(params) { | |
// Object only initial | |
if (!params.columns || params.columns.length === 0) { | |
var keys = Object.keys(params.data[0]); | |
Array.prototype.push.apply(params.columns, keys); | |
params.columns.forEach(function (title, i) { | |
params.columns[i] = {title: title, key: keys[i]}; | |
}); | |
} | |
// Array initialization form | |
else if (typeof params.columns[0] === 'string') { | |
params.data.forEach(function (row, i) { | |
var obj = {}; | |
for (var j = 0; j < row.length; j++) { | |
obj[j] = params.data[i][j]; | |
} | |
params.data[i] = obj; | |
}); | |
params.columns.forEach(function (title, i) { | |
params.columns[i] = {title: title, key: i}; | |
}); | |
} else { | |
// Use options as is | |
} | |
} | |
function initOptions(raw) { | |
settings = defaultOptions(); | |
Object.keys(raw).forEach(function (key) { | |
settings[key] = raw[key]; | |
}); | |
doc.setFontSize(settings.fontSize); | |
// Backwards compatibility | |
if(settings.margins.horizontal !== undefined) { | |
settings.margins.left = settings.margins.horizontal; | |
settings.margins.right = settings.margins.horizontal; | |
} else { | |
settings.margins.horizontal = settings.margins.left; | |
} | |
} | |
function calculateColumnWidths(rows, columns) { | |
var widths = {}; | |
// Optimal widths | |
var optimalTableWidth = 0; | |
columns.forEach(function (header) { | |
var widest = getStringWidth(header.title || '', true); | |
if(typeof header.width == "number") { | |
widest = header.width; | |
} else { | |
rows.forEach(function (row) { | |
if (!header.hasOwnProperty('key')) | |
throw new Error("The key attribute is required in every header"); | |
var w = getStringWidth(stringify(row, header.key)); | |
if (w > widest) { | |
widest = w; | |
} | |
}); | |
} | |
widths[header.key] = widest; | |
optimalTableWidth += widest; | |
}); | |
var paddingAndMargin = settings.padding * 2 * columns.length + settings.margins.left + settings.margins.right; | |
var spaceDiff = doc.internal.pageSize.width - optimalTableWidth - paddingAndMargin; | |
var keys = Object.keys(widths); | |
if (spaceDiff < 0) { | |
// Shrink columns | |
var shrinkableColumns = []; | |
var shrinkableColumnWidths = 0; | |
if (settings.overflowColumns === false) { | |
keys.forEach(function (key) { | |
if (widths[key] > MIN_COLUMN_WIDTH) { | |
shrinkableColumns.push(key); | |
shrinkableColumnWidths += widths[key]; | |
} | |
}); | |
} else { | |
shrinkableColumns = settings.overflowColumns; | |
shrinkableColumns.forEach(function (col) { | |
shrinkableColumnWidths += widths[col]; | |
}); | |
} | |
shrinkableColumns.forEach(function (key) { | |
widths[key] += spaceDiff * (widths[key] / shrinkableColumnWidths); | |
}); | |
} else if (spaceDiff > 0 && settings.extendWidth) { | |
// Fill page horizontally | |
keys.forEach(function (key) { | |
widths[key] += spaceDiff / keys.length; | |
}); | |
} | |
return widths; | |
} | |
function printHeader(headers, columnWidths) { | |
if (!headers) return; | |
// First calculate the height of the row | |
// (to do that the maxium amount of rows first need to be found) | |
var maxRows = 1; | |
if (settings.overflow === 'linebreak') { | |
headers.forEach(function (header) { | |
if (isOverflowColumn(header)) { | |
var value = header.title || ''; | |
var arr = doc.splitTextToSize(value, columnWidths[header.key]); | |
if (arr.length > maxRows) { | |
maxRows = arr.length; | |
} | |
} | |
}); | |
} | |
var rowHeight = settings.lineHeight + (maxRows - 1) * doc.internal.getLineHeight() + 5; | |
headers.forEach(function (header) { | |
var width = columnWidths[header.key] + settings.padding * 2; | |
var value = header.title || ''; | |
if (settings.overflow === 'linebreak') { | |
if (isOverflowColumn(header)) { | |
value = doc.splitTextToSize(value, columnWidths[header.key]); | |
} | |
} else if (settings.overflow === 'ellipsize') { | |
value = ellipsize(columnWidths[header.key], value); | |
} | |
settings.renderHeaderCell(cellPos.x, cellPos.y, width, rowHeight, header.key, value, settings); | |
cellPos.x += width; | |
}); | |
doc.setTextColor(70, 70, 70); | |
doc.setFontStyle('normal'); | |
cellPos.y += rowHeight; | |
cellPos.x = settings.margins.left; | |
} | |
function printRows(headers, rows, columnWidths) { | |
for (var i = 0; i < rows.length; i++) { | |
var row = rows[i]; | |
// First calculate the height of the row | |
// (to do that the maxium amount of rows first need to be found) | |
var maxRows = 1; | |
if (settings.overflow === 'linebreak') { | |
headers.forEach(function (header) { | |
if (isOverflowColumn(header)) { | |
var value = stringify(row, header.key); | |
var arr = doc.splitTextToSize(value, columnWidths[header.key]); | |
if (arr.length > maxRows) { | |
maxRows = arr.length; | |
} | |
} | |
}); | |
} | |
var rowHeight = settings.lineHeight + (maxRows - 1) * doc.internal.getLineHeight(); | |
// Render the cell | |
headers.forEach(function (header) { | |
var value = stringify(row, header.key); | |
if (settings.overflow === 'linebreak') { | |
if (isOverflowColumn(header)) { | |
value = doc.splitTextToSize(value, columnWidths[header.key]); | |
} | |
} else if (settings.overflow === 'ellipsize') { | |
value = ellipsize(columnWidths[header.key], value); | |
} | |
var width = columnWidths[header.key] + settings.padding * 2; | |
settings.renderCell(cellPos.x, cellPos.y, width, rowHeight, header.key, value, i, settings); | |
cellPos.x = cellPos.x + columnWidths[header.key] + settings.padding * 2; | |
}); | |
// Add a new page if cellpos is at the end of page | |
var newPage = (cellPos.y + settings.margins.bottom + settings.lineHeight * 2) >= doc.internal.pageSize.height; | |
if (newPage) { | |
settings.renderFooter(doc, cellPos, pageCount, settings); | |
doc.addPage(); | |
cellPos = {x: settings.margins.left, y: settings.margins.top}; | |
pageCount++; | |
settings.renderHeader(doc, pageCount, settings); | |
printHeader(headers, columnWidths); | |
} else { | |
cellPos.y += rowHeight; | |
cellPos.x = settings.margins.left; | |
} | |
} | |
} | |
function isOverflowColumn(header) { | |
return settings.overflowColumns === false || settings.overflowColumns.indexOf(header.key) !== -1; | |
} | |
/** | |
* Ellipsize the text to fit in the width | |
* @param width | |
* @param text | |
*/ | |
function ellipsize(width, text) { | |
if (width >= getStringWidth(text)) { | |
return text; | |
} | |
while (width < getStringWidth(text + "...")) { | |
if (text.length < 2) { | |
break; | |
} | |
text = text.substring(0, text.length - 1); | |
} | |
text += "..."; | |
return text; | |
} | |
function stringify(row, key) { | |
return row.hasOwnProperty(key) ? '' + row[key] : ''; | |
} | |
function getStringWidth(txt, isBold) { | |
if(isBold) { | |
doc.setFontStyle('bold'); | |
} | |
var strWidth = doc.getStringUnitWidth(txt) * doc.internal.getFontSize(); | |
if(isBold) { | |
doc.setFontStyle('normal'); | |
} | |
return strWidth; | |
} | |
})(jsPDF.API); |