[6a3a178] | 1 | 'use strict';
|
---|
| 2 | /**
|
---|
| 3 | * `list` type prompt
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | const _ = {
|
---|
| 7 | isArray: require('lodash/isArray'),
|
---|
| 8 | map: require('lodash/map'),
|
---|
| 9 | isString: require('lodash/isString'),
|
---|
| 10 | };
|
---|
| 11 | const chalk = require('chalk');
|
---|
| 12 | const cliCursor = require('cli-cursor');
|
---|
| 13 | const figures = require('figures');
|
---|
| 14 | const { map, takeUntil } = require('rxjs/operators');
|
---|
| 15 | const Base = require('./base');
|
---|
| 16 | const observe = require('../utils/events');
|
---|
| 17 | const Paginator = require('../utils/paginator');
|
---|
| 18 | const incrementListIndex = require('../utils/incrementListIndex');
|
---|
| 19 |
|
---|
| 20 | class CheckboxPrompt extends Base {
|
---|
| 21 | constructor(questions, rl, answers) {
|
---|
| 22 | super(questions, rl, answers);
|
---|
| 23 |
|
---|
| 24 | if (!this.opt.choices) {
|
---|
| 25 | this.throwParamError('choices');
|
---|
| 26 | }
|
---|
| 27 |
|
---|
| 28 | if (_.isArray(this.opt.default)) {
|
---|
| 29 | this.opt.choices.forEach(function (choice) {
|
---|
| 30 | if (this.opt.default.indexOf(choice.value) >= 0) {
|
---|
| 31 | choice.checked = true;
|
---|
| 32 | }
|
---|
| 33 | }, this);
|
---|
| 34 | }
|
---|
| 35 |
|
---|
| 36 | this.pointer = 0;
|
---|
| 37 |
|
---|
| 38 | // Make sure no default is set (so it won't be printed)
|
---|
| 39 | this.opt.default = null;
|
---|
| 40 |
|
---|
| 41 | const shouldLoop = this.opt.loop === undefined ? true : this.opt.loop;
|
---|
| 42 | this.paginator = new Paginator(this.screen, { isInfinite: shouldLoop });
|
---|
| 43 | }
|
---|
| 44 |
|
---|
| 45 | /**
|
---|
| 46 | * Start the Inquiry session
|
---|
| 47 | * @param {Function} cb Callback when prompt is done
|
---|
| 48 | * @return {this}
|
---|
| 49 | */
|
---|
| 50 |
|
---|
| 51 | _run(cb) {
|
---|
| 52 | this.done = cb;
|
---|
| 53 |
|
---|
| 54 | const events = observe(this.rl);
|
---|
| 55 |
|
---|
| 56 | const validation = this.handleSubmitEvents(
|
---|
| 57 | events.line.pipe(map(this.getCurrentValue.bind(this)))
|
---|
| 58 | );
|
---|
| 59 | validation.success.forEach(this.onEnd.bind(this));
|
---|
| 60 | validation.error.forEach(this.onError.bind(this));
|
---|
| 61 |
|
---|
| 62 | events.normalizedUpKey
|
---|
| 63 | .pipe(takeUntil(validation.success))
|
---|
| 64 | .forEach(this.onUpKey.bind(this));
|
---|
| 65 | events.normalizedDownKey
|
---|
| 66 | .pipe(takeUntil(validation.success))
|
---|
| 67 | .forEach(this.onDownKey.bind(this));
|
---|
| 68 | events.numberKey
|
---|
| 69 | .pipe(takeUntil(validation.success))
|
---|
| 70 | .forEach(this.onNumberKey.bind(this));
|
---|
| 71 | events.spaceKey
|
---|
| 72 | .pipe(takeUntil(validation.success))
|
---|
| 73 | .forEach(this.onSpaceKey.bind(this));
|
---|
| 74 | events.aKey.pipe(takeUntil(validation.success)).forEach(this.onAllKey.bind(this));
|
---|
| 75 | events.iKey.pipe(takeUntil(validation.success)).forEach(this.onInverseKey.bind(this));
|
---|
| 76 |
|
---|
| 77 | // Init the prompt
|
---|
| 78 | cliCursor.hide();
|
---|
| 79 | this.render();
|
---|
| 80 | this.firstRender = false;
|
---|
| 81 |
|
---|
| 82 | return this;
|
---|
| 83 | }
|
---|
| 84 |
|
---|
| 85 | /**
|
---|
| 86 | * Render the prompt to screen
|
---|
| 87 | * @return {CheckboxPrompt} self
|
---|
| 88 | */
|
---|
| 89 |
|
---|
| 90 | render(error) {
|
---|
| 91 | // Render question
|
---|
| 92 | let message = this.getQuestion();
|
---|
| 93 | let bottomContent = '';
|
---|
| 94 |
|
---|
| 95 | if (!this.spaceKeyPressed) {
|
---|
| 96 | message +=
|
---|
| 97 | '(Press ' +
|
---|
| 98 | chalk.cyan.bold('<space>') +
|
---|
| 99 | ' to select, ' +
|
---|
| 100 | chalk.cyan.bold('<a>') +
|
---|
| 101 | ' to toggle all, ' +
|
---|
| 102 | chalk.cyan.bold('<i>') +
|
---|
| 103 | ' to invert selection)';
|
---|
| 104 | }
|
---|
| 105 |
|
---|
| 106 | // Render choices or answer depending on the state
|
---|
| 107 | if (this.status === 'answered') {
|
---|
| 108 | message += chalk.cyan(this.selection.join(', '));
|
---|
| 109 | } else {
|
---|
| 110 | const choicesStr = renderChoices(this.opt.choices, this.pointer);
|
---|
| 111 | const indexPosition = this.opt.choices.indexOf(
|
---|
| 112 | this.opt.choices.getChoice(this.pointer)
|
---|
| 113 | );
|
---|
| 114 | const realIndexPosition =
|
---|
| 115 | this.opt.choices.reduce((acc, value, i) => {
|
---|
| 116 | // Dont count lines past the choice we are looking at
|
---|
| 117 | if (i > indexPosition) {
|
---|
| 118 | return acc;
|
---|
| 119 | }
|
---|
| 120 | // Add line if it's a separator
|
---|
| 121 | if (value.type === 'separator') {
|
---|
| 122 | return acc + 1;
|
---|
| 123 | }
|
---|
| 124 |
|
---|
| 125 | let l = value.name;
|
---|
| 126 | // Non-strings take up one line
|
---|
| 127 | if (typeof l !== 'string') {
|
---|
| 128 | return acc + 1;
|
---|
| 129 | }
|
---|
| 130 |
|
---|
| 131 | // Calculate lines taken up by string
|
---|
| 132 | l = l.split('\n');
|
---|
| 133 | return acc + l.length;
|
---|
| 134 | }, 0) - 1;
|
---|
| 135 | message +=
|
---|
| 136 | '\n' + this.paginator.paginate(choicesStr, realIndexPosition, this.opt.pageSize);
|
---|
| 137 | }
|
---|
| 138 |
|
---|
| 139 | if (error) {
|
---|
| 140 | bottomContent = chalk.red('>> ') + error;
|
---|
| 141 | }
|
---|
| 142 |
|
---|
| 143 | this.screen.render(message, bottomContent);
|
---|
| 144 | }
|
---|
| 145 |
|
---|
| 146 | /**
|
---|
| 147 | * When user press `enter` key
|
---|
| 148 | */
|
---|
| 149 |
|
---|
| 150 | onEnd(state) {
|
---|
| 151 | this.status = 'answered';
|
---|
| 152 | this.spaceKeyPressed = true;
|
---|
| 153 | // Rerender prompt (and clean subline error)
|
---|
| 154 | this.render();
|
---|
| 155 |
|
---|
| 156 | this.screen.done();
|
---|
| 157 | cliCursor.show();
|
---|
| 158 | this.done(state.value);
|
---|
| 159 | }
|
---|
| 160 |
|
---|
| 161 | onError(state) {
|
---|
| 162 | this.render(state.isValid);
|
---|
| 163 | }
|
---|
| 164 |
|
---|
| 165 | getCurrentValue() {
|
---|
| 166 | const choices = this.opt.choices.filter(
|
---|
| 167 | (choice) => Boolean(choice.checked) && !choice.disabled
|
---|
| 168 | );
|
---|
| 169 |
|
---|
| 170 | this.selection = _.map(choices, 'short');
|
---|
| 171 | return _.map(choices, 'value');
|
---|
| 172 | }
|
---|
| 173 |
|
---|
| 174 | onUpKey() {
|
---|
| 175 | this.pointer = incrementListIndex(this.pointer, 'up', this.opt);
|
---|
| 176 | this.render();
|
---|
| 177 | }
|
---|
| 178 |
|
---|
| 179 | onDownKey() {
|
---|
| 180 | this.pointer = incrementListIndex(this.pointer, 'down', this.opt);
|
---|
| 181 | this.render();
|
---|
| 182 | }
|
---|
| 183 |
|
---|
| 184 | onNumberKey(input) {
|
---|
| 185 | if (input <= this.opt.choices.realLength) {
|
---|
| 186 | this.pointer = input - 1;
|
---|
| 187 | this.toggleChoice(this.pointer);
|
---|
| 188 | }
|
---|
| 189 |
|
---|
| 190 | this.render();
|
---|
| 191 | }
|
---|
| 192 |
|
---|
| 193 | onSpaceKey() {
|
---|
| 194 | this.spaceKeyPressed = true;
|
---|
| 195 | this.toggleChoice(this.pointer);
|
---|
| 196 | this.render();
|
---|
| 197 | }
|
---|
| 198 |
|
---|
| 199 | onAllKey() {
|
---|
| 200 | const shouldBeChecked = Boolean(
|
---|
| 201 | this.opt.choices.find((choice) => choice.type !== 'separator' && !choice.checked)
|
---|
| 202 | );
|
---|
| 203 |
|
---|
| 204 | this.opt.choices.forEach((choice) => {
|
---|
| 205 | if (choice.type !== 'separator') {
|
---|
| 206 | choice.checked = shouldBeChecked;
|
---|
| 207 | }
|
---|
| 208 | });
|
---|
| 209 |
|
---|
| 210 | this.render();
|
---|
| 211 | }
|
---|
| 212 |
|
---|
| 213 | onInverseKey() {
|
---|
| 214 | this.opt.choices.forEach((choice) => {
|
---|
| 215 | if (choice.type !== 'separator') {
|
---|
| 216 | choice.checked = !choice.checked;
|
---|
| 217 | }
|
---|
| 218 | });
|
---|
| 219 |
|
---|
| 220 | this.render();
|
---|
| 221 | }
|
---|
| 222 |
|
---|
| 223 | toggleChoice(index) {
|
---|
| 224 | const item = this.opt.choices.getChoice(index);
|
---|
| 225 | if (item !== undefined) {
|
---|
| 226 | this.opt.choices.getChoice(index).checked = !item.checked;
|
---|
| 227 | }
|
---|
| 228 | }
|
---|
| 229 | }
|
---|
| 230 |
|
---|
| 231 | /**
|
---|
| 232 | * Function for rendering checkbox choices
|
---|
| 233 | * @param {Number} pointer Position of the pointer
|
---|
| 234 | * @return {String} Rendered content
|
---|
| 235 | */
|
---|
| 236 |
|
---|
| 237 | function renderChoices(choices, pointer) {
|
---|
| 238 | let output = '';
|
---|
| 239 | let separatorOffset = 0;
|
---|
| 240 |
|
---|
| 241 | choices.forEach((choice, i) => {
|
---|
| 242 | if (choice.type === 'separator') {
|
---|
| 243 | separatorOffset++;
|
---|
| 244 | output += ' ' + choice + '\n';
|
---|
| 245 | return;
|
---|
| 246 | }
|
---|
| 247 |
|
---|
| 248 | if (choice.disabled) {
|
---|
| 249 | separatorOffset++;
|
---|
| 250 | output += ' - ' + choice.name;
|
---|
| 251 | output += ' (' + (_.isString(choice.disabled) ? choice.disabled : 'Disabled') + ')';
|
---|
| 252 | } else {
|
---|
| 253 | const line = getCheckbox(choice.checked) + ' ' + choice.name;
|
---|
| 254 | if (i - separatorOffset === pointer) {
|
---|
| 255 | output += chalk.cyan(figures.pointer + line);
|
---|
| 256 | } else {
|
---|
| 257 | output += ' ' + line;
|
---|
| 258 | }
|
---|
| 259 | }
|
---|
| 260 |
|
---|
| 261 | output += '\n';
|
---|
| 262 | });
|
---|
| 263 |
|
---|
| 264 | return output.replace(/\n$/, '');
|
---|
| 265 | }
|
---|
| 266 |
|
---|
| 267 | /**
|
---|
| 268 | * Get the checkbox
|
---|
| 269 | * @param {Boolean} checked - add a X or not to the checkbox
|
---|
| 270 | * @return {String} Composited checkbox string
|
---|
| 271 | */
|
---|
| 272 |
|
---|
| 273 | function getCheckbox(checked) {
|
---|
| 274 | return checked ? chalk.green(figures.radioOn) : figures.radioOff;
|
---|
| 275 | }
|
---|
| 276 |
|
---|
| 277 | module.exports = CheckboxPrompt;
|
---|