123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599 |
- /**
- * 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() {
- self.select(self.selected + 2);
- self.screen.render();
- });
- this.on('element wheelup', function() {
- 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.createItem = function(content) {
- var self = this;
- // Note: Could potentially use Button here.
- var options = {
- screen: this.screen,
- content: content,
- align: this.align || 'left',
- top: 0,
- 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 = 1;
- 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);
- if (this.mouse) {
- item.on('click', function() {
- 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('create item');
- return item;
- };
- List.prototype.add =
- List.prototype.addItem =
- List.prototype.appendItem = function(content) {
- content = typeof content === 'string' ? content : content.getContent();
- var item = this.createItem(content);
- item.position.top = this.items.length;
- if (!this.screen.autoPadding) {
- item.position.top = this.itop + this.items.length;
- }
- this.ritems.push(content);
- this.items.push(item);
- this.append(item);
- if (this.items.length === 1) {
- this.select(0);
- }
- this.emit('add item');
- return item;
- };
- 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);
- for (var j = i; j < this.items.length; j++) {
- this.items[j].position.top--;
- }
- if (i === this.selected) {
- this.select(i - 1);
- }
- }
- this.emit('remove item');
- return child;
- };
- List.prototype.insertItem = function(child, content) {
- content = typeof content === 'string' ? content : content.getContent();
- var i = this.getItemIndex(child);
- if (!~i) return;
- if (i >= this.items.length) return this.appendItem(content);
- var item = this.createItem(content);
- for (var j = i; j < this.items.length; j++) {
- this.items[j].position.top++;
- }
- item.position.top = i + (!this.screen.autoPadding ? 1 : 0);
- this.ritems.splice(i, 0, content);
- this.items.splice(i, 0, item);
- this.append(item);
- if (i === this.selected) {
- this.select(i + 1);
- }
- this.emit('insert item');
- };
- List.prototype.getItem = function(child) {
- return this.items[this.getItemIndex(child)];
- };
- List.prototype.setItem = function(child, content) {
- content = typeof content === 'string' ? content : content.getContent();
- var i = this.getItemIndex(child);
- if (!~i) return;
- this.items[i].setContent(content);
- this.ritems[i] = content;
- };
- List.prototype.clearItems = function() {
- return this.setItems([]);
- };
- List.prototype.setItems = function(items) {
- var original = this.items.slice()
- , selected = this.selected
- , sel = this.ritems[this.selected]
- , i = 0;
- items = items.slice();
- 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.pushItem = function(content) {
- this.appendItem(content);
- return this.items.length;
- };
- List.prototype.popItem = function() {
- return this.removeItem(this.items.length - 1);
- };
- List.prototype.unshiftItem = function(content) {
- this.insertItem(0, content);
- return this.items.length;
- };
- List.prototype.shiftItem = function() {
- return this.removeItem(0);
- };
- List.prototype.spliceItem = function(child, n) {
- var self = this;
- var i = this.getItemIndex(child);
- if (!~i) return;
- var items = Array.prototype.slice.call(arguments, 2);
- var removed = [];
- while (n--) {
- removed.push(this.removeItem(i));
- }
- items.forEach(function(item) {
- self.insertItem(i++, item);
- });
- return removed;
- };
- List.prototype.find =
- List.prototype.fuzzyFind = function(search, back) {
- var start = this.selected + (back ? -1 : 1)
- , i;
- 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 (i = start; i < this.ritems.length; i++) {
- if (test(helpers.cleanTags(this.ritems[i]))) return i;
- }
- for (i = 0; i < start; i++) {
- if (test(helpers.cleanTags(this.ritems[i]))) return i;
- }
- } else {
- for (i = start; i >= 0; i--) {
- if (test(helpers.cleanTags(this.ritems[i]))) return i;
- }
- for (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.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;
|