/** * list.js - list element for blessed * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License). * https://github.com/chjj/blessed */ /** * Modules */ var helpers = require('../helpers'); var Node = require('./node'); var Box = require('./box'); /** * List */ function List(options) { var self = this; if (!(this instanceof Node)) { return new List(options); } options = options || {}; options.ignoreKeys = true; // Possibly put this here: this.items = []; options.scrollable = true; Box.call(this, options); this.value = ''; this.items = []; this.ritems = []; this.selected = 0; this._isList = true; if (!this.style.selected) { this.style.selected = {}; this.style.selected.bg = options.selectedBg; this.style.selected.fg = options.selectedFg; this.style.selected.bold = options.selectedBold; this.style.selected.underline = options.selectedUnderline; this.style.selected.blink = options.selectedBlink; this.style.selected.inverse = options.selectedInverse; this.style.selected.invisible = options.selectedInvisible; } if (!this.style.item) { this.style.item = {}; this.style.item.bg = options.itemBg; this.style.item.fg = options.itemFg; this.style.item.bold = options.itemBold; this.style.item.underline = options.itemUnderline; this.style.item.blink = options.itemBlink; this.style.item.inverse = options.itemInverse; this.style.item.invisible = options.itemInvisible; } // Legacy: for apps written before the addition of item attributes. ['bg', 'fg', 'bold', 'underline', 'blink', 'inverse', 'invisible'].forEach(function(name) { if (self.style[name] != null && self.style.item[name] == null) { self.style.item[name] = self.style[name]; } }); if (this.options.itemHoverBg) { this.options.itemHoverEffects = { bg: this.options.itemHoverBg }; } if (this.options.itemHoverEffects) { this.style.item.hover = this.options.itemHoverEffects; } if (this.options.itemFocusEffects) { this.style.item.focus = this.options.itemFocusEffects; } this.interactive = options.interactive !== false; this.mouse = options.mouse || false; if (options.items) { this.ritems = options.items; options.items.forEach(this.add.bind(this)); } this.select(0); if (options.mouse) { this.screen._listenMouse(this); this.on('element wheeldown', function(el, data) { self.select(self.selected + 2); self.screen.render(); }); this.on('element wheelup', function(el, data) { self.select(self.selected - 2); self.screen.render(); }); } if (options.keys) { this.on('keypress', function(ch, key) { if (key.name === 'up' || (options.vi && key.name === 'k')) { self.up(); self.screen.render(); return; } if (key.name === 'down' || (options.vi && key.name === 'j')) { self.down(); self.screen.render(); return; } if (key.name === 'enter' || (options.vi && key.name === 'l' && !key.shift)) { self.enterSelected(); return; } if (key.name === 'escape' || (options.vi && key.name === 'q')) { self.cancelSelected(); return; } if (options.vi && key.name === 'u' && key.ctrl) { self.move(-((self.height - self.iheight) / 2) | 0); self.screen.render(); return; } if (options.vi && key.name === 'd' && key.ctrl) { self.move((self.height - self.iheight) / 2 | 0); self.screen.render(); return; } if (options.vi && key.name === 'b' && key.ctrl) { self.move(-(self.height - self.iheight)); self.screen.render(); return; } if (options.vi && key.name === 'f' && key.ctrl) { self.move(self.height - self.iheight); self.screen.render(); return; } if (options.vi && key.name === 'h' && key.shift) { self.move(self.childBase - self.selected); self.screen.render(); return; } if (options.vi && key.name === 'm' && key.shift) { // TODO: Maybe use Math.min(this.items.length, // ... for calculating visible items elsewhere. var visible = Math.min( self.height - self.iheight, self.items.length) / 2 | 0; self.move(self.childBase + visible - self.selected); self.screen.render(); return; } if (options.vi && key.name === 'l' && key.shift) { // XXX This goes one too far on lists with an odd number of items. self.down(self.childBase + Math.min(self.height - self.iheight, self.items.length) - self.selected); self.screen.render(); return; } if (options.vi && key.name === 'g' && !key.shift) { self.select(0); self.screen.render(); return; } if (options.vi && key.name === 'g' && key.shift) { self.select(self.items.length - 1); self.screen.render(); return; } if (options.vi && (key.ch === '/' || key.ch === '?')) { if (typeof self.options.search !== 'function') { return; } return self.options.search(function(err, value) { if (typeof err === 'string' || typeof err === 'function' || typeof err === 'number' || (err && err.test)) { value = err; err = null; } if (err || !value) return self.screen.render(); self.select(self.fuzzyFind(value, key.ch === '?')); self.screen.render(); }); } }); } this.on('resize', function() { var visible = self.height - self.iheight; // if (self.selected < visible - 1) { if (visible >= self.selected + 1) { self.childBase = 0; self.childOffset = self.selected; } else { // Is this supposed to be: self.childBase = visible - self.selected + 1; ? self.childBase = self.selected - visible + 1; self.childOffset = visible - 1; } }); this.on('adopt', function(el) { if (!~self.items.indexOf(el)) { el.fixed = true; } }); // Ensure children are removed from the // item list if they are items. this.on('remove', function(el) { self.removeItem(el); }); } List.prototype.__proto__ = Box.prototype; List.prototype.type = 'list'; List.prototype.add = List.prototype.addItem = List.prototype.appendItem = function(item) { var self = this; this.ritems.push(item); // Note: Could potentially use Button here. var options = { screen: this.screen, content: item, align: this.align || 'left', top: this.items.length, left: 0, right: (this.scrollbar ? 1 : 0), tags: this.parseTags, height: 1, hoverEffects: this.mouse ? this.style.item.hover : null, focusEffects: this.mouse ? this.style.item.focus : null, autoFocus: false }; if (!this.screen.autoPadding) { options.top = this.itop + this.items.length; options.left = this.ileft; options.right = this.iright + (this.scrollbar ? 1 : 0); } // if (this.shrink) { // XXX NOTE: Maybe just do this on all shrinkage once autoPadding is default? 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() { var attr = self.items[self.selected] === item && self.interactive ? self.style.selected[name] : self.style.item[name]; if (typeof attr === 'function') attr = attr(item); return attr; }; }); if (this.style.transparent) { options.transparent = true; } var item = new Box(options); this.items.push(item); this.append(item); if (this.items.length === 1) { this.select(0); } if (this.mouse) { item.on('click', function(data) { self.focus(); if (self.items[self.selected] === item) { self.emit('action', item, self.selected); self.emit('select', item, self.selected); return; } self.select(item); self.screen.render(); }); } this.emit('add item'); }; List.prototype.find = List.prototype.fuzzyFind = function(search, back) { var start = this.selected + (back ? -1 : 1); if (typeof search === 'number') search += ''; if (search && search[0] === '/' && search[search.length - 1] === '/') { try { search = new RegExp(search.slice(1, -1)); } catch (e) { ; } } var test = typeof search === 'string' ? function(item) { return !!~item.indexOf(search); } : (search.test ? search.test.bind(search) : search); if (typeof test !== 'function') { if (this.screen.options.debug) { throw new Error('fuzzyFind(): `test` is not a function.'); } return this.selected; } if (!back) { for (var i = start; i < this.ritems.length; i++){ if (test(helpers.cleanTags(this.ritems[i]))) return i; } for (var i = 0; i < start; i++){ if (test(helpers.cleanTags(this.ritems[i]))) return i; } } else { for (var i = start; i >= 0; i--){ if (test(helpers.cleanTags(this.ritems[i]))) return i; } for (var i = this.ritems.length - 1; i > start; i--){ if (test(helpers.cleanTags(this.ritems[i]))) return i; } } return this.selected; }; List.prototype.getItemIndex = function(child) { if (typeof child === 'number') { return child; } else if (typeof child === 'string') { var i = this.ritems.indexOf(child); if (~i) return i; for (i = 0; i < this.ritems.length; i++) { if (helpers.cleanTags(this.ritems[i]) === child) { return i; } } return -1; } else { return this.items.indexOf(child); } }; List.prototype.getItem = function(child) { return this.items[this.getItemIndex(child)]; }; List.prototype.removeItem = function(child) { var i = this.getItemIndex(child); if (~i && this.items[i]) { child = this.items.splice(i, 1)[0]; this.ritems.splice(i, 1); this.remove(child); if (i === this.selected) { this.select(i - 1); } } this.emit('remove item'); }; List.prototype.clearItems = function() { return this.setItems([]); }; List.prototype.setItems = function(items) { var items = items.slice() , original = this.items.slice() , selected = this.selected , sel = this.ritems[this.selected] , i = 0; this.select(0); for (; i < items.length; i++) { if (this.items[i]) { this.items[i].setContent(items[i]); } else { this.add(items[i]); } } for (; i < original.length; i++) { this.remove(original[i]); } this.ritems = items; // Try to find our old item if it still exists. sel = items.indexOf(sel); if (~sel) { this.select(sel); } else if (items.length === original.length) { this.select(selected); } else { this.select(Math.min(selected, items.length - 1)); } this.emit('set items'); }; List.prototype.select = function(index) { if (!this.interactive) { return; } if (!this.items.length) { this.selected = 0; this.value = ''; this.scrollTo(0); return; } if (typeof index === 'object') { index = this.items.indexOf(index); } if (index < 0) { index = 0; } else if (index >= this.items.length) { index = this.items.length - 1; } if (this.selected === index && this._listInitialized) return; this._listInitialized = true; this.selected = index; this.value = helpers.cleanTags(this.ritems[this.selected]); if (!this.parent) return; this.scrollTo(this.selected); // XXX Move `action` and `select` events here. this.emit('select item', this.items[this.selected], this.selected); }; List.prototype.move = function(offset) { this.select(this.selected + offset); }; List.prototype.up = function(offset) { this.move(-(offset || 1)); }; List.prototype.down = function(offset) { this.move(offset || 1); }; List.prototype.pick = function(label, callback) { if (!callback) { callback = label; label = null; } if (!this.interactive) { return callback(); } var self = this; var focused = this.screen.focused; if (focused && focused._done) focused._done('stop'); this.screen.saveFocus(); // XXX Keep above: // var parent = this.parent; // this.detach(); // parent.append(this); this.focus(); this.show(); this.select(0); if (label) this.setLabel(label); this.screen.render(); this.once('action', function(el, selected) { if (label) self.removeLabel(); self.screen.restoreFocus(); self.hide(); self.screen.render(); if (!el) return callback(); return callback(null, helpers.cleanTags(self.ritems[selected])); }); }; List.prototype.enterSelected = function(i) { if (i != null) this.select(i); this.emit('action', this.items[this.selected], this.selected); this.emit('select', this.items[this.selected], this.selected); }; List.prototype.cancelSelected = function(i) { if (i != null) this.select(i); this.emit('action'); this.emit('cancel'); }; /** * Expose */ module.exports = List;