'use strict' const t = require('typical') const arrayify = require('array-back') const Column = require('./column') const wrap = require('wordwrapjs') const Cell = require('./cell') const ansi = require('./ansi') const _maxWidth = new WeakMap() /** * @module columns */ class Columns { constructor (columns) { this.list = [] arrayify(columns).forEach(this.add.bind(this)) } /** * sum of all generatedWidth fields * @return {number} */ totalWidth () { return this.list.length ? this.list.map(col => col.generatedWidth).reduce((a, b) => a + b) : 0 } totalFixedWidth () { return this.getFixed() .map(col => col.generatedWidth) .reduce((a, b) => a + b, 0) } get (columnName) { return this.list.find(column => column.name === columnName) } getResizable () { return this.list.filter(column => column.isResizable()) } getFixed () { return this.list.filter(column => column.isFixed()) } add (column) { const col = column instanceof Column ? column : new Column(column) this.list.push(col) return col } set maxWidth (val) { _maxWidth.set(this, val) } /** * sets `generatedWidth` for each column * @chainable */ autoSize () { const maxWidth = _maxWidth.get(this) /* size */ this.list.forEach(column => { column.generateWidth() column.generateMinWidth() }) /* adjust if user set a min or maxWidth */ this.list.forEach(column => { if (t.isDefined(column.maxWidth) && column.generatedWidth > column.maxWidth) { column.generatedWidth = column.maxWidth } if (t.isDefined(column.minWidth) && column.generatedWidth < column.minWidth) { column.generatedWidth = column.minWidth } }) const width = { total: this.totalWidth(), view: maxWidth, diff: this.totalWidth() - maxWidth, totalFixed: this.totalFixedWidth(), totalResizable: Math.max(maxWidth - this.totalFixedWidth(), 0) } /* adjust if short of space */ if (width.diff > 0) { /* share the available space between resizeable columns */ let resizableColumns = this.getResizable() resizableColumns.forEach(column => { column.generatedWidth = Math.floor(width.totalResizable / resizableColumns.length) }) /* at this point, the generatedWidth should never end up bigger than the contentWidth */ const grownColumns = this.list.filter(column => column.generatedWidth > column.contentWidth) const shrunkenColumns = this.list.filter(column => column.generatedWidth < column.contentWidth) let salvagedSpace = 0 grownColumns.forEach(column => { const currentGeneratedWidth = column.generatedWidth column.generateWidth() salvagedSpace += currentGeneratedWidth - column.generatedWidth }) shrunkenColumns.forEach(column => { column.generatedWidth += Math.floor(salvagedSpace / shrunkenColumns.length) }) /* if, after autosizing, we still don't fit within maxWidth then give up */ } return this } /** * Factory method returning all distinct columns from input * @param {object[]} - input recordset * @return {module:columns} */ static getColumns (rows) { var columns = new Columns() arrayify(rows).forEach(row => { for (let columnName in row) { let column = columns.get(columnName) if (!column) { column = columns.add({ name: columnName, contentWidth: 0, minContentWidth: 0 }) } let cell = new Cell(row[columnName], column) let cellValue = cell.value if (ansi.has(cellValue)) { cellValue = ansi.remove(cellValue) } if (cellValue.length > column.contentWidth) column.contentWidth = cellValue.length let longestWord = getLongestWord(cellValue) if (longestWord > column.minContentWidth) { column.minContentWidth = longestWord } if (!column.contentWrappable) column.contentWrappable = wrap.isWrappable(cellValue) } }) return columns } } function getLongestWord (line) { const words = wrap.getChunks(line) return words.reduce((max, word) => { return Math.max(word.length, max) }, 0) } module.exports = Columns