modules/cell.js

/**
 * @license
 * ====================================================================
 * Copyright (c) 2013 Youssef Beddad, youssef.beddad@gmail.com
 *               2013 Eduardo Menezes de Morais, eduardo.morais@usp.br
 *               2013 Lee Driscoll, https://github.com/lsdriscoll
 *               2014 Juan Pablo Gaviria, https://github.com/juanpgaviria
 *               2014 James Hall, james@parall.ax
 *               2014 Diego Casorran, https://github.com/diegocr
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 * ====================================================================
 */

import { jsPDF } from "../jspdf.js";

/**
 * @name cell
 * @module
 */
(function(jsPDFAPI) {
  "use strict";

  var NO_MARGINS = { left: 0, top: 0, bottom: 0, right: 0 };

  var px2pt = (0.264583 * 72) / 25.4;
  var printingHeaderRow = false;

  var _initialize = function() {
    if (typeof this.internal.__cell__ === "undefined") {
      this.internal.__cell__ = {};
      this.internal.__cell__.padding = 3;
      this.internal.__cell__.headerFunction = undefined;
      this.internal.__cell__.margins = Object.assign({}, NO_MARGINS);
      this.internal.__cell__.margins.width = this.getPageWidth();
      _reset.call(this);
    }
  };

  var _reset = function() {
    this.internal.__cell__.lastCell = new Cell();
    this.internal.__cell__.pages = 1;
  };

  var Cell = function() {
    var _x = arguments[0];
    Object.defineProperty(this, "x", {
      enumerable: true,
      get: function() {
        return _x;
      },
      set: function(value) {
        _x = value;
      }
    });
    var _y = arguments[1];
    Object.defineProperty(this, "y", {
      enumerable: true,
      get: function() {
        return _y;
      },
      set: function(value) {
        _y = value;
      }
    });
    var _width = arguments[2];
    Object.defineProperty(this, "width", {
      enumerable: true,
      get: function() {
        return _width;
      },
      set: function(value) {
        _width = value;
      }
    });
    var _height = arguments[3];
    Object.defineProperty(this, "height", {
      enumerable: true,
      get: function() {
        return _height;
      },
      set: function(value) {
        _height = value;
      }
    });
    var _text = arguments[4];
    Object.defineProperty(this, "text", {
      enumerable: true,
      get: function() {
        return _text;
      },
      set: function(value) {
        _text = value;
      }
    });
    var _lineNumber = arguments[5];
    Object.defineProperty(this, "lineNumber", {
      enumerable: true,
      get: function() {
        return _lineNumber;
      },
      set: function(value) {
        _lineNumber = value;
      }
    });
    var _align = arguments[6];
    Object.defineProperty(this, "align", {
      enumerable: true,
      get: function() {
        return _align;
      },
      set: function(value) {
        _align = value;
      }
    });

    return this;
  };

  Cell.prototype.clone = function() {
    return new Cell(
      this.x,
      this.y,
      this.width,
      this.height,
      this.text,
      this.lineNumber,
      this.align
    );
  };

  Cell.prototype.toArray = function() {
    return [
      this.x,
      this.y,
      this.width,
      this.height,
      this.text,
      this.lineNumber,
      this.align
    ];
  };

  /**
   * @name setHeaderFunction
   * @function
   * @param {function} func
   */
  jsPDFAPI.setHeaderFunction = function(func) {
    _initialize.call(this);
    this.internal.__cell__.headerFunction =
      typeof func === "function" ? func : undefined;
    return this;
  };

  /**
   * @name getTextDimensions
   * @function
   * @param {string} txt
   * @returns {Object} dimensions
   */
  jsPDFAPI.getTextDimensions = function(text, options) {
    _initialize.call(this);
    options = options || {};
    var fontSize = options.fontSize || this.getFontSize();
    var font = options.font || this.getFont();
    var scaleFactor = options.scaleFactor || this.internal.scaleFactor;
    var width = 0;
    var amountOfLines = 0;
    var height = 0;
    var tempWidth = 0;
    var scope = this;

    if (!Array.isArray(text) && typeof text !== "string") {
      if (typeof text === "number") {
        text = String(text);
      } else {
        throw new Error(
          "getTextDimensions expects text-parameter to be of type String or type Number or an Array of Strings."
        );
      }
    }

    const maxWidth = options.maxWidth;
    if (maxWidth > 0) {
      if (typeof text === "string") {
        text = this.splitTextToSize(text, maxWidth);
      } else if (Object.prototype.toString.call(text) === "[object Array]") {
        text = text.reduce(function(acc, textLine) {
          return acc.concat(scope.splitTextToSize(textLine, maxWidth));
        }, []);
      }
    } else {
      // Without the else clause, it will not work if you do not pass along maxWidth
      text = Array.isArray(text) ? text : [text];
    }

    for (var i = 0; i < text.length; i++) {
      tempWidth = this.getStringUnitWidth(text[i], { font: font }) * fontSize;
      if (width < tempWidth) {
        width = tempWidth;
      }
    }

    if (width !== 0) {
      amountOfLines = text.length;
    }

    width = width / scaleFactor;
    height = Math.max(
      (amountOfLines * fontSize * this.getLineHeightFactor() -
        fontSize * (this.getLineHeightFactor() - 1)) /
        scaleFactor,
      0
    );
    return { w: width, h: height };
  };

  /**
   * @name cellAddPage
   * @function
   */
  jsPDFAPI.cellAddPage = function() {
    _initialize.call(this);

    this.addPage();

    var margins = this.internal.__cell__.margins || NO_MARGINS;
    this.internal.__cell__.lastCell = new Cell(
      margins.left,
      margins.top,
      undefined,
      undefined
    );
    this.internal.__cell__.pages += 1;

    return this;
  };

  /**
   * @name cell
   * @function
   * @param {number} x
   * @param {number} y
   * @param {number} width
   * @param {number} height
   * @param {string} text
   * @param {number} lineNumber lineNumber
   * @param {string} align
   * @return {jsPDF} jsPDF-instance
   */
  var cell = (jsPDFAPI.cell = function() {
    var currentCell;

    if (arguments[0] instanceof Cell) {
      currentCell = arguments[0];
    } else {
      currentCell = new Cell(
        arguments[0],
        arguments[1],
        arguments[2],
        arguments[3],
        arguments[4],
        arguments[5]
      );
    }
    _initialize.call(this);
    var lastCell = this.internal.__cell__.lastCell;
    var padding = this.internal.__cell__.padding;
    var margins = this.internal.__cell__.margins || NO_MARGINS;
    var tableHeaderRow = this.internal.__cell__.tableHeaderRow;
    var printHeaders = this.internal.__cell__.printHeaders;
    // If this is not the first cell, we must change its position
    if (typeof lastCell.lineNumber !== "undefined") {
      if (lastCell.lineNumber === currentCell.lineNumber) {
        //Same line
        currentCell.x = (lastCell.x || 0) + (lastCell.width || 0);
        currentCell.y = lastCell.y || 0;
      } else {
        //New line
        if (
          lastCell.y + lastCell.height + currentCell.height + margins.bottom >
          this.getPageHeight()
        ) {
          this.cellAddPage();
          currentCell.y = margins.top;
          if (printHeaders && tableHeaderRow) {
            this.printHeaderRow(currentCell.lineNumber, true);
            currentCell.y += tableHeaderRow[0].height;
          }
        } else {
          currentCell.y = lastCell.y + lastCell.height || currentCell.y;
        }
      }
    }

    if (typeof currentCell.text[0] !== "undefined") {
      this.rect(
        currentCell.x,
        currentCell.y,
        currentCell.width,
        currentCell.height,
        printingHeaderRow === true ? "FD" : undefined
      );
      if (currentCell.align === "right") {
        this.text(
          currentCell.text,
          currentCell.x + currentCell.width - padding,
          currentCell.y + padding,
          { align: "right", baseline: "top" }
        );
      } else if (currentCell.align === "center") {
        this.text(
          currentCell.text,
          currentCell.x + currentCell.width / 2,
          currentCell.y + padding,
          {
            align: "center",
            baseline: "top",
            maxWidth: currentCell.width - padding - padding
          }
        );
      } else {
        this.text(
          currentCell.text,
          currentCell.x + padding,
          currentCell.y + padding,
          {
            align: "left",
            baseline: "top",
            maxWidth: currentCell.width - padding - padding
          }
        );
      }
    }
    this.internal.__cell__.lastCell = currentCell;
    return this;
  });

  /**
     * Create a table from a set of data.
     * @name table
     * @function
     * @param {Integer} [x] : left-position for top-left corner of table
     * @param {Integer} [y] top-position for top-left corner of table
     * @param {Object[]} [data] An array of objects containing key-value pairs corresponding to a row of data.
     * @param {String[]} [headers] Omit or null to auto-generate headers at a performance cost

     * @param {Object} [config.printHeaders] True to print column headers at the top of every page
     * @param {Object} [config.autoSize] True to dynamically set the column widths to match the widest cell value
     * @param {Object} [config.margins] margin values for left, top, bottom, and width
     * @param {Object} [config.fontSize] Integer fontSize to use (optional)
     * @param {Object} [config.padding] cell-padding in pt to use (optional)
     * @param {Object} [config.headerBackgroundColor] default is #c8c8c8 (optional)
     * @param {Object} [config.headerTextColor] default is #000 (optional)
     * @param {Object} [config.rowStart] callback to handle before print each row (optional)
     * @param {Object} [config.cellStart] callback to handle before print each cell (optional)
     * @returns {jsPDF} jsPDF-instance
     */

  jsPDFAPI.table = function(x, y, data, headers, config) {
    _initialize.call(this);
    if (!data) {
      throw new Error("No data for PDF table.");
    }

    config = config || {};

    var headerNames = [],
      headerLabels = [],
      headerAligns = [],
      i,
      columnMatrix = {},
      columnWidths = {},
      column,
      columnMinWidths = [],
      j,
      tableHeaderConfigs = [],
      //set up defaults. If a value is provided in config, defaults will be overwritten:
      autoSize = config.autoSize || false,
      printHeaders = config.printHeaders === false ? false : true,
      fontSize =
        config.css && typeof config.css["font-size"] !== "undefined"
          ? config.css["font-size"] * 16
          : config.fontSize || 12,
      margins =
        config.margins ||
        Object.assign({ width: this.getPageWidth() }, NO_MARGINS),
      padding = typeof config.padding === "number" ? config.padding : 3,
      headerBackgroundColor = config.headerBackgroundColor || "#c8c8c8",
      headerTextColor = config.headerTextColor || "#000";

    _reset.call(this);

    this.internal.__cell__.printHeaders = printHeaders;
    this.internal.__cell__.margins = margins;
    this.internal.__cell__.table_font_size = fontSize;
    this.internal.__cell__.padding = padding;
    this.internal.__cell__.headerBackgroundColor = headerBackgroundColor;
    this.internal.__cell__.headerTextColor = headerTextColor;
    this.setFontSize(fontSize);

    // Set header values
    if (headers === undefined || headers === null) {
      // No headers defined so we derive from data
      headerNames = Object.keys(data[0]);
      headerLabels = headerNames;
      headerAligns = headerNames.map(function() {
        return "left";
      });
    } else if (Array.isArray(headers) && typeof headers[0] === "object") {
      headerNames = headers.map(function(header) {
        return header.name;
      });
      headerLabels = headers.map(function(header) {
        return header.prompt || header.name || "";
      });
      headerAligns = headers.map(function(header) {
        return header.align || "left";
      });
      // Split header configs into names and prompts
      for (i = 0; i < headers.length; i += 1) {
        columnWidths[headers[i].name] = headers[i].width * px2pt;
      }
    } else if (Array.isArray(headers) && typeof headers[0] === "string") {
      headerNames = headers;
      headerLabels = headerNames;
      headerAligns = headerNames.map(function() {
        return "left";
      });
    }

    if (
      autoSize ||
      (Array.isArray(headers) && typeof headers[0] === "string")
    ) {
      var headerName;
      for (i = 0; i < headerNames.length; i += 1) {
        headerName = headerNames[i];

        // Create a matrix of columns e.g., {column_title: [row1_Record, row2_Record]}

        columnMatrix[headerName] = data.map(function(rec) {
          return rec[headerName];
        });

        // get header width
        this.setFont(undefined, "bold");
        columnMinWidths.push(
          this.getTextDimensions(headerLabels[i], {
            fontSize: this.internal.__cell__.table_font_size,
            scaleFactor: this.internal.scaleFactor
          }).w
        );
        column = columnMatrix[headerName];

        // get cell widths
        this.setFont(undefined, "normal");
        for (j = 0; j < column.length; j += 1) {
          columnMinWidths.push(
            this.getTextDimensions(column[j], {
              fontSize: this.internal.__cell__.table_font_size,
              scaleFactor: this.internal.scaleFactor
            }).w
          );
        }

        // get final column width
        columnWidths[headerName] =
          Math.max.apply(null, columnMinWidths) + padding + padding;

        //have to reset
        columnMinWidths = [];
      }
    }

    // -- Construct the table

    if (printHeaders) {
      var row = {};
      for (i = 0; i < headerNames.length; i += 1) {
        row[headerNames[i]] = {};
        row[headerNames[i]].text = headerLabels[i];
        row[headerNames[i]].align = headerAligns[i];
      }

      var rowHeight = calculateLineHeight.call(this, row, columnWidths);

      // Construct the header row
      tableHeaderConfigs = headerNames.map(function(value) {
        return new Cell(
          x,
          y,
          columnWidths[value],
          rowHeight,
          row[value].text,
          undefined,
          row[value].align
        );
      });

      // Store the table header config
      this.setTableHeaderRow(tableHeaderConfigs);

      // Print the header for the start of the table
      this.printHeaderRow(1, false);
    }

    // Construct the data rows

    var align = headers.reduce(function(pv, cv) {
      pv[cv.name] = cv.align;
      return pv;
    }, {});
    for (i = 0; i < data.length; i += 1) {
      if ("rowStart" in config && config.rowStart instanceof Function) {
        config.rowStart(
          {
            row: i,
            data: data[i]
          },
          this
        );
      }
      var lineHeight = calculateLineHeight.call(this, data[i], columnWidths);

      for (j = 0; j < headerNames.length; j += 1) {
        var cellData = data[i][headerNames[j]];
        if ("cellStart" in config && config.cellStart instanceof Function) {
          config.cellStart(
            {
              row: i,
              col: j,
              data: cellData
            },
            this
          );
        }
        cell.call(
          this,
          new Cell(
            x,
            y,
            columnWidths[headerNames[j]],
            lineHeight,
            cellData,
            i + 2,
            align[headerNames[j]]
          )
        );
      }
    }
    this.internal.__cell__.table_x = x;
    this.internal.__cell__.table_y = y;
    return this;
  };

  /**
   * Calculate the height for containing the highest column
   *
   * @name calculateLineHeight
   * @function
   * @param {Object[]} model is the line of data we want to calculate the height of
   * @param {Integer[]} columnWidths is size of each column
   * @returns {number} lineHeight
   * @private
   */
  var calculateLineHeight = function calculateLineHeight(model, columnWidths) {
    var padding = this.internal.__cell__.padding;
    var fontSize = this.internal.__cell__.table_font_size;
    var scaleFactor = this.internal.scaleFactor;

    return Object.keys(model)
      .map(function(key) {
        var value = model[key];
        return this.splitTextToSize(
          value.hasOwnProperty("text") ? value.text : value,
          columnWidths[key] - padding - padding
        );
      }, this)
      .map(function(value) {
        return (
          (this.getLineHeightFactor() * value.length * fontSize) / scaleFactor +
          padding +
          padding
        );
      }, this)
      .reduce(function(pv, cv) {
        return Math.max(pv, cv);
      }, 0);
  };

  /**
   * Store the config for outputting a table header
   *
   * @name setTableHeaderRow
   * @function
   * @param {Object[]} config
   * An array of cell configs that would define a header row: Each config matches the config used by jsPDFAPI.cell
   * except the lineNumber parameter is excluded
   */
  jsPDFAPI.setTableHeaderRow = function(config) {
    _initialize.call(this);
    this.internal.__cell__.tableHeaderRow = config;
  };

  /**
   * Output the store header row
   *
   * @name printHeaderRow
   * @function
   * @param {number} lineNumber The line number to output the header at
   * @param {boolean} new_page
   */
  jsPDFAPI.printHeaderRow = function(lineNumber, new_page) {
    _initialize.call(this);
    if (!this.internal.__cell__.tableHeaderRow) {
      throw new Error("Property tableHeaderRow does not exist.");
    }

    var tableHeaderCell;

    printingHeaderRow = true;
    if (typeof this.internal.__cell__.headerFunction === "function") {
      var position = this.internal.__cell__.headerFunction(
        this,
        this.internal.__cell__.pages
      );
      this.internal.__cell__.lastCell = new Cell(
        position[0],
        position[1],
        position[2],
        position[3],
        undefined,
        -1
      );
    }
    this.setFont(undefined, "bold");

    var tempHeaderConf = [];
    for (var i = 0; i < this.internal.__cell__.tableHeaderRow.length; i += 1) {
      tableHeaderCell = this.internal.__cell__.tableHeaderRow[i].clone();
      if (new_page) {
        tableHeaderCell.y = this.internal.__cell__.margins.top || 0;
        tempHeaderConf.push(tableHeaderCell);
      }
      tableHeaderCell.lineNumber = lineNumber;
      var currentTextColor = this.getTextColor();
      this.setTextColor(this.internal.__cell__.headerTextColor);
      this.setFillColor(this.internal.__cell__.headerBackgroundColor);
      cell.call(this, tableHeaderCell);
      this.setTextColor(currentTextColor);
    }
    if (tempHeaderConf.length > 0) {
      this.setTableHeaderRow(tempHeaderConf);
    }
    this.setFont(undefined, "normal");
    printingHeaderRow = false;
  };
})(jsPDF.API);