1 | 'use strict';
|
---|
2 | /**
|
---|
3 | * `rawlist` type prompt
|
---|
4 | */
|
---|
5 |
|
---|
6 | const _ = {
|
---|
7 | uniq: require('lodash/uniq'),
|
---|
8 | isString: require('lodash/isString'),
|
---|
9 | isNumber: require('lodash/isNumber'),
|
---|
10 | findIndex: require('lodash/findIndex'),
|
---|
11 | };
|
---|
12 | const chalk = require('chalk');
|
---|
13 | const { map, takeUntil } = require('rxjs/operators');
|
---|
14 | const Base = require('./base');
|
---|
15 | const Separator = require('../objects/separator');
|
---|
16 | const observe = require('../utils/events');
|
---|
17 | const Paginator = require('../utils/paginator');
|
---|
18 |
|
---|
19 | class 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 |
|
---|
260 | function 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 |
|
---|
282 | module.exports = ExpandPrompt;
|
---|