jspdf.plugin.autotable.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. /**
  2. * jsPDF AutoTable plugin
  3. * Copyright (c) 2014 Simon Bengtsson, https://github.com/someatoms/jsPDF-AutoTable
  4. *
  5. * Licensed under the MIT License.
  6. * http://opensource.org/licenses/mit-license
  7. */
  8. (function (API) {
  9. 'use strict';
  10. // On every new jsPDF object, clear variables
  11. API.events.push(['initialized', function () {
  12. doc = undefined;
  13. cellPos = undefined;
  14. pageCount = 1;
  15. settings = undefined;
  16. }], false);
  17. var MIN_COLUMN_WIDTH = 25;
  18. var doc, cellPos, pageCount = 1, settings;
  19. // See README.md or examples for documentation of the options
  20. // return a new instance every time to avoid references issues
  21. var defaultOptions = function () {
  22. return {
  23. padding: 5,
  24. fontSize: 10,
  25. lineHeight: 20,
  26. renderHeader: function (doc, pageNumber, settings) {
  27. },
  28. renderFooter: function (doc, lastCellPos, pageNumber, settings) {
  29. },
  30. renderHeaderCell: function (x, y, width, height, key, value, settings) {
  31. doc.setFillColor(52, 73, 94); // Asphalt
  32. doc.setTextColor(255, 255, 255);
  33. doc.setFontStyle('bold');
  34. doc.rect(x, y, width, height, 'F');
  35. y += settings.lineHeight / 2 + doc.internal.getLineHeight() / 2;
  36. doc.text(value, x + settings.padding, y);
  37. },
  38. renderCell: function (x, y, width, height, key, value, row, settings) {
  39. doc.setFillColor(row % 2 === 0 ? 245 : 255);
  40. doc.setTextColor(50);
  41. doc.rect(x, y, width, height, 'F');
  42. y += settings.lineHeight / 2 + doc.internal.getLineHeight() / 2 - 2.5;
  43. doc.text(value, x + settings.padding, y);
  44. },
  45. margins: {right: 40, left: 40, top: 50, bottom: 40},
  46. startY: false,
  47. overflow: 'ellipsize', // false, ellipsize or linebreak (false passes the raw text to renderCell)
  48. overflowColumns: false, // Specify which colums that gets subjected to the overflow method chosen. false indicates all
  49. avoidPageSplit: false,
  50. extendWidth: true
  51. }
  52. };
  53. /**
  54. * Create a table from a set of rows and columns.
  55. *
  56. * @param {Object[]|String[]} columns Either as an array of objects or array of strings
  57. * @param {Object[][]|String[][]} data Either as an array of objects or array of strings
  58. * @param {Object} [options={}] Options that will override the default ones (above)
  59. */
  60. API.autoTable = function (columns, data, options) {
  61. options = options || {};
  62. columns = columns || [];
  63. doc = this;
  64. var userFontSize = doc.internal.getFontSize();
  65. initData({columns: columns, data: data});
  66. initOptions(options);
  67. cellPos = {
  68. x: settings.margins.left,
  69. y: settings.startY === false ? settings.margins.top : settings.startY
  70. };
  71. var tableHeight = settings.margins.bottom + settings.margins.top + settings.lineHeight * (data.length + 1) + 5 + settings.startY;
  72. if (settings.startY !== false && settings.avoidPageSplit && tableHeight > doc.internal.pageSize.height) {
  73. pageCount++;
  74. doc.addPage();
  75. cellPos.y = settings.margins.top;
  76. }
  77. settings.renderHeader(doc, pageCount, settings);
  78. var columnWidths = calculateColumnWidths(data, columns);
  79. printHeader(columns, columnWidths);
  80. printRows(columns, data, columnWidths);
  81. settings.renderFooter(doc, cellPos, pageCount, settings);
  82. doc.setFontSize(userFontSize);
  83. return this;
  84. };
  85. /**
  86. * Returns the Y position of the last drawn cell
  87. * @returns int
  88. */
  89. API.autoTableEndPosY = function () {
  90. // If cellPos is not set, autoTable() has probably not been called
  91. return cellPos ? cellPos.y : false;
  92. };
  93. /**
  94. * @deprecated Use autoTableEndPosY()
  95. */
  96. API.autoTableEndPos = function () {
  97. return cellPos;
  98. };
  99. /**
  100. * Parses an html table. To draw a table, use it like this:
  101. * `doc.autoTable(false, doc.autoTableHtmlToJson(tableDomElem))`
  102. *
  103. * @param table Html table element
  104. * @param indexBased Boolean flag if result should be returned as seperate cols and data
  105. * @returns []|{} Array of objects with object keys as headers or based on indexes if indexBased is set to true
  106. */
  107. API.autoTableHtmlToJson = function (table, indexBased) {
  108. var data = [], headers = {}, header = table.rows[0], i, tableRow, rowData, j;
  109. if (indexBased) {
  110. headers = [];
  111. for (i = 0; i < header.cells.length; i++) {
  112. headers.push(header.cells[i] ? header.cells[i].textContent : '');
  113. }
  114. for (i = 1; i < table.rows.length; i++) {
  115. tableRow = table.rows[i];
  116. rowData = [];
  117. for (j = 0; j < header.cells.length; j++) {
  118. rowData.push(tableRow.cells[j] ? tableRow.cells[j].textContent : '');
  119. }
  120. data.push(rowData);
  121. }
  122. return {columns: headers, data: data};
  123. } else {
  124. for (i = 0; i < header.cells.length; i++) {
  125. headers[i] = header.cells[i] ? header.cells[i].textContent : '';
  126. }
  127. for (i = 1; i < table.rows.length; i++) {
  128. tableRow = table.rows[i];
  129. rowData = {};
  130. for (j = 0; j < header.cells.length; j++) {
  131. rowData[headers[j]] = tableRow.cells[j] ? tableRow.cells[j].textContent : '';
  132. }
  133. data.push(rowData);
  134. }
  135. return data;
  136. }
  137. };
  138. /**
  139. * Transform all to the object initialization form
  140. * @param params
  141. */
  142. function initData(params) {
  143. // Object only initial
  144. if (!params.columns || params.columns.length === 0) {
  145. var keys = Object.keys(params.data[0]);
  146. Array.prototype.push.apply(params.columns, keys);
  147. params.columns.forEach(function (title, i) {
  148. params.columns[i] = {title: title, key: keys[i]};
  149. });
  150. }
  151. // Array initialization form
  152. else if (typeof params.columns[0] === 'string') {
  153. params.data.forEach(function (row, i) {
  154. var obj = {};
  155. for (var j = 0; j < row.length; j++) {
  156. obj[j] = params.data[i][j];
  157. }
  158. params.data[i] = obj;
  159. });
  160. params.columns.forEach(function (title, i) {
  161. params.columns[i] = {title: title, key: i};
  162. });
  163. } else {
  164. // Use options as is
  165. }
  166. }
  167. function initOptions(raw) {
  168. settings = defaultOptions();
  169. Object.keys(raw).forEach(function (key) {
  170. settings[key] = raw[key];
  171. });
  172. doc.setFontSize(settings.fontSize);
  173. // Backwards compatibility
  174. if(settings.margins.horizontal !== undefined) {
  175. settings.margins.left = settings.margins.horizontal;
  176. settings.margins.right = settings.margins.horizontal;
  177. } else {
  178. settings.margins.horizontal = settings.margins.left;
  179. }
  180. }
  181. function calculateColumnWidths(rows, columns) {
  182. var widths = {};
  183. // Optimal widths
  184. var optimalTableWidth = 0;
  185. columns.forEach(function (header) {
  186. var widest = getStringWidth(header.title || '', true);
  187. if(typeof header.width == "number") {
  188. widest = header.width;
  189. } else {
  190. rows.forEach(function (row) {
  191. if (!header.hasOwnProperty('key'))
  192. throw new Error("The key attribute is required in every header");
  193. var w = getStringWidth(stringify(row, header.key));
  194. if (w > widest) {
  195. widest = w;
  196. }
  197. });
  198. }
  199. widths[header.key] = widest;
  200. optimalTableWidth += widest;
  201. });
  202. var paddingAndMargin = settings.padding * 2 * columns.length + settings.margins.left + settings.margins.right;
  203. var spaceDiff = doc.internal.pageSize.width - optimalTableWidth - paddingAndMargin;
  204. var keys = Object.keys(widths);
  205. if (spaceDiff < 0) {
  206. // Shrink columns
  207. var shrinkableColumns = [];
  208. var shrinkableColumnWidths = 0;
  209. if (settings.overflowColumns === false) {
  210. keys.forEach(function (key) {
  211. if (widths[key] > MIN_COLUMN_WIDTH) {
  212. shrinkableColumns.push(key);
  213. shrinkableColumnWidths += widths[key];
  214. }
  215. });
  216. } else {
  217. shrinkableColumns = settings.overflowColumns;
  218. shrinkableColumns.forEach(function (col) {
  219. shrinkableColumnWidths += widths[col];
  220. });
  221. }
  222. shrinkableColumns.forEach(function (key) {
  223. widths[key] += spaceDiff * (widths[key] / shrinkableColumnWidths);
  224. });
  225. } else if (spaceDiff > 0 && settings.extendWidth) {
  226. // Fill page horizontally
  227. keys.forEach(function (key) {
  228. widths[key] += spaceDiff / keys.length;
  229. });
  230. }
  231. return widths;
  232. }
  233. function printHeader(headers, columnWidths) {
  234. if (!headers) return;
  235. // First calculate the height of the row
  236. // (to do that the maxium amount of rows first need to be found)
  237. var maxRows = 1;
  238. if (settings.overflow === 'linebreak') {
  239. headers.forEach(function (header) {
  240. if (isOverflowColumn(header)) {
  241. var value = header.title || '';
  242. var arr = doc.splitTextToSize(value, columnWidths[header.key]);
  243. if (arr.length > maxRows) {
  244. maxRows = arr.length;
  245. }
  246. }
  247. });
  248. }
  249. var rowHeight = settings.lineHeight + (maxRows - 1) * doc.internal.getLineHeight() + 5;
  250. headers.forEach(function (header) {
  251. var width = columnWidths[header.key] + settings.padding * 2;
  252. var value = header.title || '';
  253. if (settings.overflow === 'linebreak') {
  254. if (isOverflowColumn(header)) {
  255. value = doc.splitTextToSize(value, columnWidths[header.key]);
  256. }
  257. } else if (settings.overflow === 'ellipsize') {
  258. value = ellipsize(columnWidths[header.key], value);
  259. }
  260. settings.renderHeaderCell(cellPos.x, cellPos.y, width, rowHeight, header.key, value, settings);
  261. cellPos.x += width;
  262. });
  263. doc.setTextColor(70, 70, 70);
  264. doc.setFontStyle('normal');
  265. cellPos.y += rowHeight;
  266. cellPos.x = settings.margins.left;
  267. }
  268. function printRows(headers, rows, columnWidths) {
  269. for (var i = 0; i < rows.length; i++) {
  270. var row = rows[i];
  271. // First calculate the height of the row
  272. // (to do that the maxium amount of rows first need to be found)
  273. var maxRows = 1;
  274. if (settings.overflow === 'linebreak') {
  275. headers.forEach(function (header) {
  276. if (isOverflowColumn(header)) {
  277. var value = stringify(row, header.key);
  278. var arr = doc.splitTextToSize(value, columnWidths[header.key]);
  279. if (arr.length > maxRows) {
  280. maxRows = arr.length;
  281. }
  282. }
  283. });
  284. }
  285. var rowHeight = settings.lineHeight + (maxRows - 1) * doc.internal.getLineHeight();
  286. // Render the cell
  287. headers.forEach(function (header) {
  288. var value = stringify(row, header.key);
  289. if (settings.overflow === 'linebreak') {
  290. if (isOverflowColumn(header)) {
  291. value = doc.splitTextToSize(value, columnWidths[header.key]);
  292. }
  293. } else if (settings.overflow === 'ellipsize') {
  294. value = ellipsize(columnWidths[header.key], value);
  295. }
  296. var width = columnWidths[header.key] + settings.padding * 2;
  297. settings.renderCell(cellPos.x, cellPos.y, width, rowHeight, header.key, value, i, settings);
  298. cellPos.x = cellPos.x + columnWidths[header.key] + settings.padding * 2;
  299. });
  300. // Add a new page if cellpos is at the end of page
  301. var newPage = (cellPos.y + settings.margins.bottom + settings.lineHeight * 2) >= doc.internal.pageSize.height;
  302. if (newPage) {
  303. settings.renderFooter(doc, cellPos, pageCount, settings);
  304. doc.addPage();
  305. cellPos = {x: settings.margins.left, y: settings.margins.top};
  306. pageCount++;
  307. settings.renderHeader(doc, pageCount, settings);
  308. printHeader(headers, columnWidths);
  309. } else {
  310. cellPos.y += rowHeight;
  311. cellPos.x = settings.margins.left;
  312. }
  313. }
  314. }
  315. function isOverflowColumn(header) {
  316. return settings.overflowColumns === false || settings.overflowColumns.indexOf(header.key) !== -1;
  317. }
  318. /**
  319. * Ellipsize the text to fit in the width
  320. * @param width
  321. * @param text
  322. */
  323. function ellipsize(width, text) {
  324. if (width >= getStringWidth(text)) {
  325. return text;
  326. }
  327. while (width < getStringWidth(text + "...")) {
  328. if (text.length < 2) {
  329. break;
  330. }
  331. text = text.substring(0, text.length - 1);
  332. }
  333. text += "...";
  334. return text;
  335. }
  336. function stringify(row, key) {
  337. return row.hasOwnProperty(key) ? '' + row[key] : '';
  338. }
  339. function getStringWidth(txt, isBold) {
  340. if(isBold) {
  341. doc.setFontStyle('bold');
  342. }
  343. var strWidth = doc.getStringUnitWidth(txt) * doc.internal.getFontSize();
  344. if(isBold) {
  345. doc.setFontStyle('normal');
  346. }
  347. return strWidth;
  348. }
  349. })(jsPDF.API);