multiselect.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. 'use strict';
  2. const color = require('kleur');
  3. const { cursor } = require('sisteransi');
  4. const Prompt = require('./prompt');
  5. const { clear, figures, style, wrap, entriesToDisplay } = require('../util');
  6. /**
  7. * MultiselectPrompt Base Element
  8. * @param {Object} opts Options
  9. * @param {String} opts.message Message
  10. * @param {Array} opts.choices Array of choice objects
  11. * @param {String} [opts.hint] Hint to display
  12. * @param {String} [opts.warn] Hint shown for disabled choices
  13. * @param {Number} [opts.max] Max choices
  14. * @param {Number} [opts.cursor=0] Cursor start position
  15. * @param {Number} [opts.optionsPerPage=10] Max options to display at once
  16. * @param {Stream} [opts.stdin] The Readable stream to listen to
  17. * @param {Stream} [opts.stdout] The Writable stream to write readline data to
  18. */
  19. class MultiselectPrompt extends Prompt {
  20. constructor(opts={}) {
  21. super(opts);
  22. this.msg = opts.message;
  23. this.cursor = opts.cursor || 0;
  24. this.scrollIndex = opts.cursor || 0;
  25. this.hint = opts.hint || '';
  26. this.warn = opts.warn || '- This option is disabled -';
  27. this.minSelected = opts.min;
  28. this.showMinError = false;
  29. this.maxChoices = opts.max;
  30. this.instructions = opts.instructions;
  31. this.optionsPerPage = opts.optionsPerPage || 10;
  32. this.value = opts.choices.map((ch, idx) => {
  33. if (typeof ch === 'string')
  34. ch = {title: ch, value: idx};
  35. return {
  36. title: ch && (ch.title || ch.value || ch),
  37. description: ch && ch.description,
  38. value: ch && (ch.value === undefined ? idx : ch.value),
  39. selected: ch && ch.selected,
  40. disabled: ch && ch.disabled
  41. };
  42. });
  43. this.clear = clear('', this.out.columns);
  44. if (!opts.overrideRender) {
  45. this.render();
  46. }
  47. }
  48. reset() {
  49. this.value.map(v => !v.selected);
  50. this.cursor = 0;
  51. this.fire();
  52. this.render();
  53. }
  54. selected() {
  55. return this.value.filter(v => v.selected);
  56. }
  57. exit() {
  58. this.abort();
  59. }
  60. abort() {
  61. this.done = this.aborted = true;
  62. this.fire();
  63. this.render();
  64. this.out.write('\n');
  65. this.close();
  66. }
  67. submit() {
  68. const selected = this.value
  69. .filter(e => e.selected);
  70. if (this.minSelected && selected.length < this.minSelected) {
  71. this.showMinError = true;
  72. this.render();
  73. } else {
  74. this.done = true;
  75. this.aborted = false;
  76. this.fire();
  77. this.render();
  78. this.out.write('\n');
  79. this.close();
  80. }
  81. }
  82. first() {
  83. this.cursor = 0;
  84. this.render();
  85. }
  86. last() {
  87. this.cursor = this.value.length - 1;
  88. this.render();
  89. }
  90. next() {
  91. this.cursor = (this.cursor + 1) % this.value.length;
  92. this.render();
  93. }
  94. up() {
  95. if (this.cursor === 0) {
  96. this.cursor = this.value.length - 1;
  97. } else {
  98. this.cursor--;
  99. }
  100. this.render();
  101. }
  102. down() {
  103. if (this.cursor === this.value.length - 1) {
  104. this.cursor = 0;
  105. } else {
  106. this.cursor++;
  107. }
  108. this.render();
  109. }
  110. left() {
  111. this.value[this.cursor].selected = false;
  112. this.render();
  113. }
  114. right() {
  115. if (this.value.filter(e => e.selected).length >= this.maxChoices) return this.bell();
  116. this.value[this.cursor].selected = true;
  117. this.render();
  118. }
  119. handleSpaceToggle() {
  120. const v = this.value[this.cursor];
  121. if (v.selected) {
  122. v.selected = false;
  123. this.render();
  124. } else if (v.disabled || this.value.filter(e => e.selected).length >= this.maxChoices) {
  125. return this.bell();
  126. } else {
  127. v.selected = true;
  128. this.render();
  129. }
  130. }
  131. toggleAll() {
  132. if (this.maxChoices !== undefined || this.value[this.cursor].disabled) {
  133. return this.bell();
  134. }
  135. const newSelected = !this.value[this.cursor].selected;
  136. this.value.filter(v => !v.disabled).forEach(v => v.selected = newSelected);
  137. this.render();
  138. }
  139. _(c, key) {
  140. if (c === ' ') {
  141. this.handleSpaceToggle();
  142. } else if (c === 'a') {
  143. this.toggleAll();
  144. } else {
  145. return this.bell();
  146. }
  147. }
  148. renderInstructions() {
  149. if (this.instructions === undefined || this.instructions) {
  150. if (typeof this.instructions === 'string') {
  151. return this.instructions;
  152. }
  153. return '\nInstructions:\n'
  154. + ` ${figures.arrowUp}/${figures.arrowDown}: Highlight option\n`
  155. + ` ${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection\n`
  156. + (this.maxChoices === undefined ? ` a: Toggle all\n` : '')
  157. + ` enter/return: Complete answer`;
  158. }
  159. return '';
  160. }
  161. renderOption(cursor, v, i, arrowIndicator) {
  162. const prefix = (v.selected ? color.green(figures.radioOn) : figures.radioOff) + ' ' + arrowIndicator + ' ';
  163. let title, desc;
  164. if (v.disabled) {
  165. title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
  166. } else {
  167. title = cursor === i ? color.cyan().underline(v.title) : v.title;
  168. if (cursor === i && v.description) {
  169. desc = ` - ${v.description}`;
  170. if (prefix.length + title.length + desc.length >= this.out.columns
  171. || v.description.split(/\r?\n/).length > 1) {
  172. desc = '\n' + wrap(v.description, { margin: prefix.length, width: this.out.columns });
  173. }
  174. }
  175. }
  176. return prefix + title + color.gray(desc || '');
  177. }
  178. // shared with autocompleteMultiselect
  179. paginateOptions(options) {
  180. if (options.length === 0) {
  181. return color.red('No matches for this query.');
  182. }
  183. let { startIndex, endIndex } = entriesToDisplay(this.cursor, options.length, this.optionsPerPage);
  184. let prefix, styledOptions = [];
  185. for (let i = startIndex; i < endIndex; i++) {
  186. if (i === startIndex && startIndex > 0) {
  187. prefix = figures.arrowUp;
  188. } else if (i === endIndex - 1 && endIndex < options.length) {
  189. prefix = figures.arrowDown;
  190. } else {
  191. prefix = ' ';
  192. }
  193. styledOptions.push(this.renderOption(this.cursor, options[i], i, prefix));
  194. }
  195. return '\n' + styledOptions.join('\n');
  196. }
  197. // shared with autocomleteMultiselect
  198. renderOptions(options) {
  199. if (!this.done) {
  200. return this.paginateOptions(options);
  201. }
  202. return '';
  203. }
  204. renderDoneOrInstructions() {
  205. if (this.done) {
  206. return this.value
  207. .filter(e => e.selected)
  208. .map(v => v.title)
  209. .join(', ');
  210. }
  211. const output = [color.gray(this.hint), this.renderInstructions()];
  212. if (this.value[this.cursor].disabled) {
  213. output.push(color.yellow(this.warn));
  214. }
  215. return output.join(' ');
  216. }
  217. render() {
  218. if (this.closed) return;
  219. if (this.firstRender) this.out.write(cursor.hide);
  220. super.render();
  221. // print prompt
  222. let prompt = [
  223. style.symbol(this.done, this.aborted),
  224. color.bold(this.msg),
  225. style.delimiter(false),
  226. this.renderDoneOrInstructions()
  227. ].join(' ');
  228. if (this.showMinError) {
  229. prompt += color.red(`You must select a minimum of ${this.minSelected} choices.`);
  230. this.showMinError = false;
  231. }
  232. prompt += this.renderOptions(this.value);
  233. this.out.write(this.clear + prompt);
  234. this.clear = clear(prompt, this.out.columns);
  235. }
  236. }
  237. module.exports = MultiselectPrompt;