diff --git a/README.md b/README.md index b20946b..65e5e76 100644 --- a/README.md +++ b/README.md @@ -1076,6 +1076,41 @@ A stylized table of text elements. ]); ``` + +#### ListTable (from Box) + +A stylized table of text elements with a list. + +##### Options: + +- inherits all from Box. +- __rows/data__ - array of array of strings representing rows. +- __pad__ - spaces to attempt to pad on the sides of each cell. `2` by default: + one space on each side. +- __style.header__ - header style. +- __style.cell__ - cell style. + +##### Properties: + +- inherits all from Box. + +##### Events: + +- inherits all from Box. + +##### Methods: + +- inherits all from Box. +- __setRows/setData(rows)__ - set rows in table. array of arrays of strings. + ``` + table.setData([ + [ 'Animals', 'Foods' ], + [ 'Elephant', 'Apple' ], + [ 'Bird', 'Orange' ] + ]); + ``` + + #### Terminal (from Box) A box which spins up a pseudo terminal and renders the output. Useful for diff --git a/lib/widget.js b/lib/widget.js index 5fdcd73..c9e7fc8 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -4658,6 +4658,12 @@ List.prototype.appendItem = function(item) { options.right = this.iright + (this.scrollbar ? 1 : 0); } + // if (this.shrink) { + if (this.shrink && this.options.normalShrink) { + delete options.right; + options.width = 'shrink'; + } + ['bg', 'fg', 'bold', 'underline', 'blink', 'inverse', 'invisible'].forEach(function(name) { options[name] = function() { @@ -6783,7 +6789,6 @@ Log.prototype.add = function(text) { /** * Table - * TODO: Draw custom border with proper angles. */ function Table(options) { @@ -6795,6 +6800,11 @@ function Table(options) { 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'; Box.call(this, options); @@ -6804,10 +6814,16 @@ function Table(options) { ? options.pad : 2; - this.__align = options.align || 'center'; - delete this.align; - this.setData(options.rows || options.data); + + this.screen.on('resize', function() { + var rows = self.rows; + // XXX Need to clear previous box. + self.setData([]); + self.screen.render(); + self.setData(rows); + self.screen.render(); + }); } Table.prototype.__proto__ = Box.prototype; @@ -6820,16 +6836,20 @@ Table.prototype.setData = function(rows) { , text = '' , maxes = [] , total = 0 - , line = ''; + , line = '' + , align = this.align; - // TODO Pregenerate `generateTags` calls here! + var sborder = generateTags(this.style.border) + , sheader = generateTags(this.style.header) + , scell = generateTags(this.style.cell); this.rows = rows || []; this.rows.forEach(function(row) { row.forEach(function(cell, i) { - if (!maxes[i] || maxes[i] < self._cellLength(cell) + self.pad) { - maxes[i] = self._cellLength(cell) + self.pad; + var clen = self._cellLength(cell); + if (!maxes[i] || maxes[i] < clen + self.pad) { + maxes[i] = clen + self.pad; } }); }); @@ -6838,48 +6858,54 @@ Table.prototype.setData = function(rows) { total += cell + 1 + self.pad; }); - // TODO Use these values to render the border - // elsewhere, in the usual border rendering. this._maxes = maxes; - maxes.forEach(function(width, i) { - if (i !== 0) { - line += '\u253c'; // '┼' - } - for (var i = 0; i < width; i++) { - line += '\u2500'; // '─' - } - }); - line = generateTags(self.style.border, line); + // maxes.forEach(function(width, i) { + // if (i !== 0) { + // line += '\u253c'; // '┼' + // } + // for (var i = 0; i < width; i++) { + // line += '\u2500'; // '─' + // } + // }); + // line = sborder.open + line + sborder.close; this.rows.forEach(function(row, i) { var isHeader = i === 0; var isFooter = i === self.rows.length - 1; row.forEach(function(cell, i) { var width = maxes[i]; + var clen = self._cellLength(cell); if (i !== 0) { - text += generateTags(self.style.border, '\u2502'); // '│' + // text += sborder.open + '\u2502' + sborder.close; // '│' + text += ' '; } - while (self._cellLength(cell) < width) { - if (self.__align === 'center') { + while (clen < width) { + if (align === 'center') { cell = ' ' + cell + ' '; - } else if (self.__align === 'left') { + clen += 2; + } else if (align === 'left') { cell = cell + ' '; - } else if (self.__align === 'right') { + clen += 1; + } else if (align === 'right') { cell = ' ' + cell; + clen += 1; } } - if (self._cellLength(cell) > width) { - if (self.__align === 'center') { + if (clen > width) { + if (align === 'center') { // cell = cell.slice(0, -1); cell = cell.substring(1); - } else if (self.__align === 'left') { + clen--; + } else if (align === 'left') { cell = cell.slice(0, -1); - } else if (self.__align === 'right') { + clen--; + } else if (align === 'right') { cell = cell.substring(1); + clen--; } } @@ -6889,16 +6915,17 @@ Table.prototype.setData = function(rows) { } if (isHeader) { - cell = generateTags(self.style.header, cell); + cell = sheader.open + cell + sheader.close; } else { - cell = generateTags(self.style.cell, cell); + cell = scell.open + cell + scell.close; } text += cell; }); text += '\n'; if (!isFooter) { - text += line + '\n'; + // text += line + '\n'; + text += '\n'; } }); @@ -6906,7 +6933,9 @@ Table.prototype.setData = function(rows) { text = text.slice(0, -1); } - return this.setContent(text); + delete this.align; + this.setContent(text); + this.align = align; }; Table.prototype._cellLength = function(text) { @@ -6916,6 +6945,362 @@ Table.prototype._cellLength = function(text) { return text.replace(/{(\/?)([\w\-,;!#]*)}/g, '').length; }; +Table.prototype.render = function() { + var self = this; + + var coords = this._render(); + if (!coords) return; + + if (!this._maxes) return coords; + + var lines = this.screen.lines + , xi = coords.xi + , xl = coords.xl + , yi = coords.yi + , yl = coords.yl + , rx + , ry + , i; + + var header = this.sattr( + this.style.header, + this.style.header.fg, + this.style.header.bg); + + var cell = this.sattr( + this.style.cell, + this.style.cell.fg, + this.style.cell.bg); + + var battr = this.sattr( + this.style.border, + this.style.border.fg, + this.style.border.bg); + + var width = coords.xl - coords.xi - this.iwidth / 2 + , height = coords.yl - coords.yi - this.iheight / 2; + + // Apply attributes to header cells and cells. + // Problem: Does not allow for tags in cells. + // for (var y = this.iheight / 2; y < height; y++) { + // for (var x = this.iwidth / 2; x < width; x++) { + // if (y === this.iheight / 2) { + // lines[yi + y][xi + x][0] = header; + // } else { + // lines[yi + y][xi + x][0] = cell; + // } + // } + // } + + if (!this.border) return coords; + + // Draw border with correct angles. + ry = 0; + for (i = 0; i < self.rows.length + 1; i++) { + rx = 0; + self._maxes.forEach(function(max, i) { + rx += max; + if (i === 0) { + // left side + if (ry === 0) { + // top + lines[yi + ry][xi + 0][0] = battr; + lines[yi + ry][xi + 0][1] = '\u250c'; // '┌' + } else if (ry / 2 === self.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'; // '├' + } + } else if (i === self.rows.length - 1) { + // right side + if (ry === 0) { + // top + lines[yi + ry][xi + ++rx][0] = battr; + lines[yi + ry][xi + rx][1] = '\u2510'; // '┐' + } else if (ry / 2 === self.rows.length) { + // bottom + lines[yi + ry][xi + ++rx][0] = battr; + lines[yi + ry][xi + rx][1] = '\u2518'; // '┘' + } else { + // middle + lines[yi + ry][xi + ++rx][0] = battr; + lines[yi + ry][xi + rx][1] = '\u2524'; // '┤' + } + return; + } + // center + if (ry === 0) { + // top + lines[yi + ry][xi + ++rx][0] = battr; + lines[yi + ry][xi + rx][1] = '\u252c'; // '┬' + } else if (ry / 2 === self.rows.length) { + // bottom + lines[yi + ry][xi + ++rx][0] = battr; + lines[yi + ry][xi + rx][1] = '\u2534'; // '┴' + } else { + // middle + lines[yi + ry][xi + ++rx][0] = battr; + lines[yi + ry][xi + rx][1] = '\u253c'; // '┼' + // ++rx; + } + }); + ry += 2; + } + + // Draw internal borders. + for (ry = 1; ry < self.rows.length * 2; ry++) { + rx = 0; + self._maxes.slice(0, -1).forEach(function(max, i) { + rx += max; + if (ry % 2 !== 0) { + lines[yi + ry][xi + ++rx][0] = battr; + lines[yi + ry][xi + rx][1] = '\u2502'; // '│' + } else { + rx++; + } + }); + rx = 1; + self._maxes.forEach(function(max, i) { + while (max--) { + if (ry % 2 === 0) { + lines[yi + ry][xi + rx][0] = battr; + lines[yi + ry][xi + rx][1] = '\u2500'; // '─' + } + rx++; + } + // if (ry % 2 === 0 && i !== self._maxes.length - 1) { + // lines[yi + ry][xi + rx][0] = battr; + // lines[yi + ry][xi + rx][1] = '\u253c'; // '┼' + // } + rx++; + }); + } + + return coords; +}; + +/** + * ListTable + */ + +function ListTable(options) { + var self = this; + + if (!(this instanceof Node)) { + return new ListTable(options); + } + + options = options || {}; + options.width = 'shrink'; + options.normalShrink = true; + options.style = options.style || {}; + options.style.border = options.style.border || {}; + options.style.header = options.style.header || {}; + options.style.cell = options.style.cell || {}; + this.__align = options.align || 'center'; + delete options.align; + + options.style.selected = options.style.cell.selected; + options.style.item = options.style.cell; + + List.call(this, options); + + this._header = new Box({ + parent: this, + left: this.screen.autoPadding ? 0 : this.ileft, + top: 0, + width: 'shrink', + height: 1, + style: options.style.header, + tags: options.parseTags || options.tags + }); + + this.on('scroll', function() { + self._header.setFront(); + var visible = self.height - self.iheight; + self._header.rtop = 1 + self.childBase - (self.border ? 1 : 0); + if (!self.screen.autoPadding) { + self._header.rtop = 1 + self.childBase; + } + // self.screen.render(); + }); + + this.pad = options.pad != null + ? options.pad + : 2; + + this.setData(options.rows || options.data); +} + +ListTable.prototype.__proto__ = List.prototype; + +ListTable.prototype.type = 'list-table'; + +ListTable.prototype.setRows = +ListTable.prototype.setData = function(rows) { + var self = this + , maxes = [] + , total = 0 + , align = this.__align; + + this.clearItems(); + + this.rows = rows || []; + + this.rows.forEach(function(row) { + row.forEach(function(cell, i) { + var clen = self._cellLength(cell); + if (!maxes[i] || maxes[i] < clen + self.pad) { + maxes[i] = clen + self.pad; + } + }); + }); + + maxes.forEach(function(cell) { + total += cell + 1 + self.pad; + }); + + this._maxes = maxes; + + this.addItem(''); + + this.rows.forEach(function(row, i) { + var isHeader = i === 0; + var isFooter = i === self.rows.length - 1; + var text = ''; + row.forEach(function(cell, i) { + var width = maxes[i]; + var clen = self._cellLength(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 (isHeader) { + self._header.setContent(text); + } else { + self.addItem(text); + } + }); + + this._header.setFront(); + + this.select(0); +}; + +ListTable.prototype._select = ListTable.prototype.select; +ListTable.prototype.select = function(i) { + if (i === 0) { + i = 1; + } + if (i <= 1) { + this.setScroll(0); + } + return this._select(i); +}; + +ListTable.prototype._cellLength = function(text) { + if (!this.options.tags && !this.options.parseTags) { + return text.length; + } + return text.replace(/{(\/?)([\w\-,;!#]*)}/g, '').length; +}; + +ListTable.prototype.render = function() { + var self = this; + + var coords = this._render(); + if (!coords) return; + + if (!this._maxes) return coords; + + var lines = this.screen.lines + , xi = coords.xi + , xl = coords.xl + , yi = coords.yi + , yl = coords.yl + , rx + , ry + , i; + + var battr = this.sattr( + this.style.border, + this.style.border.fg, + this.style.border.bg); + + var width = coords.xl - coords.xi - this.iwidth / 2 + , height = coords.yl - coords.yi - this.iheight / 2; + + if (!this.border) return coords; + + // Draw border with correct angles. + ry = 0; + for (i = 0; i < height + 1; i++) { + rx = 0; + self._maxes.slice(0, -1).forEach(function(max, i) { + rx += max; + // center + if (ry === 0) { + // top + lines[yi + ry][xi + ++rx][0] = battr; + lines[yi + ry][xi + rx][1] = '\u252c'; // '┬' + } else if (ry === height) { + // bottom + lines[yi + ry][xi + ++rx][0] = battr; + lines[yi + ry][xi + rx][1] = '\u2534'; // '┴' + } else { + // middle + ++rx; + } + }); + ry += 1; + } + + // Draw internal borders. + for (ry = 1; ry < height; ry++) { + rx = 0; + self._maxes.slice(0, -1).forEach(function(max, i) { + rx += max; + lines[yi + ry][xi + ++rx][0] = battr; + lines[yi + ry][xi + rx][1] = '\u2502'; // '│' + }); + } + + return coords; +}; + /** * Terminal */ @@ -7658,7 +8043,7 @@ function generateTags(style, text) { open = '{' + val + '-' + key + '}' + open; close += '{/' + val + '-' + key + '}'; } else { - if (val) { + if (val === true) { open = '{' + key + '}' + open; close += '{/' + key + '}'; } @@ -7798,6 +8183,7 @@ exports.Listbar = exports.listbar = Listbar; exports.Log = exports.log = Log; exports.Table = exports.table = Table; +exports.ListTable = exports.listtable = ListTable; exports.Terminal = exports.terminal = Terminal; exports.Image = exports.image = Image; diff --git a/test/widget-listtable.js b/test/widget-listtable.js new file mode 100644 index 0000000..753cc06 --- /dev/null +++ b/test/widget-listtable.js @@ -0,0 +1,54 @@ +var blessed = require('../') + , screen; + +screen = blessed.screen({ + dump: __dirname + '/logs/listtable.log', + autoPadding: false +}); + +var table = blessed.listtable({ + parent: screen, + top: 'center', + left: 'center', + data: null, + border: 'line', + align: 'center', + tags: true, + keys: true, + height: 4, + vi: true, + mouse: true, + style: { + border: { + fg: 'red' + }, + header: { + fg: 'blue', + bold: true + }, + cell: { + fg: 'magenta', + selected: { + bg: 'blue' + } + } + } +}); + +var data = [ + [ 'Animals', 'Foods', 'Times' ], + [ 'Elephant', 'Apple', '1:00am' ], + [ 'Bird', 'Orange', '2:15pm' ] +]; + +data[1][0] = '{red-fg}' + data[1][0] + '{/red-fg}'; + +table.setData(data); + +table.focus(); + +screen.key('q', function() { + return process.exit(0); +}); + +screen.render(); diff --git a/test/widget-table.js b/test/widget-table.js index 1f19e1b..8cccf43 100644 --- a/test/widget-table.js +++ b/test/widget-table.js @@ -3,7 +3,7 @@ var blessed = require('../') screen = blessed.screen({ dump: __dirname + '/logs/table.log', - autoPadding: true + autoPadding: false }); var table = blessed.table({ @@ -13,6 +13,7 @@ var table = blessed.table({ data: null, border: 'line', align: 'center', + tags: true, style: { border: { fg: 'red' @@ -27,11 +28,15 @@ var table = blessed.table({ } }); -table.setData([ +var data = [ [ 'Animals', 'Foods', 'Times' ], [ 'Elephant', 'Apple', '1:00am' ], [ 'Bird', 'Orange', '2:15pm' ] -]); +]; + +data[1][0] = '{red-fg}' + data[1][0] + '{/red-fg}'; + +table.setData(data); screen.key('q', function() { return process.exit(0);