3 const colors = require('ansi-colors');
4 const Prompt = require('../prompt');
5 const roles = require('../roles');
6 const utils = require('../utils');
7 const { reorder, scrollUp, scrollDown, isObject, swap } = utils;
9 class ArrayPrompt extends Prompt {
10 constructor(options) {
13 this.maxSelected = options.maxSelected || Infinity;
14 this.multiple = options.multiple || false;
15 this.initial = options.initial || 0;
16 this.delay = options.delay || 0;
22 if (typeof this.options.initial === 'function') {
23 this.initial = await this.options.initial.call(this);
25 await this.reset(true);
26 await super.initialize();
30 let { choices, initial, autofocus, suggest } = this.options;
31 this.state._choices = [];
32 this.state.choices = [];
34 this.choices = await Promise.all(await this.toChoices(choices));
35 this.choices.forEach(ch => (ch.enabled = false));
37 if (typeof suggest !== 'function' && this.selectable.length === 0) {
38 throw new Error('At least one choice must be selectable');
41 if (isObject(initial)) initial = Object.keys(initial);
42 if (Array.isArray(initial)) {
43 if (autofocus != null) this.index = this.findIndex(autofocus);
44 initial.forEach(v => this.enable(this.find(v)));
47 if (autofocus != null) initial = autofocus;
48 if (typeof initial === 'string') initial = this.findIndex(initial);
49 if (typeof initial === 'number' && initial > -1) {
50 this.index = Math.max(0, Math.min(initial, this.choices.length));
51 this.enable(this.find(this.index));
55 if (this.isDisabled(this.focused)) {
60 async toChoices(value, parent) {
61 this.state.loadingChoices = true;
65 let toChoices = async(items, parent) => {
66 if (typeof items === 'function') items = await items.call(this);
67 if (items instanceof Promise) items = await items;
69 for (let i = 0; i < items.length; i++) {
70 let choice = items[i] = await this.toChoice(items[i], index++, parent);
74 await toChoices(choice.choices, choice);
80 return toChoices(value, parent)
82 this.state.loadingChoices = false;
87 async toChoice(ele, i, parent) {
88 if (typeof ele === 'function') ele = await ele.call(this, this);
89 if (ele instanceof Promise) ele = await ele;
90 if (typeof ele === 'string') ele = { name: ele };
92 if (ele.normalized) return ele;
93 ele.normalized = true;
95 let origVal = ele.value;
96 let role = roles(ele.role, this.options);
97 ele = role(this, ele);
99 if (typeof ele.disabled === 'string' && !ele.hint) {
100 ele.hint = ele.disabled;
104 if (ele.disabled === true && ele.hint == null) {
105 ele.hint = '(disabled)';
108 // if the choice was already normalized, return it
109 if (ele.index != null) return ele;
110 ele.name = ele.name || ele.key || ele.title || ele.value || ele.message;
111 ele.message = ele.message || ele.name || '';
112 ele.value = [ele.value, ele.name].find(this.isValue.bind(this));
118 utils.define(ele, 'parent', parent);
119 ele.level = parent ? parent.level + 1 : 1;
120 if (ele.indent == null) {
121 ele.indent = parent ? parent.indent + ' ' : (ele.indent || '');
124 ele.path = parent ? parent.path + '.' + ele.name : ele.name;
125 ele.enabled = !!(this.multiple && !this.isDisabled(ele) && (ele.enabled || this.isSelected(ele)));
127 if (!this.isDisabled(ele)) {
128 this.longest = Math.max(this.longest, colors.unstyle(ele.message).length);
131 // shallow clone the choice first
132 let choice = { ...ele };
134 // then allow the choice to be reset using the "original" values
135 ele.reset = (input = choice.input, value = choice.value) => {
136 for (let key of Object.keys(choice)) ele[key] = choice[key];
141 if (origVal == null && typeof ele.initial === 'function') {
142 ele.input = await ele.initial.call(this, this.state, ele, i);
148 async onChoice(choice, i) {
149 this.emit('choice', choice, i, this);
151 if (typeof choice.onChoice === 'function') {
152 await choice.onChoice.call(this, this.state, choice, i);
156 async addChoice(ele, i, parent) {
157 let choice = await this.toChoice(ele, i, parent);
158 this.choices.push(choice);
159 this.index = this.choices.length - 1;
160 this.limit = this.choices.length;
164 async newItem(item, i, parent) {
165 let ele = { name: 'New choice name?', editable: true, newChoice: true, ...item };
166 let choice = await this.addChoice(ele, i, parent);
168 choice.updateChoice = () => {
169 delete choice.newChoice;
170 choice.name = choice.message = choice.input;
175 return this.render();
179 if (choice.indent == null) {
180 return choice.level > 1 ? ' '.repeat(choice.level - 1) : '';
182 return choice.indent;
186 if (this.multiple && this[key.name]) return this[key.name]();
190 focus(choice, enabled) {
191 if (typeof enabled !== 'boolean') enabled = choice.enabled;
192 if (enabled && !choice.enabled && this.selected.length >= this.maxSelected) {
195 this.index = choice.index;
196 choice.enabled = enabled && !this.isDisabled(choice);
201 if (!this.multiple) return this.alert();
202 this.toggle(this.focused);
203 return this.render();
207 if (this.maxSelected < this.choices.length) return this.alert();
208 let enabled = this.selectable.every(ch => ch.enabled);
209 this.choices.forEach(ch => (ch.enabled = !enabled));
210 return this.render();
214 // don't allow choices to be inverted if it will result in
215 // more than the maximum number of allowed selected items.
216 if (this.choices.length - this.selected.length > this.maxSelected) {
219 this.choices.forEach(ch => (ch.enabled = !ch.enabled));
220 return this.render();
223 g(choice = this.focused) {
224 if (!this.choices.some(ch => !!ch.parent)) return this.a();
225 this.toggle((choice.parent && !choice.choices) ? choice.parent : choice);
226 return this.render();
229 toggle(choice, enabled) {
230 if (!choice.enabled && this.selected.length >= this.maxSelected) {
234 if (typeof enabled !== 'boolean') enabled = !choice.enabled;
235 choice.enabled = enabled;
237 if (choice.choices) {
238 choice.choices.forEach(ch => this.toggle(ch, enabled));
241 let parent = choice.parent;
243 let choices = parent.choices.filter(ch => this.isDisabled(ch));
244 parent.enabled = choices.every(ch => ch.enabled === true);
245 parent = parent.parent;
248 reset(this, this.choices);
249 this.emit('toggle', choice, this);
254 if (this.selected.length >= this.maxSelected) return this.alert();
255 choice.enabled = !this.isDisabled(choice);
256 choice.choices && choice.choices.forEach(this.enable.bind(this));
261 choice.enabled = false;
262 choice.choices && choice.choices.forEach(this.disable.bind(this));
269 let number = num => {
271 if (i > this.choices.length - 1) return this.alert();
273 let focused = this.focused;
274 let choice = this.choices.find(ch => i === ch.index);
276 if (!choice.enabled && this.selected.length >= this.maxSelected) {
280 if (this.visible.indexOf(choice) === -1) {
281 let choices = reorder(this.choices);
282 let actualIdx = choices.indexOf(choice);
284 if (focused.index > actualIdx) {
285 let start = choices.slice(actualIdx, actualIdx + this.limit);
286 let end = choices.filter(ch => !start.includes(ch));
287 this.choices = start.concat(end);
289 let pos = actualIdx - this.limit + 1;
290 this.choices = choices.slice(pos).concat(choices.slice(0, pos));
294 this.index = this.choices.indexOf(choice);
295 this.toggle(this.focused);
296 return this.render();
299 clearTimeout(this.numberTimeout);
301 return new Promise(resolve => {
302 let len = this.choices.length;
305 let handle = (val = false, res) => {
306 clearTimeout(this.numberTimeout);
307 if (val) res = number(num);
312 if (num === '0' || (num.length === 1 && Number(num + '0') > len)) {
316 if (Number(num) > len) {
317 return handle(false, this.alert());
320 this.numberTimeout = setTimeout(() => handle(true), this.delay);
325 this.choices = reorder(this.choices);
327 return this.render();
331 let pos = this.choices.length - this.limit;
332 let choices = reorder(this.choices);
333 this.choices = choices.slice(pos).concat(choices.slice(0, pos));
334 this.index = this.limit - 1;
335 return this.render();
340 return this.render();
344 this.index = this.visible.length - 1;
345 return this.render();
349 if (this.visible.length <= 1) return this.alert();
354 if (this.visible.length <= 1) return this.alert();
359 if (this.cursor >= this.input.length) return this.alert();
361 return this.render();
365 if (this.cursor <= 0) return this.alert();
367 return this.render();
371 let len = this.choices.length;
372 let vis = this.visible.length;
373 let idx = this.index;
374 if (this.options.scroll === false && idx === 0) {
377 if (len > vis && idx === 0) {
378 return this.scrollUp();
380 this.index = ((idx - 1 % len) + len) % len;
381 if (this.isDisabled()) {
384 return this.render();
388 let len = this.choices.length;
389 let vis = this.visible.length;
390 let idx = this.index;
391 if (this.options.scroll === false && idx === vis - 1) {
394 if (len > vis && idx === vis - 1) {
395 return this.scrollDown();
397 this.index = (idx + 1) % len;
398 if (this.isDisabled()) {
401 return this.render();
405 this.choices = scrollUp(this.choices);
407 if (this.isDisabled()) {
410 return this.render();
413 scrollDown(i = this.visible.length - 1) {
414 this.choices = scrollDown(this.choices);
416 if (this.isDisabled()) {
419 return this.render();
423 if (this.options.sort === true) {
425 this.swap(this.index - 1);
427 this.sorting = false;
430 return this.scrollUp(this.index);
434 if (this.options.sort === true) {
436 this.swap(this.index + 1);
438 this.sorting = false;
441 return this.scrollDown(this.index);
445 if (this.visible.length <= 1) return this.alert();
446 this.limit = Math.max(this.limit - 1, 0);
447 this.index = Math.min(this.limit - 1, this.index);
448 this._limit = this.limit;
449 if (this.isDisabled()) {
452 return this.render();
456 if (this.visible.length >= this.choices.length) return this.alert();
457 this.index = Math.max(0, this.index);
458 this.limit = Math.min(this.limit + 1, this.choices.length);
459 this._limit = this.limit;
460 if (this.isDisabled()) {
463 return this.render();
467 swap(this.choices, this.index, pos);
470 isDisabled(choice = this.focused) {
471 let keys = ['disabled', 'collapsed', 'hidden', 'completing', 'readonly'];
472 if (choice && keys.some(key => choice[key] === true)) {
475 return choice && choice.role === 'heading';
478 isEnabled(choice = this.focused) {
479 if (Array.isArray(choice)) return choice.every(ch => this.isEnabled(ch));
480 if (choice.choices) {
481 let choices = choice.choices.filter(ch => !this.isDisabled(ch));
482 return choice.enabled && choices.every(ch => this.isEnabled(ch));
484 return choice.enabled && !this.isDisabled(choice);
487 isChoice(choice, value) {
488 return choice.name === value || choice.index === Number(value);
492 if (Array.isArray(this.initial)) {
493 return this.initial.some(value => this.isChoice(choice, value));
495 return this.isChoice(choice, this.initial);
498 map(names = [], prop = 'value') {
499 return [].concat(names || []).reduce((acc, name) => {
500 acc[name] = this.find(name, prop);
505 filter(value, prop) {
506 let isChoice = (ele, i) => [ele.name, i].includes(value);
507 let fn = typeof value === 'function' ? value : isChoice;
508 let choices = this.options.multiple ? this.state._choices : this.choices;
509 let result = choices.filter(fn);
511 return result.map(ch => ch[prop]);
517 if (isObject(value)) return prop ? value[prop] : value;
518 let isChoice = (ele, i) => [ele.name, i].includes(value);
519 let fn = typeof value === 'function' ? value : isChoice;
520 let choice = this.choices.find(fn);
522 return prop ? choice[prop] : choice;
527 return this.choices.indexOf(this.find(value));
531 let choice = this.focused;
532 if (!choice) return this.alert();
534 if (choice.newChoice) {
535 if (!choice.input) return this.alert();
536 choice.updateChoice();
537 return this.render();
540 if (this.choices.some(ch => ch.newChoice)) {
544 let { reorder, sort } = this.options;
545 let multi = this.multiple === true;
546 let value = this.selected;
547 if (value === void 0) {
551 // re-sort choices to original order
552 if (Array.isArray(value) && reorder !== false && sort !== true) {
553 value = utils.reorder(value);
556 this.value = multi ? value.map(ch => ch.name) : value.name;
557 return super.submit();
560 set choices(choices = []) {
561 this.state._choices = this.state._choices || [];
562 this.state.choices = choices;
564 for (let choice of choices) {
565 if (!this.state._choices.some(ch => ch.name === choice.name)) {
566 this.state._choices.push(choice);
570 if (!this._initial && this.options.initial) {
571 this._initial = true;
572 let init = this.initial;
573 if (typeof init === 'string' || typeof init === 'number') {
574 let choice = this.find(init);
576 this.initial = choice.index;
577 this.focus(choice, true);
583 return reset(this, this.state.choices || []);
586 set visible(visible) {
587 this.state.visible = visible;
590 return (this.state.visible || this.choices).slice(0, this.limit);
594 this.state.limit = num;
597 let { state, options, choices } = this;
598 let limit = state.limit || this._limit || options.limit || choices.length;
599 return Math.min(limit, this.height);
606 if (typeof super.value !== 'string' && super.value === this.initial) {
613 this.state.index = i;
616 return Math.max(0, this.state ? this.state.index : 0);
620 return this.filter(this.isEnabled.bind(this));
624 let choice = this.choices[this.index];
625 if (choice && this.state.submitted && this.multiple !== true) {
626 choice.enabled = true;
632 return this.choices.filter(choice => !this.isDisabled(choice));
636 return this.multiple ? this.enabled : this.focused;
640 function reset(prompt, choices) {
641 if (choices instanceof Promise) return choices;
642 if (typeof choices === 'function') {
643 if (utils.isAsyncFn(choices)) return choices;
644 choices = choices.call(prompt, prompt);
646 for (let choice of choices) {
647 if (Array.isArray(choice.choices)) {
648 let items = choice.choices.filter(ch => !prompt.isDisabled(ch));
649 choice.enabled = items.every(ch => ch.enabled === true);
651 if (prompt.isDisabled(choice) === true) {
652 delete choice.enabled;
658 module.exports = ArrayPrompt;