rawlist.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. /**
  2. * `rawlist` type prompt
  3. */
  4. import chalk from 'chalk';
  5. import { map, takeUntil } from 'rxjs';
  6. import Base from './base.js';
  7. import Separator from '../objects/separator.js';
  8. import observe from '../utils/events.js';
  9. import Paginator from '../utils/paginator.js';
  10. import incrementListIndex from '../utils/incrementListIndex.js';
  11. export default class RawListPrompt extends Base {
  12. constructor(questions, rl, answers) {
  13. super(questions, rl, answers);
  14. this.hiddenLine = '';
  15. this.lastKey = '';
  16. if (!this.opt.choices) {
  17. this.throwParamError('choices');
  18. }
  19. this.opt.validChoices = this.opt.choices.filter(Separator.exclude);
  20. this.selected = 0;
  21. this.rawDefault = 0;
  22. Object.assign(this.opt, {
  23. validate(val) {
  24. return val != null;
  25. },
  26. });
  27. const def = this.opt.default;
  28. if (typeof def === 'number' && def >= 0 && def < this.opt.choices.realLength) {
  29. this.selected = def;
  30. this.rawDefault = def;
  31. } else if (typeof def !== 'number' && def != null) {
  32. const index = this.opt.choices.realChoices.findIndex(({ value }) => value === def);
  33. const safeIndex = Math.max(index, 0);
  34. this.selected = safeIndex;
  35. this.rawDefault = safeIndex;
  36. }
  37. // Make sure no default is set (so it won't be printed)
  38. this.opt.default = null;
  39. const shouldLoop = this.opt.loop === undefined ? true : this.opt.loop;
  40. this.paginator = new Paginator(undefined, { isInfinite: shouldLoop });
  41. }
  42. /**
  43. * Start the Inquiry session
  44. * @param {Function} cb Callback when prompt is done
  45. * @return {this}
  46. */
  47. _run(cb) {
  48. this.done = cb;
  49. // Once user confirm (enter key)
  50. const events = observe(this.rl);
  51. const submit = events.line.pipe(map(this.getCurrentValue.bind(this)));
  52. const validation = this.handleSubmitEvents(submit);
  53. validation.success.forEach(this.onEnd.bind(this));
  54. validation.error.forEach(this.onError.bind(this));
  55. events.normalizedUpKey
  56. .pipe(takeUntil(validation.success))
  57. .forEach(this.onUpKey.bind(this));
  58. events.normalizedDownKey
  59. .pipe(takeUntil(validation.success))
  60. .forEach(this.onDownKey.bind(this));
  61. events.keypress
  62. .pipe(takeUntil(validation.success))
  63. .forEach(this.onKeypress.bind(this));
  64. // Init the prompt
  65. this.render();
  66. return this;
  67. }
  68. /**
  69. * Render the prompt to screen
  70. * @return {RawListPrompt} self
  71. */
  72. render(error) {
  73. // Render question
  74. let message = this.getQuestion();
  75. let bottomContent = '';
  76. if (this.status === 'answered') {
  77. message += chalk.cyan(this.opt.choices.getChoice(this.selected).short);
  78. } else {
  79. const choicesStr = renderChoices(this.opt.choices, this.selected);
  80. message +=
  81. '\n' + this.paginator.paginate(choicesStr, this.selected, this.opt.pageSize);
  82. message += '\n Answer: ';
  83. }
  84. message += this.rl.line;
  85. if (error) {
  86. bottomContent = '\n' + chalk.red('>> ') + error;
  87. }
  88. this.screen.render(message, bottomContent);
  89. }
  90. /**
  91. * When user press `enter` key
  92. */
  93. getCurrentValue(index) {
  94. if (index == null) {
  95. index = this.rawDefault;
  96. } else if (index === '') {
  97. this.selected = this.selected === undefined ? -1 : this.selected;
  98. index = this.selected;
  99. } else {
  100. index -= 1;
  101. }
  102. const choice = this.opt.choices.getChoice(index);
  103. return choice ? choice.value : null;
  104. }
  105. onEnd(state) {
  106. this.status = 'answered';
  107. this.answer = state.value;
  108. // Re-render prompt
  109. this.render();
  110. this.screen.done();
  111. this.done(state.value);
  112. }
  113. onError() {
  114. this.render('Please enter a valid index');
  115. }
  116. /**
  117. * When user press a key
  118. */
  119. onKeypress() {
  120. let index;
  121. if (this.lastKey === 'arrow') {
  122. index = this.hiddenLine.length ? Number(this.hiddenLine) - 1 : 0;
  123. } else {
  124. index = this.rl.line.length ? Number(this.rl.line) - 1 : 0;
  125. }
  126. this.lastKey = '';
  127. if (this.opt.choices.getChoice(index)) {
  128. this.selected = index;
  129. } else {
  130. this.selected = undefined;
  131. }
  132. this.render();
  133. }
  134. /**
  135. * When user press up key
  136. */
  137. onUpKey() {
  138. this.onArrowKey('up');
  139. }
  140. /**
  141. * When user press down key
  142. */
  143. onDownKey() {
  144. this.onArrowKey('down');
  145. }
  146. /**
  147. * When user press up or down key
  148. * @param {String} type Arrow type: up or down
  149. */
  150. onArrowKey(type) {
  151. this.selected = incrementListIndex(this.selected, type, this.opt) || 0;
  152. this.hiddenLine = String(this.selected + 1);
  153. this.rl.line = '';
  154. this.lastKey = 'arrow';
  155. }
  156. }
  157. /**
  158. * Function for rendering list choices
  159. * @param {Number} pointer Position of the pointer
  160. * @return {String} Rendered content
  161. */
  162. function renderChoices(choices, pointer) {
  163. let output = '';
  164. let separatorOffset = 0;
  165. choices.forEach((choice, i) => {
  166. output += output ? '\n ' : ' ';
  167. if (choice.type === 'separator') {
  168. separatorOffset++;
  169. output += ' ' + choice;
  170. return;
  171. }
  172. const index = i - separatorOffset;
  173. let display = index + 1 + ') ' + choice.name;
  174. if (index === pointer) {
  175. display = chalk.cyan(display);
  176. }
  177. output += display;
  178. });
  179. return output;
  180. }