list.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. /**
  2. * list.js - list element for blessed
  3. * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License).
  4. * https://github.com/chjj/blessed
  5. */
  6. /**
  7. * Modules
  8. */
  9. var helpers = require('../helpers');
  10. var Node = require('./node');
  11. var Box = require('./box');
  12. /**
  13. * List
  14. */
  15. function List(options) {
  16. var self = this;
  17. if (!(this instanceof Node)) {
  18. return new List(options);
  19. }
  20. options = options || {};
  21. options.ignoreKeys = true;
  22. // Possibly put this here: this.items = [];
  23. options.scrollable = true;
  24. Box.call(this, options);
  25. this.value = '';
  26. this.items = [];
  27. this.ritems = [];
  28. this.selected = 0;
  29. this._isList = true;
  30. if (!this.style.selected) {
  31. this.style.selected = {};
  32. this.style.selected.bg = options.selectedBg;
  33. this.style.selected.fg = options.selectedFg;
  34. this.style.selected.bold = options.selectedBold;
  35. this.style.selected.underline = options.selectedUnderline;
  36. this.style.selected.blink = options.selectedBlink;
  37. this.style.selected.inverse = options.selectedInverse;
  38. this.style.selected.invisible = options.selectedInvisible;
  39. }
  40. if (!this.style.item) {
  41. this.style.item = {};
  42. this.style.item.bg = options.itemBg;
  43. this.style.item.fg = options.itemFg;
  44. this.style.item.bold = options.itemBold;
  45. this.style.item.underline = options.itemUnderline;
  46. this.style.item.blink = options.itemBlink;
  47. this.style.item.inverse = options.itemInverse;
  48. this.style.item.invisible = options.itemInvisible;
  49. }
  50. // Legacy: for apps written before the addition of item attributes.
  51. ['bg', 'fg', 'bold', 'underline',
  52. 'blink', 'inverse', 'invisible'].forEach(function(name) {
  53. if (self.style[name] != null && self.style.item[name] == null) {
  54. self.style.item[name] = self.style[name];
  55. }
  56. });
  57. if (this.options.itemHoverBg) {
  58. this.options.itemHoverEffects = { bg: this.options.itemHoverBg };
  59. }
  60. if (this.options.itemHoverEffects) {
  61. this.style.item.hover = this.options.itemHoverEffects;
  62. }
  63. if (this.options.itemFocusEffects) {
  64. this.style.item.focus = this.options.itemFocusEffects;
  65. }
  66. this.interactive = options.interactive !== false;
  67. this.mouse = options.mouse || false;
  68. if (options.items) {
  69. this.ritems = options.items;
  70. options.items.forEach(this.add.bind(this));
  71. }
  72. this.select(0);
  73. if (options.mouse) {
  74. this.screen._listenMouse(this);
  75. this.on('element wheeldown', function() {
  76. self.select(self.selected + 2);
  77. self.screen.render();
  78. });
  79. this.on('element wheelup', function() {
  80. self.select(self.selected - 2);
  81. self.screen.render();
  82. });
  83. }
  84. if (options.keys) {
  85. this.on('keypress', function(ch, key) {
  86. if (key.name === 'up' || (options.vi && key.name === 'k')) {
  87. self.up();
  88. self.screen.render();
  89. return;
  90. }
  91. if (key.name === 'down' || (options.vi && key.name === 'j')) {
  92. self.down();
  93. self.screen.render();
  94. return;
  95. }
  96. if (key.name === 'enter'
  97. || (options.vi && key.name === 'l' && !key.shift)) {
  98. self.enterSelected();
  99. return;
  100. }
  101. if (key.name === 'escape' || (options.vi && key.name === 'q')) {
  102. self.cancelSelected();
  103. return;
  104. }
  105. if (options.vi && key.name === 'u' && key.ctrl) {
  106. self.move(-((self.height - self.iheight) / 2) | 0);
  107. self.screen.render();
  108. return;
  109. }
  110. if (options.vi && key.name === 'd' && key.ctrl) {
  111. self.move((self.height - self.iheight) / 2 | 0);
  112. self.screen.render();
  113. return;
  114. }
  115. if (options.vi && key.name === 'b' && key.ctrl) {
  116. self.move(-(self.height - self.iheight));
  117. self.screen.render();
  118. return;
  119. }
  120. if (options.vi && key.name === 'f' && key.ctrl) {
  121. self.move(self.height - self.iheight);
  122. self.screen.render();
  123. return;
  124. }
  125. if (options.vi && key.name === 'h' && key.shift) {
  126. self.move(self.childBase - self.selected);
  127. self.screen.render();
  128. return;
  129. }
  130. if (options.vi && key.name === 'm' && key.shift) {
  131. // TODO: Maybe use Math.min(this.items.length,
  132. // ... for calculating visible items elsewhere.
  133. var visible = Math.min(
  134. self.height - self.iheight,
  135. self.items.length) / 2 | 0;
  136. self.move(self.childBase + visible - self.selected);
  137. self.screen.render();
  138. return;
  139. }
  140. if (options.vi && key.name === 'l' && key.shift) {
  141. // XXX This goes one too far on lists with an odd number of items.
  142. self.down(self.childBase
  143. + Math.min(self.height - self.iheight, self.items.length)
  144. - self.selected);
  145. self.screen.render();
  146. return;
  147. }
  148. if (options.vi && key.name === 'g' && !key.shift) {
  149. self.select(0);
  150. self.screen.render();
  151. return;
  152. }
  153. if (options.vi && key.name === 'g' && key.shift) {
  154. self.select(self.items.length - 1);
  155. self.screen.render();
  156. return;
  157. }
  158. if (options.vi && (key.ch === '/' || key.ch === '?')) {
  159. if (typeof self.options.search !== 'function') {
  160. return;
  161. }
  162. return self.options.search(function(err, value) {
  163. if (typeof err === 'string' || typeof err === 'function'
  164. || typeof err === 'number' || (err && err.test)) {
  165. value = err;
  166. err = null;
  167. }
  168. if (err || !value) return self.screen.render();
  169. self.select(self.fuzzyFind(value, key.ch === '?'));
  170. self.screen.render();
  171. });
  172. }
  173. });
  174. }
  175. this.on('resize', function() {
  176. var visible = self.height - self.iheight;
  177. // if (self.selected < visible - 1) {
  178. if (visible >= self.selected + 1) {
  179. self.childBase = 0;
  180. self.childOffset = self.selected;
  181. } else {
  182. // Is this supposed to be: self.childBase = visible - self.selected + 1; ?
  183. self.childBase = self.selected - visible + 1;
  184. self.childOffset = visible - 1;
  185. }
  186. });
  187. this.on('adopt', function(el) {
  188. if (!~self.items.indexOf(el)) {
  189. el.fixed = true;
  190. }
  191. });
  192. // Ensure children are removed from the
  193. // item list if they are items.
  194. this.on('remove', function(el) {
  195. self.removeItem(el);
  196. });
  197. }
  198. List.prototype.__proto__ = Box.prototype;
  199. List.prototype.type = 'list';
  200. List.prototype.createItem = function(content) {
  201. var self = this;
  202. // Note: Could potentially use Button here.
  203. var options = {
  204. screen: this.screen,
  205. content: content,
  206. align: this.align || 'left',
  207. top: 0,
  208. left: 0,
  209. right: (this.scrollbar ? 1 : 0),
  210. tags: this.parseTags,
  211. height: 1,
  212. hoverEffects: this.mouse ? this.style.item.hover : null,
  213. focusEffects: this.mouse ? this.style.item.focus : null,
  214. autoFocus: false
  215. };
  216. if (!this.screen.autoPadding) {
  217. options.top = 1;
  218. options.left = this.ileft;
  219. options.right = this.iright + (this.scrollbar ? 1 : 0);
  220. }
  221. // if (this.shrink) {
  222. // XXX NOTE: Maybe just do this on all shrinkage once autoPadding is default?
  223. if (this.shrink && this.options.normalShrink) {
  224. delete options.right;
  225. options.width = 'shrink';
  226. }
  227. ['bg', 'fg', 'bold', 'underline',
  228. 'blink', 'inverse', 'invisible'].forEach(function(name) {
  229. options[name] = function() {
  230. var attr = self.items[self.selected] === item && self.interactive
  231. ? self.style.selected[name]
  232. : self.style.item[name];
  233. if (typeof attr === 'function') attr = attr(item);
  234. return attr;
  235. };
  236. });
  237. if (this.style.transparent) {
  238. options.transparent = true;
  239. }
  240. var item = new Box(options);
  241. if (this.mouse) {
  242. item.on('click', function() {
  243. self.focus();
  244. if (self.items[self.selected] === item) {
  245. self.emit('action', item, self.selected);
  246. self.emit('select', item, self.selected);
  247. return;
  248. }
  249. self.select(item);
  250. self.screen.render();
  251. });
  252. }
  253. this.emit('create item');
  254. return item;
  255. };
  256. List.prototype.add =
  257. List.prototype.addItem =
  258. List.prototype.appendItem = function(content) {
  259. content = typeof content === 'string' ? content : content.getContent();
  260. var item = this.createItem(content);
  261. item.position.top = this.items.length;
  262. if (!this.screen.autoPadding) {
  263. item.position.top = this.itop + this.items.length;
  264. }
  265. this.ritems.push(content);
  266. this.items.push(item);
  267. this.append(item);
  268. if (this.items.length === 1) {
  269. this.select(0);
  270. }
  271. this.emit('add item');
  272. return item;
  273. };
  274. List.prototype.removeItem = function(child) {
  275. var i = this.getItemIndex(child);
  276. if (~i && this.items[i]) {
  277. child = this.items.splice(i, 1)[0];
  278. this.ritems.splice(i, 1);
  279. this.remove(child);
  280. for (var j = i; j < this.items.length; j++) {
  281. this.items[j].position.top--;
  282. }
  283. if (i === this.selected) {
  284. this.select(i - 1);
  285. }
  286. }
  287. this.emit('remove item');
  288. return child;
  289. };
  290. List.prototype.insertItem = function(child, content) {
  291. content = typeof content === 'string' ? content : content.getContent();
  292. var i = this.getItemIndex(child);
  293. if (!~i) return;
  294. if (i >= this.items.length) return this.appendItem(content);
  295. var item = this.createItem(content);
  296. for (var j = i; j < this.items.length; j++) {
  297. this.items[j].position.top++;
  298. }
  299. item.position.top = i + (!this.screen.autoPadding ? 1 : 0);
  300. this.ritems.splice(i, 0, content);
  301. this.items.splice(i, 0, item);
  302. this.append(item);
  303. if (i === this.selected) {
  304. this.select(i + 1);
  305. }
  306. this.emit('insert item');
  307. };
  308. List.prototype.getItem = function(child) {
  309. return this.items[this.getItemIndex(child)];
  310. };
  311. List.prototype.setItem = function(child, content) {
  312. content = typeof content === 'string' ? content : content.getContent();
  313. var i = this.getItemIndex(child);
  314. if (!~i) return;
  315. this.items[i].setContent(content);
  316. this.ritems[i] = content;
  317. };
  318. List.prototype.clearItems = function() {
  319. return this.setItems([]);
  320. };
  321. List.prototype.setItems = function(items) {
  322. var original = this.items.slice()
  323. , selected = this.selected
  324. , sel = this.ritems[this.selected]
  325. , i = 0;
  326. items = items.slice();
  327. this.select(0);
  328. for (; i < items.length; i++) {
  329. if (this.items[i]) {
  330. this.items[i].setContent(items[i]);
  331. } else {
  332. this.add(items[i]);
  333. }
  334. }
  335. for (; i < original.length; i++) {
  336. this.remove(original[i]);
  337. }
  338. this.ritems = items;
  339. // Try to find our old item if it still exists.
  340. sel = items.indexOf(sel);
  341. if (~sel) {
  342. this.select(sel);
  343. } else if (items.length === original.length) {
  344. this.select(selected);
  345. } else {
  346. this.select(Math.min(selected, items.length - 1));
  347. }
  348. this.emit('set items');
  349. };
  350. List.prototype.pushItem = function(content) {
  351. this.appendItem(content);
  352. return this.items.length;
  353. };
  354. List.prototype.popItem = function() {
  355. return this.removeItem(this.items.length - 1);
  356. };
  357. List.prototype.unshiftItem = function(content) {
  358. this.insertItem(0, content);
  359. return this.items.length;
  360. };
  361. List.prototype.shiftItem = function() {
  362. return this.removeItem(0);
  363. };
  364. List.prototype.spliceItem = function(child, n) {
  365. var self = this;
  366. var i = this.getItemIndex(child);
  367. if (!~i) return;
  368. var items = Array.prototype.slice.call(arguments, 2);
  369. var removed = [];
  370. while (n--) {
  371. removed.push(this.removeItem(i));
  372. }
  373. items.forEach(function(item) {
  374. self.insertItem(i++, item);
  375. });
  376. return removed;
  377. };
  378. List.prototype.find =
  379. List.prototype.fuzzyFind = function(search, back) {
  380. var start = this.selected + (back ? -1 : 1)
  381. , i;
  382. if (typeof search === 'number') search += '';
  383. if (search && search[0] === '/' && search[search.length - 1] === '/') {
  384. try {
  385. search = new RegExp(search.slice(1, -1));
  386. } catch (e) {
  387. ;
  388. }
  389. }
  390. var test = typeof search === 'string'
  391. ? function(item) { return !!~item.indexOf(search); }
  392. : (search.test ? search.test.bind(search) : search);
  393. if (typeof test !== 'function') {
  394. if (this.screen.options.debug) {
  395. throw new Error('fuzzyFind(): `test` is not a function.');
  396. }
  397. return this.selected;
  398. }
  399. if (!back) {
  400. for (i = start; i < this.ritems.length; i++) {
  401. if (test(helpers.cleanTags(this.ritems[i]))) return i;
  402. }
  403. for (i = 0; i < start; i++) {
  404. if (test(helpers.cleanTags(this.ritems[i]))) return i;
  405. }
  406. } else {
  407. for (i = start; i >= 0; i--) {
  408. if (test(helpers.cleanTags(this.ritems[i]))) return i;
  409. }
  410. for (i = this.ritems.length - 1; i > start; i--) {
  411. if (test(helpers.cleanTags(this.ritems[i]))) return i;
  412. }
  413. }
  414. return this.selected;
  415. };
  416. List.prototype.getItemIndex = function(child) {
  417. if (typeof child === 'number') {
  418. return child;
  419. } else if (typeof child === 'string') {
  420. var i = this.ritems.indexOf(child);
  421. if (~i) return i;
  422. for (i = 0; i < this.ritems.length; i++) {
  423. if (helpers.cleanTags(this.ritems[i]) === child) {
  424. return i;
  425. }
  426. }
  427. return -1;
  428. } else {
  429. return this.items.indexOf(child);
  430. }
  431. };
  432. List.prototype.select = function(index) {
  433. if (!this.interactive) {
  434. return;
  435. }
  436. if (!this.items.length) {
  437. this.selected = 0;
  438. this.value = '';
  439. this.scrollTo(0);
  440. return;
  441. }
  442. if (typeof index === 'object') {
  443. index = this.items.indexOf(index);
  444. }
  445. if (index < 0) {
  446. index = 0;
  447. } else if (index >= this.items.length) {
  448. index = this.items.length - 1;
  449. }
  450. if (this.selected === index && this._listInitialized) return;
  451. this._listInitialized = true;
  452. this.selected = index;
  453. this.value = helpers.cleanTags(this.ritems[this.selected]);
  454. if (!this.parent) return;
  455. this.scrollTo(this.selected);
  456. // XXX Move `action` and `select` events here.
  457. this.emit('select item', this.items[this.selected], this.selected);
  458. };
  459. List.prototype.move = function(offset) {
  460. this.select(this.selected + offset);
  461. };
  462. List.prototype.up = function(offset) {
  463. this.move(-(offset || 1));
  464. };
  465. List.prototype.down = function(offset) {
  466. this.move(offset || 1);
  467. };
  468. List.prototype.pick = function(label, callback) {
  469. if (!callback) {
  470. callback = label;
  471. label = null;
  472. }
  473. if (!this.interactive) {
  474. return callback();
  475. }
  476. var self = this;
  477. var focused = this.screen.focused;
  478. if (focused && focused._done) focused._done('stop');
  479. this.screen.saveFocus();
  480. // XXX Keep above:
  481. // var parent = this.parent;
  482. // this.detach();
  483. // parent.append(this);
  484. this.focus();
  485. this.show();
  486. this.select(0);
  487. if (label) this.setLabel(label);
  488. this.screen.render();
  489. this.once('action', function(el, selected) {
  490. if (label) self.removeLabel();
  491. self.screen.restoreFocus();
  492. self.hide();
  493. self.screen.render();
  494. if (!el) return callback();
  495. return callback(null, helpers.cleanTags(self.ritems[selected]));
  496. });
  497. };
  498. List.prototype.enterSelected = function(i) {
  499. if (i != null) this.select(i);
  500. this.emit('action', this.items[this.selected], this.selected);
  501. this.emit('select', this.items[this.selected], this.selected);
  502. };
  503. List.prototype.cancelSelected = function(i) {
  504. if (i != null) this.select(i);
  505. this.emit('action');
  506. this.emit('cancel');
  507. };
  508. /**
  509. * Expose
  510. */
  511. module.exports = List;