| 1 | 'use strict';
|
|---|
| 2 |
|
|---|
| 3 | const color = require('kleur');
|
|---|
| 4 |
|
|---|
| 5 | const _require = require('sisteransi'),
|
|---|
| 6 | cursor = _require.cursor;
|
|---|
| 7 |
|
|---|
| 8 | const Prompt = require('./prompt');
|
|---|
| 9 |
|
|---|
| 10 | const _require2 = require('../util'),
|
|---|
| 11 | clear = _require2.clear,
|
|---|
| 12 | figures = _require2.figures,
|
|---|
| 13 | style = _require2.style,
|
|---|
| 14 | wrap = _require2.wrap,
|
|---|
| 15 | entriesToDisplay = _require2.entriesToDisplay;
|
|---|
| 16 | /**
|
|---|
| 17 | * MultiselectPrompt Base Element
|
|---|
| 18 | * @param {Object} opts Options
|
|---|
| 19 | * @param {String} opts.message Message
|
|---|
| 20 | * @param {Array} opts.choices Array of choice objects
|
|---|
| 21 | * @param {String} [opts.hint] Hint to display
|
|---|
| 22 | * @param {String} [opts.warn] Hint shown for disabled choices
|
|---|
| 23 | * @param {Number} [opts.max] Max choices
|
|---|
| 24 | * @param {Number} [opts.cursor=0] Cursor start position
|
|---|
| 25 | * @param {Number} [opts.optionsPerPage=10] Max options to display at once
|
|---|
| 26 | * @param {Stream} [opts.stdin] The Readable stream to listen to
|
|---|
| 27 | * @param {Stream} [opts.stdout] The Writable stream to write readline data to
|
|---|
| 28 | */
|
|---|
| 29 |
|
|---|
| 30 |
|
|---|
| 31 | class MultiselectPrompt extends Prompt {
|
|---|
| 32 | constructor(opts = {}) {
|
|---|
| 33 | super(opts);
|
|---|
| 34 | this.msg = opts.message;
|
|---|
| 35 | this.cursor = opts.cursor || 0;
|
|---|
| 36 | this.scrollIndex = opts.cursor || 0;
|
|---|
| 37 | this.hint = opts.hint || '';
|
|---|
| 38 | this.warn = opts.warn || '- This option is disabled -';
|
|---|
| 39 | this.minSelected = opts.min;
|
|---|
| 40 | this.showMinError = false;
|
|---|
| 41 | this.maxChoices = opts.max;
|
|---|
| 42 | this.instructions = opts.instructions;
|
|---|
| 43 | this.optionsPerPage = opts.optionsPerPage || 10;
|
|---|
| 44 | this.value = opts.choices.map((ch, idx) => {
|
|---|
| 45 | if (typeof ch === 'string') ch = {
|
|---|
| 46 | title: ch,
|
|---|
| 47 | value: idx
|
|---|
| 48 | };
|
|---|
| 49 | return {
|
|---|
| 50 | title: ch && (ch.title || ch.value || ch),
|
|---|
| 51 | description: ch && ch.description,
|
|---|
| 52 | value: ch && (ch.value === undefined ? idx : ch.value),
|
|---|
| 53 | selected: ch && ch.selected,
|
|---|
| 54 | disabled: ch && ch.disabled
|
|---|
| 55 | };
|
|---|
| 56 | });
|
|---|
| 57 | this.clear = clear('', this.out.columns);
|
|---|
| 58 |
|
|---|
| 59 | if (!opts.overrideRender) {
|
|---|
| 60 | this.render();
|
|---|
| 61 | }
|
|---|
| 62 | }
|
|---|
| 63 |
|
|---|
| 64 | reset() {
|
|---|
| 65 | this.value.map(v => !v.selected);
|
|---|
| 66 | this.cursor = 0;
|
|---|
| 67 | this.fire();
|
|---|
| 68 | this.render();
|
|---|
| 69 | }
|
|---|
| 70 |
|
|---|
| 71 | selected() {
|
|---|
| 72 | return this.value.filter(v => v.selected);
|
|---|
| 73 | }
|
|---|
| 74 |
|
|---|
| 75 | exit() {
|
|---|
| 76 | this.abort();
|
|---|
| 77 | }
|
|---|
| 78 |
|
|---|
| 79 | abort() {
|
|---|
| 80 | this.done = this.aborted = true;
|
|---|
| 81 | this.fire();
|
|---|
| 82 | this.render();
|
|---|
| 83 | this.out.write('\n');
|
|---|
| 84 | this.close();
|
|---|
| 85 | }
|
|---|
| 86 |
|
|---|
| 87 | submit() {
|
|---|
| 88 | const selected = this.value.filter(e => e.selected);
|
|---|
| 89 |
|
|---|
| 90 | if (this.minSelected && selected.length < this.minSelected) {
|
|---|
| 91 | this.showMinError = true;
|
|---|
| 92 | this.render();
|
|---|
| 93 | } else {
|
|---|
| 94 | this.done = true;
|
|---|
| 95 | this.aborted = false;
|
|---|
| 96 | this.fire();
|
|---|
| 97 | this.render();
|
|---|
| 98 | this.out.write('\n');
|
|---|
| 99 | this.close();
|
|---|
| 100 | }
|
|---|
| 101 | }
|
|---|
| 102 |
|
|---|
| 103 | first() {
|
|---|
| 104 | this.cursor = 0;
|
|---|
| 105 | this.render();
|
|---|
| 106 | }
|
|---|
| 107 |
|
|---|
| 108 | last() {
|
|---|
| 109 | this.cursor = this.value.length - 1;
|
|---|
| 110 | this.render();
|
|---|
| 111 | }
|
|---|
| 112 |
|
|---|
| 113 | next() {
|
|---|
| 114 | this.cursor = (this.cursor + 1) % this.value.length;
|
|---|
| 115 | this.render();
|
|---|
| 116 | }
|
|---|
| 117 |
|
|---|
| 118 | up() {
|
|---|
| 119 | if (this.cursor === 0) {
|
|---|
| 120 | this.cursor = this.value.length - 1;
|
|---|
| 121 | } else {
|
|---|
| 122 | this.cursor--;
|
|---|
| 123 | }
|
|---|
| 124 |
|
|---|
| 125 | this.render();
|
|---|
| 126 | }
|
|---|
| 127 |
|
|---|
| 128 | down() {
|
|---|
| 129 | if (this.cursor === this.value.length - 1) {
|
|---|
| 130 | this.cursor = 0;
|
|---|
| 131 | } else {
|
|---|
| 132 | this.cursor++;
|
|---|
| 133 | }
|
|---|
| 134 |
|
|---|
| 135 | this.render();
|
|---|
| 136 | }
|
|---|
| 137 |
|
|---|
| 138 | left() {
|
|---|
| 139 | this.value[this.cursor].selected = false;
|
|---|
| 140 | this.render();
|
|---|
| 141 | }
|
|---|
| 142 |
|
|---|
| 143 | right() {
|
|---|
| 144 | if (this.value.filter(e => e.selected).length >= this.maxChoices) return this.bell();
|
|---|
| 145 | this.value[this.cursor].selected = true;
|
|---|
| 146 | this.render();
|
|---|
| 147 | }
|
|---|
| 148 |
|
|---|
| 149 | handleSpaceToggle() {
|
|---|
| 150 | const v = this.value[this.cursor];
|
|---|
| 151 |
|
|---|
| 152 | if (v.selected) {
|
|---|
| 153 | v.selected = false;
|
|---|
| 154 | this.render();
|
|---|
| 155 | } else if (v.disabled || this.value.filter(e => e.selected).length >= this.maxChoices) {
|
|---|
| 156 | return this.bell();
|
|---|
| 157 | } else {
|
|---|
| 158 | v.selected = true;
|
|---|
| 159 | this.render();
|
|---|
| 160 | }
|
|---|
| 161 | }
|
|---|
| 162 |
|
|---|
| 163 | toggleAll() {
|
|---|
| 164 | if (this.maxChoices !== undefined || this.value[this.cursor].disabled) {
|
|---|
| 165 | return this.bell();
|
|---|
| 166 | }
|
|---|
| 167 |
|
|---|
| 168 | const newSelected = !this.value[this.cursor].selected;
|
|---|
| 169 | this.value.filter(v => !v.disabled).forEach(v => v.selected = newSelected);
|
|---|
| 170 | this.render();
|
|---|
| 171 | }
|
|---|
| 172 |
|
|---|
| 173 | _(c, key) {
|
|---|
| 174 | if (c === ' ') {
|
|---|
| 175 | this.handleSpaceToggle();
|
|---|
| 176 | } else if (c === 'a') {
|
|---|
| 177 | this.toggleAll();
|
|---|
| 178 | } else {
|
|---|
| 179 | return this.bell();
|
|---|
| 180 | }
|
|---|
| 181 | }
|
|---|
| 182 |
|
|---|
| 183 | renderInstructions() {
|
|---|
| 184 | if (this.instructions === undefined || this.instructions) {
|
|---|
| 185 | if (typeof this.instructions === 'string') {
|
|---|
| 186 | return this.instructions;
|
|---|
| 187 | }
|
|---|
| 188 |
|
|---|
| 189 | 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`;
|
|---|
| 190 | }
|
|---|
| 191 |
|
|---|
| 192 | return '';
|
|---|
| 193 | }
|
|---|
| 194 |
|
|---|
| 195 | renderOption(cursor, v, i, arrowIndicator) {
|
|---|
| 196 | const prefix = (v.selected ? color.green(figures.radioOn) : figures.radioOff) + ' ' + arrowIndicator + ' ';
|
|---|
| 197 | let title, desc;
|
|---|
| 198 |
|
|---|
| 199 | if (v.disabled) {
|
|---|
| 200 | title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
|
|---|
| 201 | } else {
|
|---|
| 202 | title = cursor === i ? color.cyan().underline(v.title) : v.title;
|
|---|
| 203 |
|
|---|
| 204 | if (cursor === i && v.description) {
|
|---|
| 205 | desc = ` - ${v.description}`;
|
|---|
| 206 |
|
|---|
| 207 | if (prefix.length + title.length + desc.length >= this.out.columns || v.description.split(/\r?\n/).length > 1) {
|
|---|
| 208 | desc = '\n' + wrap(v.description, {
|
|---|
| 209 | margin: prefix.length,
|
|---|
| 210 | width: this.out.columns
|
|---|
| 211 | });
|
|---|
| 212 | }
|
|---|
| 213 | }
|
|---|
| 214 | }
|
|---|
| 215 |
|
|---|
| 216 | return prefix + title + color.gray(desc || '');
|
|---|
| 217 | } // shared with autocompleteMultiselect
|
|---|
| 218 |
|
|---|
| 219 |
|
|---|
| 220 | paginateOptions(options) {
|
|---|
| 221 | if (options.length === 0) {
|
|---|
| 222 | return color.red('No matches for this query.');
|
|---|
| 223 | }
|
|---|
| 224 |
|
|---|
| 225 | let _entriesToDisplay = entriesToDisplay(this.cursor, options.length, this.optionsPerPage),
|
|---|
| 226 | startIndex = _entriesToDisplay.startIndex,
|
|---|
| 227 | endIndex = _entriesToDisplay.endIndex;
|
|---|
| 228 |
|
|---|
| 229 | let prefix,
|
|---|
| 230 | styledOptions = [];
|
|---|
| 231 |
|
|---|
| 232 | for (let i = startIndex; i < endIndex; i++) {
|
|---|
| 233 | if (i === startIndex && startIndex > 0) {
|
|---|
| 234 | prefix = figures.arrowUp;
|
|---|
| 235 | } else if (i === endIndex - 1 && endIndex < options.length) {
|
|---|
| 236 | prefix = figures.arrowDown;
|
|---|
| 237 | } else {
|
|---|
| 238 | prefix = ' ';
|
|---|
| 239 | }
|
|---|
| 240 |
|
|---|
| 241 | styledOptions.push(this.renderOption(this.cursor, options[i], i, prefix));
|
|---|
| 242 | }
|
|---|
| 243 |
|
|---|
| 244 | return '\n' + styledOptions.join('\n');
|
|---|
| 245 | } // shared with autocomleteMultiselect
|
|---|
| 246 |
|
|---|
| 247 |
|
|---|
| 248 | renderOptions(options) {
|
|---|
| 249 | if (!this.done) {
|
|---|
| 250 | return this.paginateOptions(options);
|
|---|
| 251 | }
|
|---|
| 252 |
|
|---|
| 253 | return '';
|
|---|
| 254 | }
|
|---|
| 255 |
|
|---|
| 256 | renderDoneOrInstructions() {
|
|---|
| 257 | if (this.done) {
|
|---|
| 258 | return this.value.filter(e => e.selected).map(v => v.title).join(', ');
|
|---|
| 259 | }
|
|---|
| 260 |
|
|---|
| 261 | const output = [color.gray(this.hint), this.renderInstructions()];
|
|---|
| 262 |
|
|---|
| 263 | if (this.value[this.cursor].disabled) {
|
|---|
| 264 | output.push(color.yellow(this.warn));
|
|---|
| 265 | }
|
|---|
| 266 |
|
|---|
| 267 | return output.join(' ');
|
|---|
| 268 | }
|
|---|
| 269 |
|
|---|
| 270 | render() {
|
|---|
| 271 | if (this.closed) return;
|
|---|
| 272 | if (this.firstRender) this.out.write(cursor.hide);
|
|---|
| 273 | super.render(); // print prompt
|
|---|
| 274 |
|
|---|
| 275 | let prompt = [style.symbol(this.done, this.aborted), color.bold(this.msg), style.delimiter(false), this.renderDoneOrInstructions()].join(' ');
|
|---|
| 276 |
|
|---|
| 277 | if (this.showMinError) {
|
|---|
| 278 | prompt += color.red(`You must select a minimum of ${this.minSelected} choices.`);
|
|---|
| 279 | this.showMinError = false;
|
|---|
| 280 | }
|
|---|
| 281 |
|
|---|
| 282 | prompt += this.renderOptions(this.value);
|
|---|
| 283 | this.out.write(this.clear + prompt);
|
|---|
| 284 | this.clear = clear(prompt, this.out.columns);
|
|---|
| 285 | }
|
|---|
| 286 |
|
|---|
| 287 | }
|
|---|
| 288 |
|
|---|
| 289 | module.exports = MultiselectPrompt; |
|---|