You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
5.2 KiB
209 lines
5.2 KiB
'use strict'; |
|
/** |
|
* `list` type prompt |
|
*/ |
|
|
|
var _ = { |
|
isNumber: require('lodash/isNumber'), |
|
findIndex: require('lodash/findIndex'), |
|
isString: require('lodash/isString'), |
|
}; |
|
var chalk = require('chalk'); |
|
var figures = require('figures'); |
|
var cliCursor = require('cli-cursor'); |
|
var runAsync = require('run-async'); |
|
var { flatMap, map, take, takeUntil } = require('rxjs/operators'); |
|
var Base = require('./base'); |
|
var observe = require('../utils/events'); |
|
var Paginator = require('../utils/paginator'); |
|
var incrementListIndex = require('../utils/incrementListIndex'); |
|
|
|
class ListPrompt extends Base { |
|
constructor(questions, rl, answers) { |
|
super(questions, rl, answers); |
|
|
|
if (!this.opt.choices) { |
|
this.throwParamError('choices'); |
|
} |
|
|
|
this.firstRender = true; |
|
this.selected = 0; |
|
|
|
var def = this.opt.default; |
|
|
|
// If def is a Number, then use as index. Otherwise, check for value. |
|
if (_.isNumber(def) && def >= 0 && def < this.opt.choices.realLength) { |
|
this.selected = def; |
|
} else if (!_.isNumber(def) && def != null) { |
|
let index = _.findIndex(this.opt.choices.realChoices, ({ value }) => value === def); |
|
this.selected = Math.max(index, 0); |
|
} |
|
|
|
// Make sure no default is set (so it won't be printed) |
|
this.opt.default = null; |
|
|
|
const shouldLoop = this.opt.loop === undefined ? true : this.opt.loop; |
|
this.paginator = new Paginator(this.screen, { isInfinite: shouldLoop }); |
|
} |
|
|
|
/** |
|
* Start the Inquiry session |
|
* @param {Function} cb Callback when prompt is done |
|
* @return {this} |
|
*/ |
|
|
|
_run(cb) { |
|
this.done = cb; |
|
|
|
var self = this; |
|
|
|
var events = observe(this.rl); |
|
events.normalizedUpKey.pipe(takeUntil(events.line)).forEach(this.onUpKey.bind(this)); |
|
events.normalizedDownKey |
|
.pipe(takeUntil(events.line)) |
|
.forEach(this.onDownKey.bind(this)); |
|
events.numberKey.pipe(takeUntil(events.line)).forEach(this.onNumberKey.bind(this)); |
|
events.line |
|
.pipe( |
|
take(1), |
|
map(this.getCurrentValue.bind(this)), |
|
flatMap((value) => runAsync(self.opt.filter)(value).catch((err) => err)) |
|
) |
|
.forEach(this.onSubmit.bind(this)); |
|
|
|
// Init the prompt |
|
cliCursor.hide(); |
|
this.render(); |
|
|
|
return this; |
|
} |
|
|
|
/** |
|
* Render the prompt to screen |
|
* @return {ListPrompt} self |
|
*/ |
|
|
|
render() { |
|
// Render question |
|
var message = this.getQuestion(); |
|
|
|
if (this.firstRender) { |
|
message += chalk.dim('(Use arrow keys)'); |
|
} |
|
|
|
// Render choices or answer depending on the state |
|
if (this.status === 'answered') { |
|
message += chalk.cyan(this.opt.choices.getChoice(this.selected).short); |
|
} else { |
|
var choicesStr = listRender(this.opt.choices, this.selected); |
|
var indexPosition = this.opt.choices.indexOf( |
|
this.opt.choices.getChoice(this.selected) |
|
); |
|
var realIndexPosition = |
|
this.opt.choices.reduce(function (acc, value, i) { |
|
// Dont count lines past the choice we are looking at |
|
if (i > indexPosition) { |
|
return acc; |
|
} |
|
// Add line if it's a separator |
|
if (value.type === 'separator') { |
|
return acc + 1; |
|
} |
|
|
|
var l = value.name; |
|
// Non-strings take up one line |
|
if (typeof l !== 'string') { |
|
return acc + 1; |
|
} |
|
|
|
// Calculate lines taken up by string |
|
l = l.split('\n'); |
|
return acc + l.length; |
|
}, 0) - 1; |
|
message += |
|
'\n' + this.paginator.paginate(choicesStr, realIndexPosition, this.opt.pageSize); |
|
} |
|
|
|
this.firstRender = false; |
|
|
|
this.screen.render(message); |
|
} |
|
|
|
/** |
|
* When user press `enter` key |
|
*/ |
|
|
|
onSubmit(value) { |
|
this.status = 'answered'; |
|
|
|
// Rerender prompt |
|
this.render(); |
|
|
|
this.screen.done(); |
|
cliCursor.show(); |
|
this.done(value); |
|
} |
|
|
|
getCurrentValue() { |
|
return this.opt.choices.getChoice(this.selected).value; |
|
} |
|
|
|
/** |
|
* When user press a key |
|
*/ |
|
onUpKey() { |
|
this.selected = incrementListIndex(this.selected, 'up', this.opt); |
|
this.render(); |
|
} |
|
|
|
onDownKey() { |
|
this.selected = incrementListIndex(this.selected, 'down', this.opt); |
|
this.render(); |
|
} |
|
|
|
onNumberKey(input) { |
|
if (input <= this.opt.choices.realLength) { |
|
this.selected = input - 1; |
|
} |
|
|
|
this.render(); |
|
} |
|
} |
|
|
|
/** |
|
* Function for rendering list choices |
|
* @param {Number} pointer Position of the pointer |
|
* @return {String} Rendered content |
|
*/ |
|
function listRender(choices, pointer) { |
|
var output = ''; |
|
var separatorOffset = 0; |
|
|
|
choices.forEach((choice, i) => { |
|
if (choice.type === 'separator') { |
|
separatorOffset++; |
|
output += ' ' + choice + '\n'; |
|
return; |
|
} |
|
|
|
if (choice.disabled) { |
|
separatorOffset++; |
|
output += ' - ' + choice.name; |
|
output += ' (' + (_.isString(choice.disabled) ? choice.disabled : 'Disabled') + ')'; |
|
output += '\n'; |
|
return; |
|
} |
|
|
|
var isSelected = i - separatorOffset === pointer; |
|
var line = (isSelected ? figures.pointer + ' ' : ' ') + choice.name; |
|
if (isSelected) { |
|
line = chalk.cyan(line); |
|
} |
|
|
|
output += line + ' \n'; |
|
}); |
|
|
|
return output.replace(/\n$/, ''); |
|
} |
|
|
|
module.exports = ListPrompt;
|
|
|