[6a3a178] | 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;
|
---|