Added test to ensure that release notes are parsed properly Updated to latest dependencies to fix security vulnerability Updated GH Actions Workflow to run test before updating tag Updated README.md to reflect proper whatsnew documentation.
268 lines
7.0 KiB
JavaScript
268 lines
7.0 KiB
JavaScript
'use strict';
|
|
|
|
const color = require('kleur');
|
|
const { cursor } = require('sisteransi');
|
|
const Prompt = require('./prompt');
|
|
const { clear, figures, style, wrap, entriesToDisplay } = require('../util');
|
|
|
|
/**
|
|
* MultiselectPrompt Base Element
|
|
* @param {Object} opts Options
|
|
* @param {String} opts.message Message
|
|
* @param {Array} opts.choices Array of choice objects
|
|
* @param {String} [opts.hint] Hint to display
|
|
* @param {String} [opts.warn] Hint shown for disabled choices
|
|
* @param {Number} [opts.max] Max choices
|
|
* @param {Number} [opts.cursor=0] Cursor start position
|
|
* @param {Number} [opts.optionsPerPage=10] Max options to display at once
|
|
* @param {Stream} [opts.stdin] The Readable stream to listen to
|
|
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
|
|
*/
|
|
class MultiselectPrompt extends Prompt {
|
|
constructor(opts={}) {
|
|
super(opts);
|
|
this.msg = opts.message;
|
|
this.cursor = opts.cursor || 0;
|
|
this.scrollIndex = opts.cursor || 0;
|
|
this.hint = opts.hint || '';
|
|
this.warn = opts.warn || '- This option is disabled -';
|
|
this.minSelected = opts.min;
|
|
this.showMinError = false;
|
|
this.maxChoices = opts.max;
|
|
this.instructions = opts.instructions;
|
|
this.optionsPerPage = opts.optionsPerPage || 10;
|
|
this.value = opts.choices.map((ch, idx) => {
|
|
if (typeof ch === 'string')
|
|
ch = {title: ch, value: idx};
|
|
return {
|
|
title: ch && (ch.title || ch.value || ch),
|
|
description: ch && ch.description,
|
|
value: ch && (ch.value === undefined ? idx : ch.value),
|
|
selected: ch && ch.selected,
|
|
disabled: ch && ch.disabled
|
|
};
|
|
});
|
|
this.clear = clear('');
|
|
if (!opts.overrideRender) {
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this.value.map(v => !v.selected);
|
|
this.cursor = 0;
|
|
this.fire();
|
|
this.render();
|
|
}
|
|
|
|
selected() {
|
|
return this.value.filter(v => v.selected);
|
|
}
|
|
|
|
abort() {
|
|
this.done = this.aborted = true;
|
|
this.fire();
|
|
this.render();
|
|
this.out.write('\n');
|
|
this.close();
|
|
}
|
|
|
|
submit() {
|
|
const selected = this.value
|
|
.filter(e => e.selected);
|
|
if (this.minSelected && selected.length < this.minSelected) {
|
|
this.showMinError = true;
|
|
this.render();
|
|
} else {
|
|
this.done = true;
|
|
this.aborted = false;
|
|
this.fire();
|
|
this.render();
|
|
this.out.write('\n');
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
first() {
|
|
this.cursor = 0;
|
|
this.render();
|
|
}
|
|
|
|
last() {
|
|
this.cursor = this.value.length - 1;
|
|
this.render();
|
|
}
|
|
next() {
|
|
this.cursor = (this.cursor + 1) % this.value.length;
|
|
this.render();
|
|
}
|
|
|
|
up() {
|
|
if (this.cursor === 0) {
|
|
this.cursor = this.value.length - 1;
|
|
} else {
|
|
this.cursor--;
|
|
}
|
|
this.render();
|
|
}
|
|
|
|
down() {
|
|
if (this.cursor === this.value.length - 1) {
|
|
this.cursor = 0;
|
|
} else {
|
|
this.cursor++;
|
|
}
|
|
this.render();
|
|
}
|
|
|
|
left() {
|
|
this.value[this.cursor].selected = false;
|
|
this.render();
|
|
}
|
|
|
|
right() {
|
|
if (this.value.filter(e => e.selected).length >= this.maxChoices) return this.bell();
|
|
this.value[this.cursor].selected = true;
|
|
this.render();
|
|
}
|
|
|
|
handleSpaceToggle() {
|
|
const v = this.value[this.cursor];
|
|
|
|
if (v.selected) {
|
|
v.selected = false;
|
|
this.render();
|
|
} else if (v.disabled || this.value.filter(e => e.selected).length >= this.maxChoices) {
|
|
return this.bell();
|
|
} else {
|
|
v.selected = true;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
toggleAll() {
|
|
if (this.maxChoices !== undefined || this.value[this.cursor].disabled) {
|
|
return this.bell();
|
|
}
|
|
|
|
const newSelected = !this.value[this.cursor].selected;
|
|
this.value.filter(v => !v.disabled).forEach(v => v.selected = newSelected);
|
|
this.render();
|
|
}
|
|
|
|
_(c, key) {
|
|
if (c === ' ') {
|
|
this.handleSpaceToggle();
|
|
} else if (c === 'a') {
|
|
this.toggleAll();
|
|
} else {
|
|
return this.bell();
|
|
}
|
|
}
|
|
|
|
renderInstructions() {
|
|
if (this.instructions === undefined || this.instructions) {
|
|
if (typeof this.instructions === 'string') {
|
|
return this.instructions;
|
|
}
|
|
return '\nInstructions:\n'
|
|
+ ` ${figures.arrowUp}/${figures.arrowDown}: Highlight option\n`
|
|
+ ` ${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection\n`
|
|
+ (this.maxChoices === undefined ? ` a: Toggle all\n` : '')
|
|
+ ` enter/return: Complete answer`;
|
|
}
|
|
return '';
|
|
}
|
|
|
|
renderOption(cursor, v, i, arrowIndicator) {
|
|
const prefix = (v.selected ? color.green(figures.radioOn) : figures.radioOff) + ' ' + arrowIndicator + ' ';
|
|
let title, desc;
|
|
|
|
if (v.disabled) {
|
|
title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
|
|
} else {
|
|
title = cursor === i ? color.cyan().underline(v.title) : v.title;
|
|
if (cursor === i && v.description) {
|
|
desc = ` - ${v.description}`;
|
|
if (prefix.length + title.length + desc.length >= this.out.columns
|
|
|| v.description.split(/\r?\n/).length > 1) {
|
|
desc = '\n' + wrap(v.description, { margin: prefix.length, width: this.out.columns });
|
|
}
|
|
}
|
|
}
|
|
|
|
return prefix + title + color.gray(desc || '');
|
|
}
|
|
|
|
// shared with autocompleteMultiselect
|
|
paginateOptions(options) {
|
|
if (options.length === 0) {
|
|
return color.red('No matches for this query.');
|
|
}
|
|
|
|
let { startIndex, endIndex } = entriesToDisplay(this.cursor, options.length, this.optionsPerPage);
|
|
let prefix, styledOptions = [];
|
|
|
|
for (let i = startIndex; i < endIndex; i++) {
|
|
if (i === startIndex && startIndex > 0) {
|
|
prefix = figures.arrowUp;
|
|
} else if (i === endIndex - 1 && endIndex < options.length) {
|
|
prefix = figures.arrowDown;
|
|
} else {
|
|
prefix = ' ';
|
|
}
|
|
styledOptions.push(this.renderOption(this.cursor, options[i], i, prefix));
|
|
}
|
|
|
|
return '\n' + styledOptions.join('\n');
|
|
}
|
|
|
|
// shared with autocomleteMultiselect
|
|
renderOptions(options) {
|
|
if (!this.done) {
|
|
return this.paginateOptions(options);
|
|
}
|
|
return '';
|
|
}
|
|
|
|
renderDoneOrInstructions() {
|
|
if (this.done) {
|
|
return this.value
|
|
.filter(e => e.selected)
|
|
.map(v => v.title)
|
|
.join(', ');
|
|
}
|
|
|
|
const output = [color.gray(this.hint), this.renderInstructions()];
|
|
|
|
if (this.value[this.cursor].disabled) {
|
|
output.push(color.yellow(this.warn));
|
|
}
|
|
return output.join(' ');
|
|
}
|
|
|
|
render() {
|
|
if (this.closed) return;
|
|
if (this.firstRender) this.out.write(cursor.hide);
|
|
super.render();
|
|
|
|
// print prompt
|
|
let prompt = [
|
|
style.symbol(this.done, this.aborted),
|
|
color.bold(this.msg),
|
|
style.delimiter(false),
|
|
this.renderDoneOrInstructions()
|
|
].join(' ');
|
|
if (this.showMinError) {
|
|
prompt += color.red(`You must select a minimum of ${this.minSelected} choices.`);
|
|
this.showMinError = false;
|
|
}
|
|
prompt += this.renderOptions(this.value);
|
|
|
|
this.out.write(this.clear + prompt);
|
|
this.clear = clear(prompt);
|
|
}
|
|
}
|
|
|
|
module.exports = MultiselectPrompt;
|