modules/cell.js

  1. /**
  2. * @license
  3. * ====================================================================
  4. * Copyright (c) 2013 Youssef Beddad, youssef.beddad@gmail.com
  5. * 2013 Eduardo Menezes de Morais, eduardo.morais@usp.br
  6. * 2013 Lee Driscoll, https://github.com/lsdriscoll
  7. * 2014 Juan Pablo Gaviria, https://github.com/juanpgaviria
  8. * 2014 James Hall, james@parall.ax
  9. * 2014 Diego Casorran, https://github.com/diegocr
  10. *
  11. * Permission is hereby granted, free of charge, to any person obtaining
  12. * a copy of this software and associated documentation files (the
  13. * "Software"), to deal in the Software without restriction, including
  14. * without limitation the rights to use, copy, modify, merge, publish,
  15. * distribute, sublicense, and/or sell copies of the Software, and to
  16. * permit persons to whom the Software is furnished to do so, subject to
  17. * the following conditions:
  18. *
  19. * The above copyright notice and this permission notice shall be
  20. * included in all copies or substantial portions of the Software.
  21. *
  22. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  23. * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  24. * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  25. * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  26. * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  27. * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  28. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  29. * ====================================================================
  30. */
  31. import { jsPDF } from "../jspdf.js";
  32. /**
  33. * @name cell
  34. * @module
  35. */
  36. (function(jsPDFAPI) {
  37. "use strict";
  38. var NO_MARGINS = { left: 0, top: 0, bottom: 0, right: 0 };
  39. var px2pt = (0.264583 * 72) / 25.4;
  40. var printingHeaderRow = false;
  41. var _initialize = function() {
  42. if (typeof this.internal.__cell__ === "undefined") {
  43. this.internal.__cell__ = {};
  44. this.internal.__cell__.padding = 3;
  45. this.internal.__cell__.headerFunction = undefined;
  46. this.internal.__cell__.margins = Object.assign({}, NO_MARGINS);
  47. this.internal.__cell__.margins.width = this.getPageWidth();
  48. _reset.call(this);
  49. }
  50. };
  51. var _reset = function() {
  52. this.internal.__cell__.lastCell = new Cell();
  53. this.internal.__cell__.pages = 1;
  54. };
  55. var Cell = function() {
  56. var _x = arguments[0];
  57. Object.defineProperty(this, "x", {
  58. enumerable: true,
  59. get: function() {
  60. return _x;
  61. },
  62. set: function(value) {
  63. _x = value;
  64. }
  65. });
  66. var _y = arguments[1];
  67. Object.defineProperty(this, "y", {
  68. enumerable: true,
  69. get: function() {
  70. return _y;
  71. },
  72. set: function(value) {
  73. _y = value;
  74. }
  75. });
  76. var _width = arguments[2];
  77. Object.defineProperty(this, "width", {
  78. enumerable: true,
  79. get: function() {
  80. return _width;
  81. },
  82. set: function(value) {
  83. _width = value;
  84. }
  85. });
  86. var _height = arguments[3];
  87. Object.defineProperty(this, "height", {
  88. enumerable: true,
  89. get: function() {
  90. return _height;
  91. },
  92. set: function(value) {
  93. _height = value;
  94. }
  95. });
  96. var _text = arguments[4];
  97. Object.defineProperty(this, "text", {
  98. enumerable: true,
  99. get: function() {
  100. return _text;
  101. },
  102. set: function(value) {
  103. _text = value;
  104. }
  105. });
  106. var _lineNumber = arguments[5];
  107. Object.defineProperty(this, "lineNumber", {
  108. enumerable: true,
  109. get: function() {
  110. return _lineNumber;
  111. },
  112. set: function(value) {
  113. _lineNumber = value;
  114. }
  115. });
  116. var _align = arguments[6];
  117. Object.defineProperty(this, "align", {
  118. enumerable: true,
  119. get: function() {
  120. return _align;
  121. },
  122. set: function(value) {
  123. _align = value;
  124. }
  125. });
  126. return this;
  127. };
  128. Cell.prototype.clone = function() {
  129. return new Cell(
  130. this.x,
  131. this.y,
  132. this.width,
  133. this.height,
  134. this.text,
  135. this.lineNumber,
  136. this.align
  137. );
  138. };
  139. Cell.prototype.toArray = function() {
  140. return [
  141. this.x,
  142. this.y,
  143. this.width,
  144. this.height,
  145. this.text,
  146. this.lineNumber,
  147. this.align
  148. ];
  149. };
  150. /**
  151. * @name setHeaderFunction
  152. * @function
  153. * @param {function} func
  154. */
  155. jsPDFAPI.setHeaderFunction = function(func) {
  156. _initialize.call(this);
  157. this.internal.__cell__.headerFunction =
  158. typeof func === "function" ? func : undefined;
  159. return this;
  160. };
  161. /**
  162. * @name getTextDimensions
  163. * @function
  164. * @param {string} txt
  165. * @returns {Object} dimensions
  166. */
  167. jsPDFAPI.getTextDimensions = function(text, options) {
  168. _initialize.call(this);
  169. options = options || {};
  170. var fontSize = options.fontSize || this.getFontSize();
  171. var font = options.font || this.getFont();
  172. var scaleFactor = options.scaleFactor || this.internal.scaleFactor;
  173. var width = 0;
  174. var amountOfLines = 0;
  175. var height = 0;
  176. var tempWidth = 0;
  177. var scope = this;
  178. if (!Array.isArray(text) && typeof text !== "string") {
  179. if (typeof text === "number") {
  180. text = String(text);
  181. } else {
  182. throw new Error(
  183. "getTextDimensions expects text-parameter to be of type String or type Number or an Array of Strings."
  184. );
  185. }
  186. }
  187. const maxWidth = options.maxWidth;
  188. if (maxWidth > 0) {
  189. if (typeof text === "string") {
  190. text = this.splitTextToSize(text, maxWidth);
  191. } else if (Object.prototype.toString.call(text) === "[object Array]") {
  192. text = text.reduce(function(acc, textLine) {
  193. return acc.concat(scope.splitTextToSize(textLine, maxWidth));
  194. }, []);
  195. }
  196. } else {
  197. // Without the else clause, it will not work if you do not pass along maxWidth
  198. text = Array.isArray(text) ? text : [text];
  199. }
  200. for (var i = 0; i < text.length; i++) {
  201. tempWidth = this.getStringUnitWidth(text[i], { font: font }) * fontSize;
  202. if (width < tempWidth) {
  203. width = tempWidth;
  204. }
  205. }
  206. if (width !== 0) {
  207. amountOfLines = text.length;
  208. }
  209. width = width / scaleFactor;
  210. height = Math.max(
  211. (amountOfLines * fontSize * this.getLineHeightFactor() -
  212. fontSize * (this.getLineHeightFactor() - 1)) /
  213. scaleFactor,
  214. 0
  215. );
  216. return { w: width, h: height };
  217. };
  218. /**
  219. * @name cellAddPage
  220. * @function
  221. */
  222. jsPDFAPI.cellAddPage = function() {
  223. _initialize.call(this);
  224. this.addPage();
  225. var margins = this.internal.__cell__.margins || NO_MARGINS;
  226. this.internal.__cell__.lastCell = new Cell(
  227. margins.left,
  228. margins.top,
  229. undefined,
  230. undefined
  231. );
  232. this.internal.__cell__.pages += 1;
  233. return this;
  234. };
  235. /**
  236. * @name cell
  237. * @function
  238. * @param {number} x
  239. * @param {number} y
  240. * @param {number} width
  241. * @param {number} height
  242. * @param {string} text
  243. * @param {number} lineNumber lineNumber
  244. * @param {string} align
  245. * @return {jsPDF} jsPDF-instance
  246. */
  247. var cell = (jsPDFAPI.cell = function() {
  248. var currentCell;
  249. if (arguments[0] instanceof Cell) {
  250. currentCell = arguments[0];
  251. } else {
  252. currentCell = new Cell(
  253. arguments[0],
  254. arguments[1],
  255. arguments[2],
  256. arguments[3],
  257. arguments[4],
  258. arguments[5]
  259. );
  260. }
  261. _initialize.call(this);
  262. var lastCell = this.internal.__cell__.lastCell;
  263. var padding = this.internal.__cell__.padding;
  264. var margins = this.internal.__cell__.margins || NO_MARGINS;
  265. var tableHeaderRow = this.internal.__cell__.tableHeaderRow;
  266. var printHeaders = this.internal.__cell__.printHeaders;
  267. // If this is not the first cell, we must change its position
  268. if (typeof lastCell.lineNumber !== "undefined") {
  269. if (lastCell.lineNumber === currentCell.lineNumber) {
  270. //Same line
  271. currentCell.x = (lastCell.x || 0) + (lastCell.width || 0);
  272. currentCell.y = lastCell.y || 0;
  273. } else {
  274. //New line
  275. if (
  276. lastCell.y + lastCell.height + currentCell.height + margins.bottom >
  277. this.getPageHeight()
  278. ) {
  279. this.cellAddPage();
  280. currentCell.y = margins.top;
  281. if (printHeaders && tableHeaderRow) {
  282. this.printHeaderRow(currentCell.lineNumber, true);
  283. currentCell.y += tableHeaderRow[0].height;
  284. }
  285. } else {
  286. currentCell.y = lastCell.y + lastCell.height || currentCell.y;
  287. }
  288. }
  289. }
  290. if (typeof currentCell.text[0] !== "undefined") {
  291. this.rect(
  292. currentCell.x,
  293. currentCell.y,
  294. currentCell.width,
  295. currentCell.height,
  296. printingHeaderRow === true ? "FD" : undefined
  297. );
  298. if (currentCell.align === "right") {
  299. this.text(
  300. currentCell.text,
  301. currentCell.x + currentCell.width - padding,
  302. currentCell.y + padding,
  303. { align: "right", baseline: "top" }
  304. );
  305. } else if (currentCell.align === "center") {
  306. this.text(
  307. currentCell.text,
  308. currentCell.x + currentCell.width / 2,
  309. currentCell.y + padding,
  310. {
  311. align: "center",
  312. baseline: "top",
  313. maxWidth: currentCell.width - padding - padding
  314. }
  315. );
  316. } else {
  317. this.text(
  318. currentCell.text,
  319. currentCell.x + padding,
  320. currentCell.y + padding,
  321. {
  322. align: "left",
  323. baseline: "top",
  324. maxWidth: currentCell.width - padding - padding
  325. }
  326. );
  327. }
  328. }
  329. this.internal.__cell__.lastCell = currentCell;
  330. return this;
  331. });
  332. /**
  333. * Create a table from a set of data.
  334. * @name table
  335. * @function
  336. * @param {Integer} [x] : left-position for top-left corner of table
  337. * @param {Integer} [y] top-position for top-left corner of table
  338. * @param {Object[]} [data] An array of objects containing key-value pairs corresponding to a row of data.
  339. * @param {String[]} [headers] Omit or null to auto-generate headers at a performance cost
  340. * @param {Object} [config.printHeaders] True to print column headers at the top of every page
  341. * @param {Object} [config.autoSize] True to dynamically set the column widths to match the widest cell value
  342. * @param {Object} [config.margins] margin values for left, top, bottom, and width
  343. * @param {Object} [config.fontSize] Integer fontSize to use (optional)
  344. * @param {Object} [config.padding] cell-padding in pt to use (optional)
  345. * @param {Object} [config.headerBackgroundColor] default is #c8c8c8 (optional)
  346. * @param {Object} [config.headerTextColor] default is #000 (optional)
  347. * @param {Object} [config.rowStart] callback to handle before print each row (optional)
  348. * @param {Object} [config.cellStart] callback to handle before print each cell (optional)
  349. * @returns {jsPDF} jsPDF-instance
  350. */
  351. jsPDFAPI.table = function(x, y, data, headers, config) {
  352. _initialize.call(this);
  353. if (!data) {
  354. throw new Error("No data for PDF table.");
  355. }
  356. config = config || {};
  357. var headerNames = [],
  358. headerLabels = [],
  359. headerAligns = [],
  360. i,
  361. columnMatrix = {},
  362. columnWidths = {},
  363. column,
  364. columnMinWidths = [],
  365. j,
  366. tableHeaderConfigs = [],
  367. //set up defaults. If a value is provided in config, defaults will be overwritten:
  368. autoSize = config.autoSize || false,
  369. printHeaders = config.printHeaders === false ? false : true,
  370. fontSize =
  371. config.css && typeof config.css["font-size"] !== "undefined"
  372. ? config.css["font-size"] * 16
  373. : config.fontSize || 12,
  374. margins =
  375. config.margins ||
  376. Object.assign({ width: this.getPageWidth() }, NO_MARGINS),
  377. padding = typeof config.padding === "number" ? config.padding : 3,
  378. headerBackgroundColor = config.headerBackgroundColor || "#c8c8c8",
  379. headerTextColor = config.headerTextColor || "#000";
  380. _reset.call(this);
  381. this.internal.__cell__.printHeaders = printHeaders;
  382. this.internal.__cell__.margins = margins;
  383. this.internal.__cell__.table_font_size = fontSize;
  384. this.internal.__cell__.padding = padding;
  385. this.internal.__cell__.headerBackgroundColor = headerBackgroundColor;
  386. this.internal.__cell__.headerTextColor = headerTextColor;
  387. this.setFontSize(fontSize);
  388. // Set header values
  389. if (headers === undefined || headers === null) {
  390. // No headers defined so we derive from data
  391. headerNames = Object.keys(data[0]);
  392. headerLabels = headerNames;
  393. headerAligns = headerNames.map(function() {
  394. return "left";
  395. });
  396. } else if (Array.isArray(headers) && typeof headers[0] === "object") {
  397. headerNames = headers.map(function(header) {
  398. return header.name;
  399. });
  400. headerLabels = headers.map(function(header) {
  401. return header.prompt || header.name || "";
  402. });
  403. headerAligns = headers.map(function(header) {
  404. return header.align || "left";
  405. });
  406. // Split header configs into names and prompts
  407. for (i = 0; i < headers.length; i += 1) {
  408. columnWidths[headers[i].name] = headers[i].width * px2pt;
  409. }
  410. } else if (Array.isArray(headers) && typeof headers[0] === "string") {
  411. headerNames = headers;
  412. headerLabels = headerNames;
  413. headerAligns = headerNames.map(function() {
  414. return "left";
  415. });
  416. }
  417. if (
  418. autoSize ||
  419. (Array.isArray(headers) && typeof headers[0] === "string")
  420. ) {
  421. var headerName;
  422. for (i = 0; i < headerNames.length; i += 1) {
  423. headerName = headerNames[i];
  424. // Create a matrix of columns e.g., {column_title: [row1_Record, row2_Record]}
  425. columnMatrix[headerName] = data.map(function(rec) {
  426. return rec[headerName];
  427. });
  428. // get header width
  429. this.setFont(undefined, "bold");
  430. columnMinWidths.push(
  431. this.getTextDimensions(headerLabels[i], {
  432. fontSize: this.internal.__cell__.table_font_size,
  433. scaleFactor: this.internal.scaleFactor
  434. }).w
  435. );
  436. column = columnMatrix[headerName];
  437. // get cell widths
  438. this.setFont(undefined, "normal");
  439. for (j = 0; j < column.length; j += 1) {
  440. columnMinWidths.push(
  441. this.getTextDimensions(column[j], {
  442. fontSize: this.internal.__cell__.table_font_size,
  443. scaleFactor: this.internal.scaleFactor
  444. }).w
  445. );
  446. }
  447. // get final column width
  448. columnWidths[headerName] =
  449. Math.max.apply(null, columnMinWidths) + padding + padding;
  450. //have to reset
  451. columnMinWidths = [];
  452. }
  453. }
  454. // -- Construct the table
  455. if (printHeaders) {
  456. var row = {};
  457. for (i = 0; i < headerNames.length; i += 1) {
  458. row[headerNames[i]] = {};
  459. row[headerNames[i]].text = headerLabels[i];
  460. row[headerNames[i]].align = headerAligns[i];
  461. }
  462. var rowHeight = calculateLineHeight.call(this, row, columnWidths);
  463. // Construct the header row
  464. tableHeaderConfigs = headerNames.map(function(value) {
  465. return new Cell(
  466. x,
  467. y,
  468. columnWidths[value],
  469. rowHeight,
  470. row[value].text,
  471. undefined,
  472. row[value].align
  473. );
  474. });
  475. // Store the table header config
  476. this.setTableHeaderRow(tableHeaderConfigs);
  477. // Print the header for the start of the table
  478. this.printHeaderRow(1, false);
  479. }
  480. // Construct the data rows
  481. var align = headers.reduce(function(pv, cv) {
  482. pv[cv.name] = cv.align;
  483. return pv;
  484. }, {});
  485. for (i = 0; i < data.length; i += 1) {
  486. if ("rowStart" in config && config.rowStart instanceof Function) {
  487. config.rowStart(
  488. {
  489. row: i,
  490. data: data[i]
  491. },
  492. this
  493. );
  494. }
  495. var lineHeight = calculateLineHeight.call(this, data[i], columnWidths);
  496. for (j = 0; j < headerNames.length; j += 1) {
  497. var cellData = data[i][headerNames[j]];
  498. if ("cellStart" in config && config.cellStart instanceof Function) {
  499. config.cellStart(
  500. {
  501. row: i,
  502. col: j,
  503. data: cellData
  504. },
  505. this
  506. );
  507. }
  508. cell.call(
  509. this,
  510. new Cell(
  511. x,
  512. y,
  513. columnWidths[headerNames[j]],
  514. lineHeight,
  515. cellData,
  516. i + 2,
  517. align[headerNames[j]]
  518. )
  519. );
  520. }
  521. }
  522. this.internal.__cell__.table_x = x;
  523. this.internal.__cell__.table_y = y;
  524. return this;
  525. };
  526. /**
  527. * Calculate the height for containing the highest column
  528. *
  529. * @name calculateLineHeight
  530. * @function
  531. * @param {Object[]} model is the line of data we want to calculate the height of
  532. * @param {Integer[]} columnWidths is size of each column
  533. * @returns {number} lineHeight
  534. * @private
  535. */
  536. var calculateLineHeight = function calculateLineHeight(model, columnWidths) {
  537. var padding = this.internal.__cell__.padding;
  538. var fontSize = this.internal.__cell__.table_font_size;
  539. var scaleFactor = this.internal.scaleFactor;
  540. return Object.keys(model)
  541. .map(function(key) {
  542. var value = model[key];
  543. return this.splitTextToSize(
  544. value.hasOwnProperty("text") ? value.text : value,
  545. columnWidths[key] - padding - padding
  546. );
  547. }, this)
  548. .map(function(value) {
  549. return (
  550. (this.getLineHeightFactor() * value.length * fontSize) / scaleFactor +
  551. padding +
  552. padding
  553. );
  554. }, this)
  555. .reduce(function(pv, cv) {
  556. return Math.max(pv, cv);
  557. }, 0);
  558. };
  559. /**
  560. * Store the config for outputting a table header
  561. *
  562. * @name setTableHeaderRow
  563. * @function
  564. * @param {Object[]} config
  565. * An array of cell configs that would define a header row: Each config matches the config used by jsPDFAPI.cell
  566. * except the lineNumber parameter is excluded
  567. */
  568. jsPDFAPI.setTableHeaderRow = function(config) {
  569. _initialize.call(this);
  570. this.internal.__cell__.tableHeaderRow = config;
  571. };
  572. /**
  573. * Output the store header row
  574. *
  575. * @name printHeaderRow
  576. * @function
  577. * @param {number} lineNumber The line number to output the header at
  578. * @param {boolean} new_page
  579. */
  580. jsPDFAPI.printHeaderRow = function(lineNumber, new_page) {
  581. _initialize.call(this);
  582. if (!this.internal.__cell__.tableHeaderRow) {
  583. throw new Error("Property tableHeaderRow does not exist.");
  584. }
  585. var tableHeaderCell;
  586. printingHeaderRow = true;
  587. if (typeof this.internal.__cell__.headerFunction === "function") {
  588. var position = this.internal.__cell__.headerFunction(
  589. this,
  590. this.internal.__cell__.pages
  591. );
  592. this.internal.__cell__.lastCell = new Cell(
  593. position[0],
  594. position[1],
  595. position[2],
  596. position[3],
  597. undefined,
  598. -1
  599. );
  600. }
  601. this.setFont(undefined, "bold");
  602. var tempHeaderConf = [];
  603. for (var i = 0; i < this.internal.__cell__.tableHeaderRow.length; i += 1) {
  604. tableHeaderCell = this.internal.__cell__.tableHeaderRow[i].clone();
  605. if (new_page) {
  606. tableHeaderCell.y = this.internal.__cell__.margins.top || 0;
  607. tempHeaderConf.push(tableHeaderCell);
  608. }
  609. tableHeaderCell.lineNumber = lineNumber;
  610. var currentTextColor = this.getTextColor();
  611. this.setTextColor(this.internal.__cell__.headerTextColor);
  612. this.setFillColor(this.internal.__cell__.headerBackgroundColor);
  613. cell.call(this, tableHeaderCell);
  614. this.setTextColor(currentTextColor);
  615. }
  616. if (tempHeaderConf.length > 0) {
  617. this.setTableHeaderRow(tempHeaderConf);
  618. }
  619. this.setFont(undefined, "normal");
  620. printingHeaderRow = false;
  621. };
  622. })(jsPDF.API);