UNPKG

5.37 kBJavaScriptView Raw
1'use strict';
2/**
3 * `list` type prompt
4 */
5
6const _ = {
7 isNumber: require('lodash/isNumber'),
8 findIndex: require('lodash/findIndex'),
9 isString: require('lodash/isString'),
10};
11const chalk = require('chalk');
12const figures = require('figures');
13const cliCursor = require('cli-cursor');
14const runAsync = require('run-async');
15const { flatMap, map, take, takeUntil } = require('rxjs/operators');
16const Base = require('./base');
17const observe = require('../utils/events');
18const Paginator = require('../utils/paginator');
19const incrementListIndex = require('../utils/incrementListIndex');
20
21class ListPrompt extends Base {
22 constructor(questions, rl, answers) {
23 super(questions, rl, answers);
24
25 if (!this.opt.choices) {
26 this.throwParamError('choices');
27 }
28
29 this.firstRender = true;
30 this.selected = 0;
31
32 const def = this.opt.default;
33
34 // If def is a Number, then use as index. Otherwise, check for value.
35 if (_.isNumber(def) && def >= 0 && def < this.opt.choices.realLength) {
36 this.selected = def;
37 } else if (!_.isNumber(def) && def != null) {
38 const index = _.findIndex(
39 this.opt.choices.realChoices,
40 ({ value }) => value === def
41 );
42 this.selected = Math.max(index, 0);
43 }
44
45 // Make sure no default is set (so it won't be printed)
46 this.opt.default = null;
47
48 const shouldLoop = this.opt.loop === undefined ? true : this.opt.loop;
49 this.paginator = new Paginator(this.screen, { isInfinite: shouldLoop });
50 }
51
52 /**
53 * Start the Inquiry session
54 * @param {Function} cb Callback when prompt is done
55 * @return {this}
56 */
57
58 _run(cb) {
59 this.done = cb;
60
61 const self = this;
62
63 const events = observe(this.rl);
64 events.normalizedUpKey.pipe(takeUntil(events.line)).forEach(this.onUpKey.bind(this));
65 events.normalizedDownKey
66 .pipe(takeUntil(events.line))
67 .forEach(this.onDownKey.bind(this));
68 events.numberKey.pipe(takeUntil(events.line)).forEach(this.onNumberKey.bind(this));
69 events.line
70 .pipe(
71 take(1),
72 map(this.getCurrentValue.bind(this)),
73 flatMap((value) =>
74 runAsync(self.opt.filter)(value, self.answers).catch((err) => err)
75 )
76 )
77 .forEach(this.onSubmit.bind(this));
78
79 // Init the prompt
80 cliCursor.hide();
81 this.render();
82
83 return this;
84 }
85
86 /**
87 * Render the prompt to screen
88 * @return {ListPrompt} self
89 */
90
91 render() {
92 // Render question
93 let message = this.getQuestion();
94
95 if (this.firstRender) {
96 message += chalk.dim('(Use arrow keys)');
97 }
98
99 // Render choices or answer depending on the state
100 if (this.status === 'answered') {
101 message += chalk.cyan(this.opt.choices.getChoice(this.selected).short);
102 } else {
103 const choicesStr = listRender(this.opt.choices, this.selected);
104 const indexPosition = this.opt.choices.indexOf(
105 this.opt.choices.getChoice(this.selected)
106 );
107 const realIndexPosition =
108 this.opt.choices.reduce((acc, value, i) => {
109 // Dont count lines past the choice we are looking at
110 if (i > indexPosition) {
111 return acc;
112 }
113 // Add line if it's a separator
114 if (value.type === 'separator') {
115 return acc + 1;
116 }
117
118 let l = value.name;
119 // Non-strings take up one line
120 if (typeof l !== 'string') {
121 return acc + 1;
122 }
123
124 // Calculate lines taken up by string
125 l = l.split('\n');
126 return acc + l.length;
127 }, 0) - 1;
128 message +=
129 '\n' + this.paginator.paginate(choicesStr, realIndexPosition, this.opt.pageSize);
130 }
131
132 this.firstRender = false;
133
134 this.screen.render(message);
135 }
136
137 /**
138 * When user press `enter` key
139 */
140
141 onSubmit(value) {
142 this.status = 'answered';
143
144 // Rerender prompt
145 this.render();
146
147 this.screen.done();
148 cliCursor.show();
149 this.done(value);
150 }
151
152 getCurrentValue() {
153 return this.opt.choices.getChoice(this.selected).value;
154 }
155
156 /**
157 * When user press a key
158 */
159 onUpKey() {
160 this.selected = incrementListIndex(this.selected, 'up', this.opt);
161 this.render();
162 }
163
164 onDownKey() {
165 this.selected = incrementListIndex(this.selected, 'down', this.opt);
166 this.render();
167 }
168
169 onNumberKey(input) {
170 if (input <= this.opt.choices.realLength) {
171 this.selected = input - 1;
172 }
173
174 this.render();
175 }
176}
177
178/**
179 * Function for rendering list choices
180 * @param {Number} pointer Position of the pointer
181 * @return {String} Rendered content
182 */
183function listRender(choices, pointer) {
184 let output = '';
185 let separatorOffset = 0;
186
187 choices.forEach((choice, i) => {
188 if (choice.type === 'separator') {
189 separatorOffset++;
190 output += ' ' + choice + '\n';
191 return;
192 }
193
194 if (choice.disabled) {
195 separatorOffset++;
196 output += ' - ' + choice.name;
197 output += ' (' + (_.isString(choice.disabled) ? choice.disabled : 'Disabled') + ')';
198 output += '\n';
199 return;
200 }
201
202 const isSelected = i - separatorOffset === pointer;
203 let line = (isSelected ? figures.pointer + ' ' : ' ') + choice.name;
204 if (isSelected) {
205 line = chalk.cyan(line);
206 }
207
208 output += line + ' \n';
209 });
210
211 return output.replace(/\n$/, '');
212}
213
214module.exports = ListPrompt;