e4794d4085af652ef3514e19d138144f2d54c6e8af6268dd9c82c3d2c59f7640ed23797d83ad85e570cda40d339c3d0049601d24c3caeebc6232680786aa82 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664
  1. 'use strict';
  2. const stripAnsi = require('strip-ansi');
  3. const Prompt = require('../prompt');
  4. const roles = require('../roles');
  5. const utils = require('../utils');
  6. const { reorder, scrollUp, scrollDown, isObject, swap } = utils;
  7. class ArrayPrompt extends Prompt {
  8. constructor(options) {
  9. super(options);
  10. this.cursorHide();
  11. this.maxSelected = options.maxSelected || Infinity;
  12. this.multiple = options.multiple || false;
  13. this.initial = options.initial || 0;
  14. this.delay = options.delay || 0;
  15. this.longest = 0;
  16. this.num = '';
  17. }
  18. async initialize() {
  19. if (typeof this.options.initial === 'function') {
  20. this.initial = await this.options.initial.call(this);
  21. }
  22. await this.reset(true);
  23. await super.initialize();
  24. }
  25. async reset() {
  26. let { choices, initial, autofocus, suggest } = this.options;
  27. this.state._choices = [];
  28. this.state.choices = [];
  29. this.choices = await Promise.all(await this.toChoices(choices));
  30. this.choices.forEach(ch => (ch.enabled = false));
  31. if (typeof suggest !== 'function' && this.selectable.length === 0) {
  32. throw new Error('At least one choice must be selectable');
  33. }
  34. if (isObject(initial)) initial = Object.keys(initial);
  35. if (Array.isArray(initial)) {
  36. if (autofocus != null) this.index = this.findIndex(autofocus);
  37. initial.forEach(v => this.enable(this.find(v)));
  38. await this.render();
  39. } else {
  40. if (autofocus != null) initial = autofocus;
  41. if (typeof initial === 'string') initial = this.findIndex(initial);
  42. if (typeof initial === 'number' && initial > -1) {
  43. this.index = Math.max(0, Math.min(initial, this.choices.length));
  44. this.enable(this.find(this.index));
  45. }
  46. }
  47. if (this.isDisabled(this.focused)) {
  48. await this.down();
  49. }
  50. }
  51. async toChoices(value, parent) {
  52. this.state.loadingChoices = true;
  53. let choices = [];
  54. let index = 0;
  55. let toChoices = async(items, parent) => {
  56. if (typeof items === 'function') items = await items.call(this);
  57. if (items instanceof Promise) items = await items;
  58. for (let i = 0; i < items.length; i++) {
  59. let choice = items[i] = await this.toChoice(items[i], index++, parent);
  60. choices.push(choice);
  61. if (choice.choices) {
  62. await toChoices(choice.choices, choice);
  63. }
  64. }
  65. return choices;
  66. };
  67. return toChoices(value, parent)
  68. .then(choices => {
  69. this.state.loadingChoices = false;
  70. return choices;
  71. });
  72. }
  73. async toChoice(ele, i, parent) {
  74. if (typeof ele === 'function') ele = await ele.call(this, this);
  75. if (ele instanceof Promise) ele = await ele;
  76. if (typeof ele === 'string') ele = { name: ele };
  77. if (ele.normalized) return ele;
  78. ele.normalized = true;
  79. let origVal = ele.value;
  80. let role = roles(ele.role, this.options);
  81. ele = role(this, ele);
  82. if (typeof ele.disabled === 'string' && !ele.hint) {
  83. ele.hint = ele.disabled;
  84. ele.disabled = true;
  85. }
  86. if (ele.disabled === true && ele.hint == null) {
  87. ele.hint = '(disabled)';
  88. }
  89. // if the choice was already normalized, return it
  90. if (ele.index != null) return ele;
  91. ele.name = ele.name || ele.key || ele.title || ele.value || ele.message;
  92. ele.message = ele.message || ele.name || '';
  93. ele.value = [ele.value, ele.name].find(this.isValue.bind(this));
  94. ele.input = '';
  95. ele.index = i;
  96. ele.cursor = 0;
  97. utils.define(ele, 'parent', parent);
  98. ele.level = parent ? parent.level + 1 : 1;
  99. if (ele.indent == null) {
  100. ele.indent = parent ? parent.indent + ' ' : (ele.indent || '');
  101. }
  102. ele.path = parent ? parent.path + '.' + ele.name : ele.name;
  103. ele.enabled = !!(this.multiple && !this.isDisabled(ele) && (ele.enabled || this.isSelected(ele)));
  104. if (!this.isDisabled(ele)) {
  105. this.longest = Math.max(this.longest, stripAnsi(ele.message).length);
  106. }
  107. // shallow clone the choice first
  108. let choice = { ...ele };
  109. // then allow the choice to be reset using the "original" values
  110. ele.reset = (input = choice.input, value = choice.value) => {
  111. for (let key of Object.keys(choice)) ele[key] = choice[key];
  112. ele.input = input;
  113. ele.value = value;
  114. };
  115. if (origVal == null && typeof ele.initial === 'function') {
  116. ele.input = await ele.initial.call(this, this.state, ele, i);
  117. }
  118. return ele;
  119. }
  120. async onChoice(choice, i) {
  121. this.emit('choice', choice, i, this);
  122. if (typeof choice.onChoice === 'function') {
  123. await choice.onChoice.call(this, this.state, choice, i);
  124. }
  125. }
  126. async addChoice(ele, i, parent) {
  127. let choice = await this.toChoice(ele, i, parent);
  128. this.choices.push(choice);
  129. this.index = this.choices.length - 1;
  130. this.limit = this.choices.length;
  131. return choice;
  132. }
  133. async newItem(item, i, parent) {
  134. let ele = { name: 'New choice name?', editable: true, newChoice: true, ...item };
  135. let choice = await this.addChoice(ele, i, parent);
  136. choice.updateChoice = () => {
  137. delete choice.newChoice;
  138. choice.name = choice.message = choice.input;
  139. choice.input = '';
  140. choice.cursor = 0;
  141. };
  142. return this.render();
  143. }
  144. indent(choice) {
  145. if (choice.indent == null) {
  146. return choice.level > 1 ? ' '.repeat(choice.level - 1) : '';
  147. }
  148. return choice.indent;
  149. }
  150. dispatch(s, key) {
  151. if (this.multiple && this[key.name]) return this[key.name]();
  152. this.alert();
  153. }
  154. focus(choice, enabled) {
  155. if (typeof enabled !== 'boolean') enabled = choice.enabled;
  156. if (enabled && !choice.enabled && this.selected.length >= this.maxSelected) {
  157. return this.alert();
  158. }
  159. this.index = choice.index;
  160. choice.enabled = enabled && !this.isDisabled(choice);
  161. return choice;
  162. }
  163. space() {
  164. if (!this.multiple) return this.alert();
  165. if (!this.focused) return;
  166. this.toggle(this.focused);
  167. return this.render();
  168. }
  169. a() {
  170. if (this.maxSelected < this.choices.length) return this.alert();
  171. let enabled = this.selectable.every(ch => ch.enabled);
  172. this.choices.forEach(ch => (ch.enabled = !enabled));
  173. return this.render();
  174. }
  175. i() {
  176. // don't allow choices to be inverted if it will result in
  177. // more than the maximum number of allowed selected items.
  178. if (this.choices.length - this.selected.length > this.maxSelected) {
  179. return this.alert();
  180. }
  181. this.choices.forEach(ch => (ch.enabled = !ch.enabled));
  182. return this.render();
  183. }
  184. g() {
  185. if (!this.choices.some(ch => !!ch.parent)) return this.a();
  186. const focused = this.focused;
  187. this.toggle((focused.parent && !focused.choices) ? focused.parent : focused);
  188. return this.render();
  189. }
  190. toggle(choice, enabled) {
  191. if (!choice.enabled && this.selected.length >= this.maxSelected) {
  192. return this.alert();
  193. }
  194. if (typeof enabled !== 'boolean') enabled = !choice.enabled;
  195. choice.enabled = enabled;
  196. if (choice.choices) {
  197. choice.choices.forEach(ch => this.toggle(ch, enabled));
  198. }
  199. let parent = choice.parent;
  200. while (parent) {
  201. let choices = parent.choices.filter(ch => this.isDisabled(ch));
  202. parent.enabled = choices.every(ch => ch.enabled === true);
  203. parent = parent.parent;
  204. }
  205. reset(this, this.choices);
  206. this.emit('toggle', choice, this);
  207. return choice;
  208. }
  209. enable(choice) {
  210. if (this.selected.length >= this.maxSelected) return this.alert();
  211. choice.enabled = !this.isDisabled(choice);
  212. choice.choices && choice.choices.forEach(this.enable.bind(this));
  213. return choice;
  214. }
  215. disable(choice) {
  216. choice.enabled = false;
  217. choice.choices && choice.choices.forEach(this.disable.bind(this));
  218. return choice;
  219. }
  220. number(n) {
  221. this.num += n;
  222. let number = num => {
  223. let i = Number(num);
  224. if (i > this.choices.length - 1) return this.alert();
  225. let focused = this.focused;
  226. let choice = this.choices.find(ch => i === ch.index);
  227. if (!choice.enabled && this.selected.length >= this.maxSelected) {
  228. return this.alert();
  229. }
  230. if (this.visible.indexOf(choice) === -1) {
  231. let choices = reorder(this.choices);
  232. let actualIdx = choices.indexOf(choice);
  233. if (focused.index > actualIdx) {
  234. let start = choices.slice(actualIdx, actualIdx + this.limit);
  235. let end = choices.filter(ch => !start.includes(ch));
  236. this.choices = start.concat(end);
  237. } else {
  238. let pos = actualIdx - this.limit + 1;
  239. this.choices = choices.slice(pos).concat(choices.slice(0, pos));
  240. }
  241. }
  242. this.index = this.choices.indexOf(choice);
  243. this.toggle(this.focused);
  244. return this.render();
  245. };
  246. clearTimeout(this.numberTimeout);
  247. return new Promise(resolve => {
  248. let len = this.choices.length;
  249. let num = this.num;
  250. let handle = (val = false, res) => {
  251. clearTimeout(this.numberTimeout);
  252. if (val) res = number(num);
  253. this.num = '';
  254. resolve(res);
  255. };
  256. if (num === '0' || (num.length === 1 && Number(num + '0') > len)) {
  257. return handle(true);
  258. }
  259. if (Number(num) > len) {
  260. return handle(false, this.alert());
  261. }
  262. this.numberTimeout = setTimeout(() => handle(true), this.delay);
  263. });
  264. }
  265. home() {
  266. this.choices = reorder(this.choices);
  267. this.index = 0;
  268. return this.render();
  269. }
  270. end() {
  271. let pos = this.choices.length - this.limit;
  272. let choices = reorder(this.choices);
  273. this.choices = choices.slice(pos).concat(choices.slice(0, pos));
  274. this.index = this.limit - 1;
  275. return this.render();
  276. }
  277. first() {
  278. this.index = 0;
  279. return this.render();
  280. }
  281. last() {
  282. this.index = this.visible.length - 1;
  283. return this.render();
  284. }
  285. prev() {
  286. if (this.visible.length <= 1) return this.alert();
  287. return this.up();
  288. }
  289. next() {
  290. if (this.visible.length <= 1) return this.alert();
  291. return this.down();
  292. }
  293. right() {
  294. if (this.cursor >= this.input.length) return this.alert();
  295. this.cursor++;
  296. return this.render();
  297. }
  298. left() {
  299. if (this.cursor <= 0) return this.alert();
  300. this.cursor--;
  301. return this.render();
  302. }
  303. up() {
  304. let len = this.choices.length;
  305. let vis = this.visible.length;
  306. let idx = this.index;
  307. if (this.options.scroll === false && idx === 0) {
  308. return this.alert();
  309. }
  310. if (len > vis && idx === 0) {
  311. return this.scrollUp();
  312. }
  313. this.index = ((idx - 1 % len) + len) % len;
  314. if (this.isDisabled() && !this.allChoicesAreDisabled()) {
  315. return this.up();
  316. }
  317. return this.render();
  318. }
  319. down() {
  320. let len = this.choices.length;
  321. let vis = this.visible.length;
  322. let idx = this.index;
  323. if (this.options.scroll === false && idx === vis - 1) {
  324. return this.alert();
  325. }
  326. if (len > vis && idx === vis - 1) {
  327. return this.scrollDown();
  328. }
  329. this.index = (idx + 1) % len;
  330. if (this.isDisabled() && !this.allChoicesAreDisabled()) {
  331. return this.down();
  332. }
  333. return this.render();
  334. }
  335. scrollUp(i = 0) {
  336. this.choices = scrollUp(this.choices);
  337. this.index = i;
  338. if (this.isDisabled()) {
  339. return this.up();
  340. }
  341. return this.render();
  342. }
  343. scrollDown(i = this.visible.length - 1) {
  344. this.choices = scrollDown(this.choices);
  345. this.index = i;
  346. if (this.isDisabled()) {
  347. return this.down();
  348. }
  349. return this.render();
  350. }
  351. async shiftUp() {
  352. if (this.options.sort === true) {
  353. this.sorting = true;
  354. this.swap(this.index - 1);
  355. await this.up();
  356. this.sorting = false;
  357. return;
  358. }
  359. return this.scrollUp(this.index);
  360. }
  361. async shiftDown() {
  362. if (this.options.sort === true) {
  363. this.sorting = true;
  364. this.swap(this.index + 1);
  365. await this.down();
  366. this.sorting = false;
  367. return;
  368. }
  369. return this.scrollDown(this.index);
  370. }
  371. pageUp() {
  372. if (this.visible.length <= 1) return this.alert();
  373. this.limit = Math.max(this.limit - 1, 0);
  374. this.index = Math.min(this.limit - 1, this.index);
  375. this._limit = this.limit;
  376. if (this.isDisabled()) {
  377. return this.up();
  378. }
  379. return this.render();
  380. }
  381. pageDown() {
  382. if (this.visible.length >= this.choices.length) return this.alert();
  383. this.index = Math.max(0, this.index);
  384. this.limit = Math.min(this.limit + 1, this.choices.length);
  385. this._limit = this.limit;
  386. if (this.isDisabled()) {
  387. return this.down();
  388. }
  389. return this.render();
  390. }
  391. swap(pos) {
  392. swap(this.choices, this.index, pos);
  393. }
  394. allChoicesAreDisabled(choices = this.choices) {
  395. return choices.every(choice => this.isDisabled(choice));
  396. }
  397. isDisabled(choice = this.focused) {
  398. let keys = ['disabled', 'collapsed', 'hidden', 'completing', 'readonly'];
  399. if (choice && keys.some(key => choice[key] === true)) {
  400. return true;
  401. }
  402. return choice && choice.role === 'heading';
  403. }
  404. isEnabled(choice = this.focused) {
  405. if (Array.isArray(choice)) return choice.every(ch => this.isEnabled(ch));
  406. if (choice.choices) {
  407. let choices = choice.choices.filter(ch => !this.isDisabled(ch));
  408. return choice.enabled && choices.every(ch => this.isEnabled(ch));
  409. }
  410. return choice.enabled && !this.isDisabled(choice);
  411. }
  412. isChoice(choice, value) {
  413. return choice.name === value || choice.index === Number(value);
  414. }
  415. isSelected(choice) {
  416. if (Array.isArray(this.initial)) {
  417. return this.initial.some(value => this.isChoice(choice, value));
  418. }
  419. return this.isChoice(choice, this.initial);
  420. }
  421. map(names = [], prop = 'value') {
  422. return [].concat(names || []).reduce((acc, name) => {
  423. acc[name] = this.find(name, prop);
  424. return acc;
  425. }, {});
  426. }
  427. filter(value, prop) {
  428. let isChoice = (ele, i) => [ele.name, i].includes(value);
  429. let fn = typeof value === 'function' ? value : isChoice;
  430. let choices = this.options.multiple ? this.state._choices : this.choices;
  431. let result = choices.filter(fn);
  432. if (prop) {
  433. return result.map(ch => ch[prop]);
  434. }
  435. return result;
  436. }
  437. find(value, prop) {
  438. if (isObject(value)) return prop ? value[prop] : value;
  439. let isChoice = (ele, i) => [ele.name, i].includes(value);
  440. let fn = typeof value === 'function' ? value : isChoice;
  441. let choice = this.choices.find(fn);
  442. if (choice) {
  443. return prop ? choice[prop] : choice;
  444. }
  445. }
  446. findIndex(value) {
  447. return this.choices.indexOf(this.find(value));
  448. }
  449. async submit() {
  450. let choice = this.focused;
  451. if (!choice) return this.alert();
  452. if (choice.newChoice) {
  453. if (!choice.input) return this.alert();
  454. choice.updateChoice();
  455. return this.render();
  456. }
  457. if (this.choices.some(ch => ch.newChoice)) {
  458. return this.alert();
  459. }
  460. let { reorder, sort } = this.options;
  461. let multi = this.multiple === true;
  462. let value = this.selected;
  463. if (value === void 0) {
  464. return this.alert();
  465. }
  466. // re-sort choices to original order
  467. if (Array.isArray(value) && reorder !== false && sort !== true) {
  468. value = utils.reorder(value);
  469. }
  470. this.value = multi ? value.map(ch => ch.name) : value.name;
  471. return super.submit();
  472. }
  473. set choices(choices = []) {
  474. this.state._choices = this.state._choices || [];
  475. this.state.choices = choices;
  476. for (let choice of choices) {
  477. if (!this.state._choices.some(ch => ch.name === choice.name)) {
  478. this.state._choices.push(choice);
  479. }
  480. }
  481. if (!this._initial && this.options.initial) {
  482. this._initial = true;
  483. let init = this.initial;
  484. if (typeof init === 'string' || typeof init === 'number') {
  485. let choice = this.find(init);
  486. if (choice) {
  487. this.initial = choice.index;
  488. this.focus(choice, true);
  489. }
  490. }
  491. }
  492. }
  493. get choices() {
  494. return reset(this, this.state.choices || []);
  495. }
  496. set visible(visible) {
  497. this.state.visible = visible;
  498. }
  499. get visible() {
  500. return (this.state.visible || this.choices).slice(0, this.limit);
  501. }
  502. set limit(num) {
  503. this.state.limit = num;
  504. }
  505. get limit() {
  506. let { state, options, choices } = this;
  507. let limit = state.limit || this._limit || options.limit || choices.length;
  508. return Math.min(limit, this.height);
  509. }
  510. set value(value) {
  511. super.value = value;
  512. }
  513. get value() {
  514. if (typeof super.value !== 'string' && super.value === this.initial) {
  515. return this.input;
  516. }
  517. return super.value;
  518. }
  519. set index(i) {
  520. this.state.index = i;
  521. }
  522. get index() {
  523. return Math.max(0, this.state ? this.state.index : 0);
  524. }
  525. get enabled() {
  526. return this.filter(this.isEnabled.bind(this));
  527. }
  528. get focused() {
  529. let choice = this.choices[this.index];
  530. if (choice && this.state.submitted && this.multiple !== true) {
  531. choice.enabled = true;
  532. }
  533. return choice;
  534. }
  535. get selectable() {
  536. return this.choices.filter(choice => !this.isDisabled(choice));
  537. }
  538. get selected() {
  539. return this.multiple ? this.enabled : this.focused;
  540. }
  541. }
  542. function reset(prompt, choices) {
  543. if (choices instanceof Promise) return choices;
  544. if (typeof choices === 'function') {
  545. if (utils.isAsyncFn(choices)) return choices;
  546. choices = choices.call(prompt, prompt);
  547. }
  548. for (let choice of choices) {
  549. if (Array.isArray(choice.choices)) {
  550. let items = choice.choices.filter(ch => !prompt.isDisabled(ch));
  551. choice.enabled = items.every(ch => ch.enabled === true);
  552. }
  553. if (prompt.isDisabled(choice) === true) {
  554. delete choice.enabled;
  555. }
  556. }
  557. return choices;
  558. }
  559. module.exports = ArrayPrompt;