UNPKG

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