expand.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. 'use strict';
  2. /**
  3. * `rawlist` type prompt
  4. */
  5. var _ = require('lodash');
  6. var chalk = require('chalk');
  7. var Base = require('./base');
  8. var Separator = require('../objects/separator');
  9. var observe = require('../utils/events');
  10. var Paginator = require('../utils/paginator');
  11. class ExpandPrompt extends Base {
  12. constructor(questions, rl, answers) {
  13. super(questions, rl, answers);
  14. if (!this.opt.choices) {
  15. this.throwParamError('choices');
  16. }
  17. this.validateChoices(this.opt.choices);
  18. // Add the default `help` (/expand) option
  19. this.opt.choices.push({
  20. key: 'h',
  21. name: 'Help, list all options',
  22. value: 'help'
  23. });
  24. this.opt.validate = choice => {
  25. if (choice == null) {
  26. return 'Please enter a valid command';
  27. }
  28. return choice !== 'help';
  29. };
  30. // Setup the default string (capitalize the default key)
  31. this.opt.default = this.generateChoicesString(this.opt.choices, this.opt.default);
  32. this.paginator = new Paginator(this.screen);
  33. }
  34. /**
  35. * Start the Inquiry session
  36. * @param {Function} cb Callback when prompt is done
  37. * @return {this}
  38. */
  39. _run(cb) {
  40. this.done = cb;
  41. // Save user answer and update prompt to show selected option.
  42. var events = observe(this.rl);
  43. var validation = this.handleSubmitEvents(
  44. events.line.map(this.getCurrentValue.bind(this))
  45. );
  46. validation.success.forEach(this.onSubmit.bind(this));
  47. validation.error.forEach(this.onError.bind(this));
  48. this.keypressObs = events.keypress
  49. .takeUntil(validation.success)
  50. .forEach(this.onKeypress.bind(this));
  51. // Init the prompt
  52. this.render();
  53. return this;
  54. }
  55. /**
  56. * Render the prompt to screen
  57. * @return {ExpandPrompt} self
  58. */
  59. render(error, hint) {
  60. var message = this.getQuestion();
  61. var bottomContent = '';
  62. if (this.status === 'answered') {
  63. message += chalk.cyan(this.answer);
  64. } else if (this.status === 'expanded') {
  65. var choicesStr = renderChoices(this.opt.choices, this.selectedKey);
  66. message += this.paginator.paginate(choicesStr, this.selectedKey, this.opt.pageSize);
  67. message += '\n Answer: ';
  68. }
  69. message += this.rl.line;
  70. if (error) {
  71. bottomContent = chalk.red('>> ') + error;
  72. }
  73. if (hint) {
  74. bottomContent = chalk.cyan('>> ') + hint;
  75. }
  76. this.screen.render(message, bottomContent);
  77. }
  78. getCurrentValue(input) {
  79. if (!input) {
  80. input = this.rawDefault;
  81. }
  82. var selected = this.opt.choices.where({ key: input.toLowerCase().trim() })[0];
  83. if (!selected) {
  84. return null;
  85. }
  86. return selected.value;
  87. }
  88. /**
  89. * Generate the prompt choices string
  90. * @return {String} Choices string
  91. */
  92. getChoices() {
  93. var output = '';
  94. this.opt.choices.forEach(choice => {
  95. output += '\n ';
  96. if (choice.type === 'separator') {
  97. output += ' ' + choice;
  98. return;
  99. }
  100. var choiceStr = choice.key + ') ' + choice.name;
  101. if (this.selectedKey === choice.key) {
  102. choiceStr = chalk.cyan(choiceStr);
  103. }
  104. output += choiceStr;
  105. });
  106. return output;
  107. }
  108. onError(state) {
  109. if (state.value === 'help') {
  110. this.selectedKey = '';
  111. this.status = 'expanded';
  112. this.render();
  113. return;
  114. }
  115. this.render(state.isValid);
  116. }
  117. /**
  118. * When user press `enter` key
  119. */
  120. onSubmit(state) {
  121. this.status = 'answered';
  122. var choice = this.opt.choices.where({ value: state.value })[0];
  123. this.answer = choice.short || choice.name;
  124. // Re-render prompt
  125. this.render();
  126. this.screen.done();
  127. this.done(state.value);
  128. }
  129. /**
  130. * When user press a key
  131. */
  132. onKeypress() {
  133. this.selectedKey = this.rl.line.toLowerCase();
  134. var selected = this.opt.choices.where({ key: this.selectedKey })[0];
  135. if (this.status === 'expanded') {
  136. this.render();
  137. } else {
  138. this.render(null, selected ? selected.name : null);
  139. }
  140. }
  141. /**
  142. * Validate the choices
  143. * @param {Array} choices
  144. */
  145. validateChoices(choices) {
  146. var formatError;
  147. var errors = [];
  148. var keymap = {};
  149. choices.filter(Separator.exclude).forEach(choice => {
  150. if (!choice.key || choice.key.length !== 1) {
  151. formatError = true;
  152. }
  153. if (keymap[choice.key]) {
  154. errors.push(choice.key);
  155. }
  156. keymap[choice.key] = true;
  157. choice.key = String(choice.key).toLowerCase();
  158. });
  159. if (formatError) {
  160. throw new Error(
  161. 'Format error: `key` param must be a single letter and is required.'
  162. );
  163. }
  164. if (keymap.h) {
  165. throw new Error(
  166. 'Reserved key error: `key` param cannot be `h` - this value is reserved.'
  167. );
  168. }
  169. if (errors.length) {
  170. throw new Error(
  171. 'Duplicate key error: `key` param must be unique. Duplicates: ' +
  172. _.uniq(errors).join(', ')
  173. );
  174. }
  175. }
  176. /**
  177. * Generate a string out of the choices keys
  178. * @param {Array} choices
  179. * @param {Number|String} default - the choice index or name to capitalize
  180. * @return {String} The rendered choices key string
  181. */
  182. generateChoicesString(choices, defaultChoice) {
  183. var defIndex = choices.realLength - 1;
  184. if (_.isNumber(defaultChoice) && this.opt.choices.getChoice(defaultChoice)) {
  185. defIndex = defaultChoice;
  186. } else if (_.isString(defaultChoice)) {
  187. let index = _.findIndex(
  188. choices.realChoices,
  189. ({ value }) => value === defaultChoice
  190. );
  191. defIndex = index === -1 ? defIndex : index;
  192. }
  193. var defStr = this.opt.choices.pluck('key');
  194. this.rawDefault = defStr[defIndex];
  195. defStr[defIndex] = String(defStr[defIndex]).toUpperCase();
  196. return defStr.join('');
  197. }
  198. }
  199. /**
  200. * Function for rendering checkbox choices
  201. * @param {String} pointer Selected key
  202. * @return {String} Rendered content
  203. */
  204. function renderChoices(choices, pointer) {
  205. var output = '';
  206. choices.forEach(choice => {
  207. output += '\n ';
  208. if (choice.type === 'separator') {
  209. output += ' ' + choice;
  210. return;
  211. }
  212. var choiceStr = choice.key + ') ' + choice.name;
  213. if (pointer === choice.key) {
  214. choiceStr = chalk.cyan(choiceStr);
  215. }
  216. output += choiceStr;
  217. });
  218. return output;
  219. }
  220. module.exports = ExpandPrompt;