inquirer-ts-checkbox-plus-prompt
Version:
Checkbox with autocomplete and other additions for Inquirer
260 lines (257 loc) • 9.71 kB
JavaScript
import chalk from 'chalk';
import cliCursor from 'cli-cursor';
import figures from 'figures';
import Choices from 'inquirer/lib/objects/choices';
import Base from 'inquirer/lib/prompts/base';
import observe from 'inquirer/lib/utils/events';
import Paginator from 'inquirer/lib/utils/paginator';
import { map, takeUntil } from 'rxjs/operators';
class CheckboxPlusPrompt extends Base {
constructor(questions, rl, answers) {
super(questions, rl, answers);
this.pointer = 0;
if (this.opt.highlight == null) {
this.opt.highlight = false;
}
if (typeof this.opt.searchable == null) {
this.opt.searchable = false;
}
if (typeof this.opt.default == null) {
this.opt.default = undefined;
}
if (this.opt.source == null) {
this.throwParamError('source');
}
this.pointer = 0;
this.firstSourceLoading = true;
this.choices = new Choices([], answers);
this.checkedChoices = [];
this.value = [];
this.lastQuery = undefined;
this.searching = false;
this.lastSourcePromise = undefined;
this.default = this.opt.default;
this.opt.default = undefined;
this.selection = [];
this.done = undefined;
this.paginator = new Paginator(this.screen);
}
_run(callback) {
this.done = callback;
this.executeSource().then(() => {
const events = observe(this.rl);
const validation = this.handleSubmitEvents(events.line.pipe(map(this.getCurrentValue.bind(this))));
validation.success.forEach(this.onEnd.bind(this));
validation.error.forEach(this.onError.bind(this));
events.normalizedUpKey.pipe(takeUntil(validation.success)).forEach(this.onUpKey.bind(this));
events.normalizedDownKey
.pipe(takeUntil(validation.success))
.forEach(this.onDownKey.bind(this));
events.spaceKey.pipe(takeUntil(validation.success)).forEach(this.onSpaceKey.bind(this));
if (this.opt.searchable === false) {
events.numberKey.pipe(takeUntil(validation.success)).forEach(this.onNumberKey.bind(this));
events.aKey.pipe(takeUntil(validation.success)).forEach(this.onAllKey.bind(this));
events.iKey.pipe(takeUntil(validation.success)).forEach(this.onInverseKey.bind(this));
}
else {
events.keypress.pipe(takeUntil(validation.success)).forEach(this.onKeypress.bind(this));
}
if (this.rl.line) {
this.onKeypress();
}
cliCursor.hide();
this.render();
});
return this;
}
getValue(choice) {
if (choice.type === 'separator') {
return undefined;
}
return choice.value;
}
executeSource() {
let sourcePromise;
this.rl.line = this.rl.line.trim();
if (this.rl.line === this.lastQuery) {
return Promise.resolve(undefined);
}
if (this.opt.searchable) {
sourcePromise = this.opt.source(this.answers, this.rl.line);
}
else {
sourcePromise = this.opt.source(this.answers, undefined);
}
this.lastQuery = this.rl.line;
this.lastSourcePromise = sourcePromise;
this.searching = true;
sourcePromise.then((choices) => {
if (this.lastSourcePromise !== sourcePromise) {
return;
}
this.searching = false;
this.choices = new Choices(choices, this.answers);
this.choices.forEach((choice) => {
if (this.value.some((eachValue) => this.getValue(choice) === eachValue)) {
this.toggleChoice(choice, true);
}
else {
this.toggleChoice(choice, false);
}
if (this.default != null) {
if (this.default.some((defaultValue) => this.getValue(choice) === defaultValue)) {
this.toggleChoice(choice, true);
}
}
});
this.pointer = 0;
this.render();
this.default = undefined;
this.firstSourceLoading = false;
});
return sourcePromise;
}
render(error) {
let message = this.getQuestion();
let bottomContent = '';
if (this.status === 'answered') {
message += chalk.cyan(this.selection.join(', '));
return this.screen.render(message, bottomContent);
}
if (this.firstSourceLoading) {
if (this.opt.searchable) {
message += `(Press ${chalk.cyan.bold('<space>')} to select, or type anything to filter the list)`;
}
else {
message += `(Press ${chalk.cyan.bold('<space>')} to select, ${chalk.cyan.bold('<a>')} to toggle all, ${chalk.cyan.bold('<i>')} to invert selection)`;
}
}
if (this.opt.searchable) {
message += this.rl.line;
}
if (this.searching) {
message += `\n ${chalk.cyan('Searching...')}`;
}
else if (!this.choices.length) {
message += `\n ${chalk.yellow('No results...')}`;
}
else {
const choicesStr = this.renderChoices(this.choices, this.pointer);
const choice = this.choices.getChoice(this.pointer);
const indexPosition = this.choices.indexOf(choice);
message += `\n${this.paginator.paginate(choicesStr, indexPosition, this.opt.pageSize)}`;
}
if (error) {
bottomContent = chalk.red('>> ') + error;
}
return this.screen.render(message, bottomContent);
}
onEnd(state) {
this.status = 'answered';
this.render();
this.screen.done();
cliCursor.show();
this.done?.('value' in state ? state.value : undefined);
}
onError(state) {
this.render(state.isValid);
}
getCurrentValue() {
this.selection = this.checkedChoices.map((checkedChoice) => checkedChoice.short);
const values = this.checkedChoices.map((checkedChoice) => checkedChoice.value);
return values;
}
onUpKey() {
const len = this.choices.realLength;
this.pointer = this.pointer > 0 ? this.pointer - 1 : len - 1;
this.render();
}
onDownKey() {
const len = this.choices.realLength;
this.pointer = this.pointer < len - 1 ? this.pointer + 1 : 0;
this.render();
}
onNumberKey(input) {
if (input <= this.choices.realLength) {
this.pointer = input - 1;
this.toggleChoice(this.choices.getChoice(this.pointer));
}
this.render();
}
onSpaceKey() {
if (this.choices.getChoice(this.pointer) == null) {
return;
}
this.toggleChoice(this.choices.getChoice(this.pointer));
this.render();
}
onAllKey() {
const shouldBeChecked = Boolean(this.choices.find((choice) => {
return choice.type !== 'separator' && !choice.checked;
}));
this.choices.forEach((choice) => {
if (choice.type !== 'separator') {
choice.checked = shouldBeChecked;
}
return choice;
});
this.render();
}
onInverseKey() {
this.choices.forEach((choice) => {
if (choice.type !== 'separator') {
choice.checked = !choice.checked;
}
});
this.render();
}
onKeypress() {
this.executeSource();
this.render();
}
toggleChoice(choice, nextChecked) {
if (choice.type === 'separator') {
return;
}
const checked = nextChecked == null ? !(choice.checked ?? false) : nextChecked;
this.value = this.value.filter((eachValue) => eachValue !== choice.value);
this.checkedChoices = this.checkedChoices.filter((checkedChoice) => checkedChoice.value !== choice.value);
choice.checked = false;
if (checked === true) {
this.value.push(choice.value);
this.checkedChoices.push(choice);
choice.checked = true;
}
}
static getCheckboxFigure(checked = false) {
return checked ? chalk.green(figures.radioOn) : figures.radioOff;
}
renderChoices(choices, pointer) {
const output = [];
let separatorOffset = 0;
choices.forEach((choice, index) => {
if (choice.type === 'separator') {
separatorOffset += 1;
output.push(` ${choice}\n`);
return;
}
if (choice.disabled) {
separatorOffset += 1;
output.push(` - ${choice.name} (${typeof choice.disabled === 'string' ? choice.disabled : 'Disabled'})\n`);
return;
}
if (index - separatorOffset === pointer) {
output.push(chalk.cyan(figures.pointer));
output.push(CheckboxPlusPrompt.getCheckboxFigure(choice.checked));
output.push(' ');
output.push(this.opt.highlight ? chalk.gray(choice.name) : choice.name);
}
else {
output.push(` ${CheckboxPlusPrompt.getCheckboxFigure(choice.checked)} ${choice.name}`);
}
output.push('\n');
});
return output.join('').replace(/\n$/, '');
}
}
export { CheckboxPlusPrompt };