123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658 |
- 'use strict';
-
- const colors = require('ansi-colors');
- const Prompt = require('../prompt');
- const roles = require('../roles');
- const utils = require('../utils');
- const { reorder, scrollUp, scrollDown, isObject, swap } = utils;
-
- class ArrayPrompt extends Prompt {
- constructor(options) {
- super(options);
- this.cursorHide();
- this.maxSelected = options.maxSelected || Infinity;
- this.multiple = options.multiple || false;
- this.initial = options.initial || 0;
- this.delay = options.delay || 0;
- this.longest = 0;
- this.num = '';
- }
-
- async initialize() {
- if (typeof this.options.initial === 'function') {
- this.initial = await this.options.initial.call(this);
- }
- await this.reset(true);
- await super.initialize();
- }
-
- async reset() {
- let { choices, initial, autofocus, suggest } = this.options;
- this.state._choices = [];
- this.state.choices = [];
-
- this.choices = await Promise.all(await this.toChoices(choices));
- this.choices.forEach(ch => (ch.enabled = false));
-
- if (typeof suggest !== 'function' && this.selectable.length === 0) {
- throw new Error('At least one choice must be selectable');
- }
-
- if (isObject(initial)) initial = Object.keys(initial);
- if (Array.isArray(initial)) {
- if (autofocus != null) this.index = this.findIndex(autofocus);
- initial.forEach(v => this.enable(this.find(v)));
- await this.render();
- } else {
- if (autofocus != null) initial = autofocus;
- if (typeof initial === 'string') initial = this.findIndex(initial);
- if (typeof initial === 'number' && initial > -1) {
- this.index = Math.max(0, Math.min(initial, this.choices.length));
- this.enable(this.find(this.index));
- }
- }
-
- if (this.isDisabled(this.focused)) {
- await this.down();
- }
- }
-
- async toChoices(value, parent) {
- this.state.loadingChoices = true;
- let choices = [];
- let index = 0;
-
- let toChoices = async(items, parent) => {
- if (typeof items === 'function') items = await items.call(this);
- if (items instanceof Promise) items = await items;
-
- for (let i = 0; i < items.length; i++) {
- let choice = items[i] = await this.toChoice(items[i], index++, parent);
- choices.push(choice);
-
- if (choice.choices) {
- await toChoices(choice.choices, choice);
- }
- }
- return choices;
- };
-
- return toChoices(value, parent)
- .then(choices => {
- this.state.loadingChoices = false;
- return choices;
- });
- }
-
- async toChoice(ele, i, parent) {
- if (typeof ele === 'function') ele = await ele.call(this, this);
- if (ele instanceof Promise) ele = await ele;
- if (typeof ele === 'string') ele = { name: ele };
-
- if (ele.normalized) return ele;
- ele.normalized = true;
-
- let origVal = ele.value;
- let role = roles(ele.role, this.options);
- ele = role(this, ele);
-
- if (typeof ele.disabled === 'string' && !ele.hint) {
- ele.hint = ele.disabled;
- ele.disabled = true;
- }
-
- if (ele.disabled === true && ele.hint == null) {
- ele.hint = '(disabled)';
- }
-
- // if the choice was already normalized, return it
- if (ele.index != null) return ele;
- ele.name = ele.name || ele.key || ele.title || ele.value || ele.message;
- ele.message = ele.message || ele.name || '';
- ele.value = [ele.value, ele.name].find(this.isValue.bind(this));
-
- ele.input = '';
- ele.index = i;
- ele.cursor = 0;
-
- utils.define(ele, 'parent', parent);
- ele.level = parent ? parent.level + 1 : 1;
- if (ele.indent == null) {
- ele.indent = parent ? parent.indent + ' ' : (ele.indent || '');
- }
-
- ele.path = parent ? parent.path + '.' + ele.name : ele.name;
- ele.enabled = !!(this.multiple && !this.isDisabled(ele) && (ele.enabled || this.isSelected(ele)));
-
- if (!this.isDisabled(ele)) {
- this.longest = Math.max(this.longest, colors.unstyle(ele.message).length);
- }
-
- // shallow clone the choice first
- let choice = { ...ele };
-
- // then allow the choice to be reset using the "original" values
- ele.reset = (input = choice.input, value = choice.value) => {
- for (let key of Object.keys(choice)) ele[key] = choice[key];
- ele.input = input;
- ele.value = value;
- };
-
- if (origVal == null && typeof ele.initial === 'function') {
- ele.input = await ele.initial.call(this, this.state, ele, i);
- }
-
- return ele;
- }
-
- async onChoice(choice, i) {
- this.emit('choice', choice, i, this);
-
- if (typeof choice.onChoice === 'function') {
- await choice.onChoice.call(this, this.state, choice, i);
- }
- }
-
- async addChoice(ele, i, parent) {
- let choice = await this.toChoice(ele, i, parent);
- this.choices.push(choice);
- this.index = this.choices.length - 1;
- this.limit = this.choices.length;
- return choice;
- }
-
- async newItem(item, i, parent) {
- let ele = { name: 'New choice name?', editable: true, newChoice: true, ...item };
- let choice = await this.addChoice(ele, i, parent);
-
- choice.updateChoice = () => {
- delete choice.newChoice;
- choice.name = choice.message = choice.input;
- choice.input = '';
- choice.cursor = 0;
- };
-
- return this.render();
- }
-
- indent(choice) {
- if (choice.indent == null) {
- return choice.level > 1 ? ' '.repeat(choice.level - 1) : '';
- }
- return choice.indent;
- }
-
- dispatch(s, key) {
- if (this.multiple && this[key.name]) return this[key.name]();
- this.alert();
- }
-
- focus(choice, enabled) {
- if (typeof enabled !== 'boolean') enabled = choice.enabled;
- if (enabled && !choice.enabled && this.selected.length >= this.maxSelected) {
- return this.alert();
- }
- this.index = choice.index;
- choice.enabled = enabled && !this.isDisabled(choice);
- return choice;
- }
-
- space() {
- if (!this.multiple) return this.alert();
- this.toggle(this.focused);
- return this.render();
- }
-
- a() {
- if (this.maxSelected < this.choices.length) return this.alert();
- let enabled = this.selectable.every(ch => ch.enabled);
- this.choices.forEach(ch => (ch.enabled = !enabled));
- return this.render();
- }
-
- i() {
- // don't allow choices to be inverted if it will result in
- // more than the maximum number of allowed selected items.
- if (this.choices.length - this.selected.length > this.maxSelected) {
- return this.alert();
- }
- this.choices.forEach(ch => (ch.enabled = !ch.enabled));
- return this.render();
- }
-
- g(choice = this.focused) {
- if (!this.choices.some(ch => !!ch.parent)) return this.a();
- this.toggle((choice.parent && !choice.choices) ? choice.parent : choice);
- return this.render();
- }
-
- toggle(choice, enabled) {
- if (!choice.enabled && this.selected.length >= this.maxSelected) {
- return this.alert();
- }
-
- if (typeof enabled !== 'boolean') enabled = !choice.enabled;
- choice.enabled = enabled;
-
- if (choice.choices) {
- choice.choices.forEach(ch => this.toggle(ch, enabled));
- }
-
- let parent = choice.parent;
- while (parent) {
- let choices = parent.choices.filter(ch => this.isDisabled(ch));
- parent.enabled = choices.every(ch => ch.enabled === true);
- parent = parent.parent;
- }
-
- reset(this, this.choices);
- this.emit('toggle', choice, this);
- return choice;
- }
-
- enable(choice) {
- if (this.selected.length >= this.maxSelected) return this.alert();
- choice.enabled = !this.isDisabled(choice);
- choice.choices && choice.choices.forEach(this.enable.bind(this));
- return choice;
- }
-
- disable(choice) {
- choice.enabled = false;
- choice.choices && choice.choices.forEach(this.disable.bind(this));
- return choice;
- }
-
- number(n) {
- this.num += n;
-
- let number = num => {
- let i = Number(num);
- if (i > this.choices.length - 1) return this.alert();
-
- let focused = this.focused;
- let choice = this.choices.find(ch => i === ch.index);
-
- if (!choice.enabled && this.selected.length >= this.maxSelected) {
- return this.alert();
- }
-
- if (this.visible.indexOf(choice) === -1) {
- let choices = reorder(this.choices);
- let actualIdx = choices.indexOf(choice);
-
- if (focused.index > actualIdx) {
- let start = choices.slice(actualIdx, actualIdx + this.limit);
- let end = choices.filter(ch => !start.includes(ch));
- this.choices = start.concat(end);
- } else {
- let pos = actualIdx - this.limit + 1;
- this.choices = choices.slice(pos).concat(choices.slice(0, pos));
- }
- }
-
- this.index = this.choices.indexOf(choice);
- this.toggle(this.focused);
- return this.render();
- };
-
- clearTimeout(this.numberTimeout);
-
- return new Promise(resolve => {
- let len = this.choices.length;
- let num = this.num;
-
- let handle = (val = false, res) => {
- clearTimeout(this.numberTimeout);
- if (val) res = number(num);
- this.num = '';
- resolve(res);
- };
-
- if (num === '0' || (num.length === 1 && Number(num + '0') > len)) {
- return handle(true);
- }
-
- if (Number(num) > len) {
- return handle(false, this.alert());
- }
-
- this.numberTimeout = setTimeout(() => handle(true), this.delay);
- });
- }
-
- home() {
- this.choices = reorder(this.choices);
- this.index = 0;
- return this.render();
- }
-
- end() {
- let pos = this.choices.length - this.limit;
- let choices = reorder(this.choices);
- this.choices = choices.slice(pos).concat(choices.slice(0, pos));
- this.index = this.limit - 1;
- return this.render();
- }
-
- first() {
- this.index = 0;
- return this.render();
- }
-
- last() {
- this.index = this.visible.length - 1;
- return this.render();
- }
-
- prev() {
- if (this.visible.length <= 1) return this.alert();
- return this.up();
- }
-
- next() {
- if (this.visible.length <= 1) return this.alert();
- return this.down();
- }
-
- right() {
- if (this.cursor >= this.input.length) return this.alert();
- this.cursor++;
- return this.render();
- }
-
- left() {
- if (this.cursor <= 0) return this.alert();
- this.cursor--;
- return this.render();
- }
-
- up() {
- let len = this.choices.length;
- let vis = this.visible.length;
- let idx = this.index;
- if (this.options.scroll === false && idx === 0) {
- return this.alert();
- }
- if (len > vis && idx === 0) {
- return this.scrollUp();
- }
- this.index = ((idx - 1 % len) + len) % len;
- if (this.isDisabled()) {
- return this.up();
- }
- return this.render();
- }
-
- down() {
- let len = this.choices.length;
- let vis = this.visible.length;
- let idx = this.index;
- if (this.options.scroll === false && idx === vis - 1) {
- return this.alert();
- }
- if (len > vis && idx === vis - 1) {
- return this.scrollDown();
- }
- this.index = (idx + 1) % len;
- if (this.isDisabled()) {
- return this.down();
- }
- return this.render();
- }
-
- scrollUp(i = 0) {
- this.choices = scrollUp(this.choices);
- this.index = i;
- if (this.isDisabled()) {
- return this.up();
- }
- return this.render();
- }
-
- scrollDown(i = this.visible.length - 1) {
- this.choices = scrollDown(this.choices);
- this.index = i;
- if (this.isDisabled()) {
- return this.down();
- }
- return this.render();
- }
-
- async shiftUp() {
- if (this.options.sort === true) {
- this.sorting = true;
- this.swap(this.index - 1);
- await this.up();
- this.sorting = false;
- return;
- }
- return this.scrollUp(this.index);
- }
-
- async shiftDown() {
- if (this.options.sort === true) {
- this.sorting = true;
- this.swap(this.index + 1);
- await this.down();
- this.sorting = false;
- return;
- }
- return this.scrollDown(this.index);
- }
-
- pageUp() {
- if (this.visible.length <= 1) return this.alert();
- this.limit = Math.max(this.limit - 1, 0);
- this.index = Math.min(this.limit - 1, this.index);
- this._limit = this.limit;
- if (this.isDisabled()) {
- return this.up();
- }
- return this.render();
- }
-
- pageDown() {
- if (this.visible.length >= this.choices.length) return this.alert();
- this.index = Math.max(0, this.index);
- this.limit = Math.min(this.limit + 1, this.choices.length);
- this._limit = this.limit;
- if (this.isDisabled()) {
- return this.down();
- }
- return this.render();
- }
-
- swap(pos) {
- swap(this.choices, this.index, pos);
- }
-
- isDisabled(choice = this.focused) {
- let keys = ['disabled', 'collapsed', 'hidden', 'completing', 'readonly'];
- if (choice && keys.some(key => choice[key] === true)) {
- return true;
- }
- return choice && choice.role === 'heading';
- }
-
- isEnabled(choice = this.focused) {
- if (Array.isArray(choice)) return choice.every(ch => this.isEnabled(ch));
- if (choice.choices) {
- let choices = choice.choices.filter(ch => !this.isDisabled(ch));
- return choice.enabled && choices.every(ch => this.isEnabled(ch));
- }
- return choice.enabled && !this.isDisabled(choice);
- }
-
- isChoice(choice, value) {
- return choice.name === value || choice.index === Number(value);
- }
-
- isSelected(choice) {
- if (Array.isArray(this.initial)) {
- return this.initial.some(value => this.isChoice(choice, value));
- }
- return this.isChoice(choice, this.initial);
- }
-
- map(names = [], prop = 'value') {
- return [].concat(names || []).reduce((acc, name) => {
- acc[name] = this.find(name, prop);
- return acc;
- }, {});
- }
-
- filter(value, prop) {
- let isChoice = (ele, i) => [ele.name, i].includes(value);
- let fn = typeof value === 'function' ? value : isChoice;
- let choices = this.options.multiple ? this.state._choices : this.choices;
- let result = choices.filter(fn);
- if (prop) {
- return result.map(ch => ch[prop]);
- }
- return result;
- }
-
- find(value, prop) {
- if (isObject(value)) return prop ? value[prop] : value;
- let isChoice = (ele, i) => [ele.name, i].includes(value);
- let fn = typeof value === 'function' ? value : isChoice;
- let choice = this.choices.find(fn);
- if (choice) {
- return prop ? choice[prop] : choice;
- }
- }
-
- findIndex(value) {
- return this.choices.indexOf(this.find(value));
- }
-
- async submit() {
- let choice = this.focused;
- if (!choice) return this.alert();
-
- if (choice.newChoice) {
- if (!choice.input) return this.alert();
- choice.updateChoice();
- return this.render();
- }
-
- if (this.choices.some(ch => ch.newChoice)) {
- return this.alert();
- }
-
- let { reorder, sort } = this.options;
- let multi = this.multiple === true;
- let value = this.selected;
- if (value === void 0) {
- return this.alert();
- }
-
- // re-sort choices to original order
- if (Array.isArray(value) && reorder !== false && sort !== true) {
- value = utils.reorder(value);
- }
-
- this.value = multi ? value.map(ch => ch.name) : value.name;
- return super.submit();
- }
-
- set choices(choices = []) {
- this.state._choices = this.state._choices || [];
- this.state.choices = choices;
-
- for (let choice of choices) {
- if (!this.state._choices.some(ch => ch.name === choice.name)) {
- this.state._choices.push(choice);
- }
- }
-
- if (!this._initial && this.options.initial) {
- this._initial = true;
- let init = this.initial;
- if (typeof init === 'string' || typeof init === 'number') {
- let choice = this.find(init);
- if (choice) {
- this.initial = choice.index;
- this.focus(choice, true);
- }
- }
- }
- }
- get choices() {
- return reset(this, this.state.choices || []);
- }
-
- set visible(visible) {
- this.state.visible = visible;
- }
- get visible() {
- return (this.state.visible || this.choices).slice(0, this.limit);
- }
-
- set limit(num) {
- this.state.limit = num;
- }
- get limit() {
- let { state, options, choices } = this;
- let limit = state.limit || this._limit || options.limit || choices.length;
- return Math.min(limit, this.height);
- }
-
- set value(value) {
- super.value = value;
- }
- get value() {
- if (typeof super.value !== 'string' && super.value === this.initial) {
- return this.input;
- }
- return super.value;
- }
-
- set index(i) {
- this.state.index = i;
- }
- get index() {
- return Math.max(0, this.state ? this.state.index : 0);
- }
-
- get enabled() {
- return this.filter(this.isEnabled.bind(this));
- }
-
- get focused() {
- let choice = this.choices[this.index];
- if (choice && this.state.submitted && this.multiple !== true) {
- choice.enabled = true;
- }
- return choice;
- }
-
- get selectable() {
- return this.choices.filter(choice => !this.isDisabled(choice));
- }
-
- get selected() {
- return this.multiple ? this.enabled : this.focused;
- }
- }
-
- function reset(prompt, choices) {
- if (choices instanceof Promise) return choices;
- if (typeof choices === 'function') {
- if (utils.isAsyncFn(choices)) return choices;
- choices = choices.call(prompt, prompt);
- }
- for (let choice of choices) {
- if (Array.isArray(choice.choices)) {
- let items = choice.choices.filter(ch => !prompt.isDisabled(ch));
- choice.enabled = items.every(ch => ch.enabled === true);
- }
- if (prompt.isDisabled(choice) === true) {
- delete choice.enabled;
- }
- }
- return choices;
- }
-
- module.exports = ArrayPrompt;
|