listbar.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. /**
  2. * listbar.js - listbar 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. * Listbar / HorizontalList
  14. */
  15. function Listbar(options) {
  16. var self = this;
  17. if (!(this instanceof Node)) {
  18. return new Listbar(options);
  19. }
  20. options = options || {};
  21. this.items = [];
  22. this.ritems = [];
  23. this.commands = [];
  24. this.leftBase = 0;
  25. this.leftOffset = 0;
  26. this.mouse = options.mouse || false;
  27. Box.call(this, options);
  28. if (!this.style.selected) {
  29. this.style.selected = {};
  30. }
  31. if (!this.style.item) {
  32. this.style.item = {};
  33. }
  34. if (options.commands || options.items) {
  35. this.setItems(options.commands || options.items);
  36. }
  37. if (options.keys) {
  38. this.on('keypress', function(ch, key) {
  39. if (key.name === 'left'
  40. || (options.vi && key.name === 'h')
  41. || (key.shift && key.name === 'tab')) {
  42. self.moveLeft();
  43. self.screen.render();
  44. // Stop propagation if we're in a form.
  45. if (key.name === 'tab') return false;
  46. return;
  47. }
  48. if (key.name === 'right'
  49. || (options.vi && key.name === 'l')
  50. || key.name === 'tab') {
  51. self.moveRight();
  52. self.screen.render();
  53. // Stop propagation if we're in a form.
  54. if (key.name === 'tab') return false;
  55. return;
  56. }
  57. if (key.name === 'enter'
  58. || (options.vi && key.name === 'k' && !key.shift)) {
  59. self.emit('action', self.items[self.selected], self.selected);
  60. self.emit('select', self.items[self.selected], self.selected);
  61. var item = self.items[self.selected];
  62. if (item._.cmd.callback) {
  63. item._.cmd.callback();
  64. }
  65. self.screen.render();
  66. return;
  67. }
  68. if (key.name === 'escape' || (options.vi && key.name === 'q')) {
  69. self.emit('action');
  70. self.emit('cancel');
  71. return;
  72. }
  73. });
  74. }
  75. if (options.autoCommandKeys) {
  76. this.onScreenEvent('keypress', function(ch) {
  77. if (/^[0-9]$/.test(ch)) {
  78. var i = +ch - 1;
  79. if (!~i) i = 9;
  80. return self.selectTab(i);
  81. }
  82. });
  83. }
  84. this.on('focus', function() {
  85. self.select(self.selected);
  86. });
  87. }
  88. Listbar.prototype.__proto__ = Box.prototype;
  89. Listbar.prototype.type = 'listbar';
  90. Listbar.prototype.__defineGetter__('selected', function() {
  91. return this.leftBase + this.leftOffset;
  92. });
  93. Listbar.prototype.setItems = function(commands) {
  94. var self = this;
  95. if (!Array.isArray(commands)) {
  96. commands = Object.keys(commands).reduce(function(obj, key, i) {
  97. var cmd = commands[key]
  98. , cb;
  99. if (typeof cmd === 'function') {
  100. cb = cmd;
  101. cmd = { callback: cb };
  102. }
  103. if (cmd.text == null) cmd.text = key;
  104. if (cmd.prefix == null) cmd.prefix = ++i + '';
  105. if (cmd.text == null && cmd.callback) {
  106. cmd.text = cmd.callback.name;
  107. }
  108. obj.push(cmd);
  109. return obj;
  110. }, []);
  111. }
  112. this.items.forEach(function(el) {
  113. el.detach();
  114. });
  115. this.items = [];
  116. this.ritems = [];
  117. this.commands = [];
  118. commands.forEach(function(cmd) {
  119. self.add(cmd);
  120. });
  121. this.emit('set items');
  122. };
  123. Listbar.prototype.add =
  124. Listbar.prototype.addItem =
  125. Listbar.prototype.appendItem = function(item, callback) {
  126. var self = this
  127. , prev = this.items[this.items.length - 1]
  128. , drawn
  129. , cmd
  130. , title
  131. , len;
  132. if (!this.parent) {
  133. drawn = 0;
  134. } else {
  135. drawn = prev ? prev.aleft + prev.width : 0;
  136. if (!this.screen.autoPadding) {
  137. drawn += this.ileft;
  138. }
  139. }
  140. if (typeof item === 'object') {
  141. cmd = item;
  142. if (cmd.prefix == null) cmd.prefix = (this.items.length + 1) + '';
  143. }
  144. if (typeof item === 'string') {
  145. cmd = {
  146. prefix: (this.items.length + 1) + '',
  147. text: item,
  148. callback: callback
  149. };
  150. }
  151. if (typeof item === 'function') {
  152. cmd = {
  153. prefix: (this.items.length + 1) + '',
  154. text: item.name,
  155. callback: item
  156. };
  157. }
  158. if (cmd.keys && cmd.keys[0]) {
  159. cmd.prefix = cmd.keys[0];
  160. }
  161. var t = helpers.generateTags(this.style.prefix || { fg: 'lightblack' });
  162. title = (cmd.prefix != null ? t.open + cmd.prefix + t.close + ':' : '') + cmd.text;
  163. len = ((cmd.prefix != null ? cmd.prefix + ':' : '') + cmd.text).length;
  164. var options = {
  165. screen: this.screen,
  166. top: 0,
  167. left: drawn + 1,
  168. height: 1,
  169. content: title,
  170. width: len + 2,
  171. align: 'center',
  172. autoFocus: false,
  173. tags: true,
  174. mouse: true,
  175. style: helpers.merge({}, this.style.item),
  176. noOverflow: true
  177. };
  178. if (!this.screen.autoPadding) {
  179. options.top += this.itop;
  180. options.left += this.ileft;
  181. }
  182. ['bg', 'fg', 'bold', 'underline',
  183. 'blink', 'inverse', 'invisible'].forEach(function(name) {
  184. options.style[name] = function() {
  185. var attr = self.items[self.selected] === el
  186. ? self.style.selected[name]
  187. : self.style.item[name];
  188. if (typeof attr === 'function') attr = attr(el);
  189. return attr;
  190. };
  191. });
  192. var el = new Box(options);
  193. this._[cmd.text] = el;
  194. cmd.element = el;
  195. el._.cmd = cmd;
  196. this.ritems.push(cmd.text);
  197. this.items.push(el);
  198. this.commands.push(cmd);
  199. this.append(el);
  200. if (cmd.callback) {
  201. if (cmd.keys) {
  202. this.screen.key(cmd.keys, function() {
  203. self.emit('action', el, self.selected);
  204. self.emit('select', el, self.selected);
  205. if (el._.cmd.callback) {
  206. el._.cmd.callback();
  207. }
  208. self.select(el);
  209. self.screen.render();
  210. });
  211. }
  212. }
  213. if (this.items.length === 1) {
  214. this.select(0);
  215. }
  216. // XXX May be affected by new element.options.mouse option.
  217. if (this.mouse) {
  218. el.on('click', function() {
  219. self.emit('action', el, self.selected);
  220. self.emit('select', el, self.selected);
  221. if (el._.cmd.callback) {
  222. el._.cmd.callback();
  223. }
  224. self.select(el);
  225. self.screen.render();
  226. });
  227. }
  228. this.emit('add item');
  229. };
  230. Listbar.prototype.render = function() {
  231. var self = this
  232. , drawn = 0;
  233. if (!this.screen.autoPadding) {
  234. drawn += this.ileft;
  235. }
  236. this.items.forEach(function(el, i) {
  237. if (i < self.leftBase) {
  238. el.hide();
  239. } else {
  240. el.rleft = drawn + 1;
  241. drawn += el.width + 2;
  242. el.show();
  243. }
  244. });
  245. return this._render();
  246. };
  247. Listbar.prototype.select = function(offset) {
  248. if (typeof offset !== 'number') {
  249. offset = this.items.indexOf(offset);
  250. }
  251. if (offset < 0) {
  252. offset = 0;
  253. } else if (offset >= this.items.length) {
  254. offset = this.items.length - 1;
  255. }
  256. if (!this.parent) {
  257. this.emit('select item', this.items[offset], offset);
  258. return;
  259. }
  260. var lpos = this._getCoords();
  261. if (!lpos) return;
  262. var self = this
  263. , width = (lpos.xl - lpos.xi) - this.iwidth
  264. , drawn = 0
  265. , visible = 0
  266. , el;
  267. el = this.items[offset];
  268. if (!el) return;
  269. this.items.forEach(function(el, i) {
  270. if (i < self.leftBase) return;
  271. var lpos = el._getCoords();
  272. if (!lpos) return;
  273. if (lpos.xl - lpos.xi <= 0) return;
  274. drawn += (lpos.xl - lpos.xi) + 2;
  275. if (drawn <= width) visible++;
  276. });
  277. var diff = offset - (this.leftBase + this.leftOffset);
  278. if (offset > this.leftBase + this.leftOffset) {
  279. if (offset > this.leftBase + visible - 1) {
  280. this.leftOffset = 0;
  281. this.leftBase = offset;
  282. } else {
  283. this.leftOffset += diff;
  284. }
  285. } else if (offset < this.leftBase + this.leftOffset) {
  286. diff = -diff;
  287. if (offset < this.leftBase) {
  288. this.leftOffset = 0;
  289. this.leftBase = offset;
  290. } else {
  291. this.leftOffset -= diff;
  292. }
  293. }
  294. // XXX Move `action` and `select` events here.
  295. this.emit('select item', el, offset);
  296. };
  297. Listbar.prototype.removeItem = function(child) {
  298. var i = typeof child !== 'number'
  299. ? this.items.indexOf(child)
  300. : child;
  301. if (~i && this.items[i]) {
  302. child = this.items.splice(i, 1)[0];
  303. this.ritems.splice(i, 1);
  304. this.commands.splice(i, 1);
  305. this.remove(child);
  306. if (i === this.selected) {
  307. this.select(i - 1);
  308. }
  309. }
  310. this.emit('remove item');
  311. };
  312. Listbar.prototype.move = function(offset) {
  313. this.select(this.selected + offset);
  314. };
  315. Listbar.prototype.moveLeft = function(offset) {
  316. this.move(-(offset || 1));
  317. };
  318. Listbar.prototype.moveRight = function(offset) {
  319. this.move(offset || 1);
  320. };
  321. Listbar.prototype.selectTab = function(index) {
  322. var item = this.items[index];
  323. if (item) {
  324. if (item._.cmd.callback) {
  325. item._.cmd.callback();
  326. }
  327. this.select(index);
  328. this.screen.render();
  329. }
  330. this.emit('select tab', item, index);
  331. };
  332. /**
  333. * Expose
  334. */
  335. module.exports = Listbar;