UNPKG

neo-neo-bblessed

Version:

A fork of neo-blessed (which is a fork of blessed) with bug fixes and maintenance.

446 lines (390 loc) 11.7 kB
/** * table.js - table element for blessed * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License). * https://github.com/chjj/blessed */ /** * Modules */ import Node from './node.js'; import boxFactory from './box.js'; const Box = boxFactory.Box; /** * Interfaces */ interface TableStyle { border?: any; header?: any; cell?: any; [key: string]: any; } interface TableBorder { left?: boolean; right?: boolean; top?: boolean; bottom?: boolean; } interface TableOptions { shrink?: boolean; style?: TableStyle; align?: 'left' | 'center' | 'right'; height?: number; pad?: number; rows?: string[][]; data?: string[][]; noCellBorders?: boolean; fillCellBorders?: boolean; [key: string]: any; } interface TablePosition { width?: number; [key: string]: any; } interface TableScreen { lines: any[][]; render(): void; } interface TableCoords { xi: number; xl: number; yi: number; yl: number; } interface TableInterface extends Box { type: string; pad: number; rows: string[][]; _maxes?: number[]; align: 'left' | 'center' | 'right'; style: TableStyle; position: TablePosition; width: number; screen: TableScreen; border?: TableBorder; options: TableOptions; detached: boolean; iright: number; ibottom: number; itop: number; ileft: number; iheight: number; // Methods _calculateMaxes(): number[]; setRows(rows: string[][]): void; setData(rows: string[][]): void; render(): TableCoords | undefined; _render(): TableCoords | undefined; strWidth(str: string): number; setContent(content: string): void; sattr(style: any): number; on(event: string, listener: Function): void; } /** * Table - Modern ES6 Class */ class Table extends Box { type = 'table'; pad: number; rows: string[][]; _maxes?: number[]; align: 'left' | 'center' | 'right'; constructor(options?: TableOptions) { // Handle malformed options gracefully if (!options || typeof options !== 'object' || Array.isArray(options)) { options = {}; } options.shrink = true; options.style = options.style || {}; options.style.border = options.style.border || {}; options.style.header = options.style.header || {}; options.style.cell = options.style.cell || {}; options.align = options.align || 'center'; // Regular tables do not get custom height (this would // require extra padding). Maybe add in the future. delete options.height; super(options); this.pad = options.pad != null ? options.pad : 2; this.rows = []; this.align = options.align || 'center'; this.setData(options.rows || options.data); this.on('attach', () => { this.setContent(''); this.setData(this.rows); }); this.on('resize', () => { this.setContent(''); this.setData(this.rows); this.screen.render(); }); } _calculateMaxes(): number[] { var maxes: number[] = []; if (this.detached) return; this.rows = this.rows || []; this.rows.forEach((row: string[]) => { row.forEach((cell: string, i: number) => { var clen = this.strWidth(cell); if (!maxes[i] || maxes[i] < clen) { maxes[i] = clen; } }); }); var total = maxes.reduce((total: number, max: number) => { return total + max; }, 0); total += maxes.length + 1; // XXX There might be an issue with resizing where on the first resize event // width appears to be less than total if it's a percentage or left/right // combination. if (this.width < total) { delete this.position.width; } if (this.position.width != null) { var missing = this.width - total; var w = (missing / maxes.length) | 0; var wr = missing % maxes.length; maxes = maxes.map((max: number, i: number) => { if (i === maxes.length - 1) { return max + w + wr; } return max + w; }); } else { maxes = maxes.map((max: number) => { return max + this.pad; }); } return (this._maxes = maxes); } setRows(rows?: string[][]): void { this.setData(rows); } setData(rows?: string[][]): void { var text = '', align = this.align; this.rows = rows || []; this._calculateMaxes(); if (!this._maxes) return; this.rows.forEach((row: string[], i: number) => { var isFooter = i === this.rows.length - 1; row.forEach((cell: string, i: number) => { var width = this._maxes[i]; var clen = this.strWidth(cell); if (i !== 0) { text += ' '; } while (clen < width) { if (align === 'center') { cell = ' ' + cell + ' '; clen += 2; } else if (align === 'left') { cell = cell + ' '; clen += 1; } else if (align === 'right') { cell = ' ' + cell; clen += 1; } } if (clen > width) { if (align === 'center') { cell = cell.substring(1); clen--; } else if (align === 'left') { cell = cell.slice(0, -1); clen--; } else if (align === 'right') { cell = cell.substring(1); clen--; } } text += cell; }); if (!isFooter) { text += '\n\n'; } }); delete this.align; this.setContent(text); this.align = align; } render(): TableCoords | undefined { var coords = this._render(); if (!coords) return; this._calculateMaxes(); if (!this._maxes) return coords; var lines = this.screen.lines, xi = coords.xi, yi = coords.yi, rx, ry, i; var dattr = this.sattr(this.style), hattr = this.sattr(this.style.header), cattr = this.sattr(this.style.cell), battr = this.sattr(this.style.border); var width = coords.xl - coords.xi - this.iright, height = coords.yl - coords.yi - this.ibottom; // Apply attributes to header cells and cells. for (var y = this.itop; y < height; y++) { if (!lines[yi + y]) break; for (var x = this.ileft; x < width; x++) { if (!lines[yi + y][xi + x]) break; // Check to see if it's not the default attr. Allows for tags: if (lines[yi + y][xi + x][0] !== dattr) continue; if (y === this.itop) { lines[yi + y][xi + x][0] = hattr; } else { lines[yi + y][xi + x][0] = cattr; } lines[yi + y].dirty = true; } } if (!this.border || this.options.noCellBorders) return coords; // Draw border with correct angles. ry = 0; for (i = 0; i < this.rows.length + 1; i++) { if (!lines[yi + ry]) break; rx = 0; this._maxes.forEach((max: number, i: number) => { rx += max; if (i === 0) { if (!lines[yi + ry][xi + 0]) return; // left side if (ry === 0) { // top lines[yi + ry][xi + 0][0] = battr; // lines[yi + ry][xi + 0][1] = '\u250c'; // '┌' } else if (ry / 2 === this.rows.length) { // bottom lines[yi + ry][xi + 0][0] = battr; // lines[yi + ry][xi + 0][1] = '\u2514'; // '└' } else { // middle lines[yi + ry][xi + 0][0] = battr; lines[yi + ry][xi + 0][1] = '\u251c'; // '├' // XXX If we alter iwidth and ileft for no borders - nothing should be written here if (!this.border.left) { lines[yi + ry][xi + 0][1] = '\u2500'; // '─' } } lines[yi + ry].dirty = true; } else if (i === this._maxes.length - 1) { if (!lines[yi + ry][xi + rx + 1]) return; // right side if (ry === 0) { // top rx++; lines[yi + ry][xi + rx][0] = battr; // lines[yi + ry][xi + rx][1] = '\u2510'; // '┐' } else if (ry / 2 === this.rows.length) { // bottom rx++; lines[yi + ry][xi + rx][0] = battr; // lines[yi + ry][xi + rx][1] = '\u2518'; // '┘' } else { // middle rx++; lines[yi + ry][xi + rx][0] = battr; lines[yi + ry][xi + rx][1] = '\u2524'; // '┤' // XXX If we alter iwidth and iright for no borders - nothing should be written here if (!this.border.right) { lines[yi + ry][xi + rx][1] = '\u2500'; // '─' } } lines[yi + ry].dirty = true; return; } if (!lines[yi + ry][xi + rx + 1]) return; // center if (ry === 0) { // top rx++; lines[yi + ry][xi + rx][0] = battr; lines[yi + ry][xi + rx][1] = '\u252c'; // '┬' // XXX If we alter iheight and itop for no borders - nothing should be written here if (!this.border.top) { lines[yi + ry][xi + rx][1] = '\u2502'; // '│' } } else if (ry / 2 === this.rows.length) { // bottom rx++; lines[yi + ry][xi + rx][0] = battr; lines[yi + ry][xi + rx][1] = '\u2534'; // '┴' // XXX If we alter iheight and ibottom for no borders - nothing should be written here if (!this.border.bottom) { lines[yi + ry][xi + rx][1] = '\u2502'; // '│' } } else { // middle if (this.options.fillCellBorders) { var lbg = (ry <= 2 ? hattr : cattr) & 0x1ff; rx++; lines[yi + ry][xi + rx][0] = (battr & ~0x1ff) | lbg; } else { rx++; lines[yi + ry][xi + rx][0] = battr; } lines[yi + ry][xi + rx][1] = '\u253c'; // '┼' // rx++; } lines[yi + ry].dirty = true; }); ry += 2; } // Draw internal borders. for (ry = 1; ry < this.rows.length * 2; ry++) { if (!lines[yi + ry]) break; rx = 0; this._maxes.slice(0, -1).forEach((max: number) => { rx += max; if (!lines[yi + ry][xi + rx + 1]) return; if (ry % 2 !== 0) { if (this.options.fillCellBorders) { var lbg = (ry <= 2 ? hattr : cattr) & 0x1ff; rx++; lines[yi + ry][xi + rx][0] = (battr & ~0x1ff) | lbg; } else { rx++; lines[yi + ry][xi + rx][0] = battr; } lines[yi + ry][xi + rx][1] = '\u2502'; // '│' lines[yi + ry].dirty = true; } else { rx++; } }); rx = 1; this._maxes.forEach((max: number) => { while (max--) { if (ry % 2 === 0) { if (!lines[yi + ry]) break; if (!lines[yi + ry][xi + rx + 1]) break; if (this.options.fillCellBorders) { var lbg = (ry <= 2 ? hattr : cattr) & 0x1ff; lines[yi + ry][xi + rx][0] = (battr & ~0x1ff) | lbg; } else { lines[yi + ry][xi + rx][0] = battr; } lines[yi + ry][xi + rx][1] = '\u2500'; // '─' lines[yi + ry].dirty = true; } rx++; } rx++; }); } return coords; } } /** * Factory function for backward compatibility */ function table(options?: TableOptions): TableInterface { return new Table(options) as TableInterface; } // Attach the class as a property for direct access table.Table = Table; /** * Expose */ export default table;