Source: PDFTable.js

/*
eslint no-underscore-dangle: ["error", {
  "allow": [
  "_addColumn",
  "_addHeader",
  "_addRow",
  "_columnWidth",
  "_headerLineGap",
  "_rowLineGap"]
}]
*/


/**
* Initialize a table for outputting to a PDF.
* Usage:
*   ```javascript
*   let pdfBuilder = new PDFBuilder();
*   let table = new PDFTable();
*   pdfBuilder.addTable()
*   ```
*
*   Minimum options required to build out the table:
*   ```javascript
*   let table = new PDFTable([{
*     header: { text: 'ID' },
*     rows:  [{ text: '1' }, { text: '2' }, { text: '3' }]
*   }, {
*     header: { text: 'Name' },
*     rows:  [{ text: 'Billy' }, { text: 'Bob' }, { text: 'Suzy' }]
*   }]);
*   ```
*
*   All available options:
*   ```javascript
*   let table = new PDFTable([{
*     header: {
*       align:       'center',
*       borderColor: '#000',
*       borderStyle: 'bottom',
*       borderWidth: 2,
*       color:       '#000',
*       font:        'Helvetica-Bold',
*       fontSize:    14,
*       text:        'Name'
*     },
*     rows: [{
*       align:       'center',
*       borderColor: '#000',
*       borderStyle: 'bottom',
*       borderWidth: 2,
*       color:       '#000',
*       font:        'Helvetica-Bold',
*       fontSize:    14,
*       text:        'Brian'
*     }],
*     width: 150 // or 0.25 for 25% of the table's width
*   }]);
*   ```
*/
class PDFTable {
  /**
   * @param {Array} columns Columns for the table
   * @param {Object} options Options for formatting and styling the table
   * @param {string} options.cellAlign=left Align text in the cell
   * @param {boolean} options.cellAllowWrap=false Allow cell wrapping
   * @param {number} options.cellBorderWidth=1 Cell border width
   * @param {string} options.cellColor=#222 Cell text color
   * @param {string} options.cellEmptyColor=#FFFFFF Empty cell color
   * @param {string} options.cellEmptyText=none Empty cell text content
   * @param {string} options.cellFont=Helvetica Cell font style
   * @param {number} options.cellFontSize=14 Cell font size
   * @param {boolean} options.cellItalic=false Cell is italicized
   * @param {boolean} options.headerAllowWrap=false Allow the header to wrap
   * @param {string} options.headerAlign=left Align text in the row header
   * @param {string} options.headerColor=#000 Row header text color
   * @param {Margin} options.margins Table margins
  */
  constructor(columns = [], options = {}) {
    this.cellAlign = options.cellAlign || 'left';
    this.cellColor = options.cellColor || '#222';
    this.cellBorderColor = options.headerColor || '#ccc';
    this.cellBorderWidth = options.cellBorderWidth || 1;
    this.cellFont = options.cellFont || 'Helvetica';
    this.cellItalic = options.cellItalic || false;
    this.cellFontSize = options.cellFontSize || 14;
    this.headerAlign = options.headerAlign || 'left';
    this.headerColor = options.headerColor || '#000';
    this.headerBorderColor = options.headerColor || '#000';
    this.headerBorderStyle = options.headerBorderStyle || 'bottom';
    this.headerBorderWidth = options.headerBorderWidth || 1.5;
    this.headerFont = options.headerFont || 'Helvetica-Bold';
    this.headerItalic = options.headerItalic || false;
    this.headerFontSize = options.headerFontSize || 14;
    const defaultMargins = {
      bottom: 30,
      left: 0,
      right: 0,
      top: 0,
    };
    this.margins = options.margins || defaultMargins;

    this.cellEmptyColor = options.cellEmptyColor || '#FFFFFF';
    this.cellEmptyFontSize = options.cellEmptyFontSize || this.cellFontSize;
    this.cellEmptyItalic = options.cellEmptyItalic || this.cellItalic;
    this.cellEmptyText = options.cellEmptyText || 'none';

    this.headerEmptyColor = options.headerEmptyColor || '#FFFFFF';
    this.headerEmptyFontSize = options.headerEmptyFontSize || this.headerFontSize;
    this.headerEmptyItalic = options.headerEmptyItalic || this.headerItalic;
    this.headerEmptyText = options.headerEmptyText || 'none';
    this.headerAllowWrap = options.headerAllowWrap || false;
    this.cellAllowWrap = options.cellAllowWrap || false;

    this.skewFactor = Math.tan((-15 * Math.PI) / 180);
    this.columns = columns;
  }

  /**
    Add a table to the PDF document.
    @param {PDFBuilder} builder
  */
  addToPDF(builder) {
    const { doc } = builder;

    this.builder = builder;
    this.currentPage = doc.bufferedPageRange().count - 1;
    this.currentPosition = { x: builder.pageLeft(), y: doc.y };
    this.startingPosition = { x: builder.pageLeft(), y: doc.y };
    this.startingPage = doc.bufferedPageRange().count - 1;

    for (let i = 0; i < this.columns.length; i += 1) {
      this.currentPage = this.startingPage;
      this.currentPosition.y = this.startingPosition.y;
      doc.switchToPage(this.currentPage);
      this._addColumn(this.columns[i]);
    }

    doc.y += this.margins.bottom;
  }

  /**
    Returns the full width of the table based on the page size and margins.
    @return {number}
  */
  width() {
    return this.builder.contentWidth();
  }


  /**
    Ouput a column from the table to the PDF.
    @param {Object} column
    @private
  */
  _addColumn(column) {
    this.columnWidth = this._columnWidth(column.width);

    // Add Header Row
    this._addHeader(column);

    // Add Data Rows
    for (let i = 0; i < column.rows.length; i += 1) {
      this._addRow(column, column.rows[i]);
    }

    // Update position for the next column
    this.currentPosition.x += this.columnWidth;
  }


  /**
    Ouput the column header to the PDF.
    @param {Object} column
    @param {Number} width
    @private
  */
  _addHeader(column) {
    const { doc } = this.builder;
    const { header } = column;
    const align = header.align || this.headerAlign;
    const borderColor = header.borderColor || this.headerBorderColor;
    // const borderStyle = header.borderStyle || this.headerBorderStyle;
    const borderWidth = header.borderWidth || this.headerBorderWidth;
    let color = header.color || this.headerColor;
    const font = header.font || this.headerFont;
    let italic = header.italic || this.headerItalic;
    let fontSize = header.fontSize || this.headerFontSize;
    const lineGap = this._headerLineGap(header);
    const position = this.currentPosition;
    const width = this.columnWidth;
    let { text } = header;
    const emptyColor = header.emptyColor || this.headerEmptyColor;
    const emptyFontSize = header.emptyFontSize || this.headerEmptyFontSize;
    const emptyItalic = header.emptyItalic || this.headerEmptyItalic;
    const emptyText = header.emptyText || this.headerEmptyText;
    const allowWrap = header.allowWrap || this.headerAllowWrap;

    if (text === undefined || text === null || text.length < 1) {
      color = emptyColor;
      fontSize = emptyFontSize;
      italic = emptyItalic;
      text = emptyText;
    }

    doc.save();
    doc.fillColor(color);
    doc.font(font);
    doc.fontSize(fontSize);
    if (italic) {
      doc.transform(1, 0, this.skewFactor, 1, (-position.y * this.skewFactor) + 1, 0);
    }
    if (allowWrap) {
      doc.text(text, position.x, position.y, {
        lineBreak: false,
        align,
        lineGap,
        width,
      });
    } else {
      doc.text(text, position.x, position.y, {
        lineBreak: false,
        align,
        lineGap,
        width,
        height: lineGap,
        ellipsis: true,
      });
    }
    doc.restore();

    doc
      .moveTo(doc.x, doc.y - (fontSize * 0.85))
      .lineWidth(borderWidth)
      .lineTo(doc.x + width, doc.y - (fontSize * 0.85))
      .strokeColor(borderColor)
      .stroke();
  }

  _addRow(column, row) {
    const { doc } = this.builder;
    const pageCount = doc.bufferedPageRange().count;

    // Repeat the header if a new page is needed
    const newPage = this.builder.addPageIfNeeded(this.currentPage, pageCount);
    if (newPage) {
      this.currentPage += 1;
      this.currentPosition.y = doc.y;
      this._addHeader(column, column.header);
    }

    const align = row.align || this.cellAlign;
    const borderColor = row.borderColor || this.cellBorderColor;
    const borderStyle = row.borderStyle || this.cellBorderStyle;
    const borderWidth = row.borderWidth || this.cellBorderWidth;
    let italic = row.italic || this.cellItalic;
    let color = row.color || this.cellColor;
    const font = row.font || this.cellFont;
    let fontSize = row.fontSize || this.cellFontSize;

    const allowWrap = row.allowWrap || this.cellAllowWrap;

    const emptyColor = row.emptyColor || this.cellEmptyColor;
    const emptyFontSize = row.emptyFontSize || this.cellEmptyFontSize;
    const emptyItalic = row.emptyItalic || this.cellEmptyItalic;
    const emptyText = row.emptyText || this.cellEmptyText;

    let { text } = row;

    const lineGap = this._rowLineGap(row);
    const width = this.columnWidth;
    if (text === undefined || text === null || text.length < 1) {
      color = emptyColor;
      fontSize = emptyFontSize;
      italic = emptyItalic;
      text = emptyText;
    }

    doc.save();
    doc.fillColor(color);
    doc.font(font);
    doc.fontSize(fontSize);
    if (italic) {
      doc.transform(1, 0, this.skewFactor, 1, (-doc.y * this.skewFactor) + 1, 0);
    }

    if (allowWrap) {
      doc.text(text, {
        lineBreak: false,
        align,
        lineGap,
        width,
      });
    } else {
      doc.text(text, {
        lineBreak: false,
        align,
        lineGap,
        width,
        height: lineGap,
        ellipsis: true,
      });
    }
    doc.restore();

    if (borderStyle === 'none') {
      return;
    }

    doc
      .moveTo(doc.x, doc.y - (fontSize * 0.7))
      .lineWidth(borderWidth)
      .lineTo(doc.x + width, doc.y - (fontSize * 0.7))
      .strokeColor(borderColor)
      .stroke();
  }

  /**
    Ouput the column header to the PDF.
    @param {Number} width
    @return {Number}
    @private
  */
  _columnWidth(width) {
    if (this.columns.length < 2) {
      return this.width();
    }

    if (width == null) {
      return this.width() / this.columns.length;
    }

    if (width < 1) {
      return this.width() * width;
    }

    return width;
  }

  _headerLineGap(header) {
    const fontSize = header.fontSize || this.headerFontSize;
    return fontSize * 1.2;
  }

  _rowLineGap(row) {
    const fontSize = row.fontSize || this.cellFontSize;
    return fontSize * 1.07;
  }
}

export default PDFTable;