Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Prompts #43

Merged
merged 8 commits into from
Mar 4, 2018
216 changes: 97 additions & 119 deletions lib/prompts.js
Original file line number Diff line number Diff line change
@@ -1,186 +1,164 @@
'use strict';

const el = require('./elements');
const noop = () => {};
const noop = v => v;

function toPrompt(type, args, opts={}) {
if (typeof args.message !== 'string') {
throw new Error('message is required');
}

return new Promise((res, rej) => {
const p = new el[type](args);
const onAbort = opts.onAbort || noop;
const onSubmit = opts.onSubmit || noop;
p.on('state', args.onState || noop);
p.on('submit', x => res(onSubmit(x)));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now all prompts also have individual onAbort and onSubmit. That's cool

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Individual prompt functions, yeah. But not the user-defined prompt objects.

p.on('abort', x => rej(onAbort(x)));
});
}

/**
* Text prompt
* @param {string} message Prompt message to display
* @param {string} [initial] Default string value
* @param {string} [style="default"] Render style ('default', 'password', 'invisible')
* @param {function} [onState] On state change callback
* @param {string} args.message Prompt message to display
* @param {string} [args.initial] Default string value
* @param {string} [args.style="default"] Render style ('default', 'password', 'invisible')
* @param {function} [args.onState] On state change callback
* @returns {Promise} Promise with user input
*/
function text({ message, initial, style, onState = noop }) {
if (typeof message !== 'string') throw new Error('message is required');
return new Promise((resolve, reject) => {
const p = new el.TextPrompt({ message, initial, style });
p.on('submit', resolve);
p.on('abort', reject);
p.on('state', onState)
});
function text(args) {
return toPrompt('TextPrompt', args);
}

/**
* Password prompt with masked input
* @param {string} message Prompt message to display
* @param {string} [initial] Default string value
* @param {function} [onState] On state change callback
* @param {string} args.message Prompt message to display
* @param {string} [args.initial] Default string value
* @param {function} [args.onState] On state change callback
* @returns {Promise} Promise with user input
*
*/
const password = ({ message, initial, onState }) => text({ message, initial, onState, style: 'password' });
function password(args) {
args.style = 'password';
return text(args);
}

/**
* Prompt where input is invisible, like sudo
* @param {string} message Prompt message to display
* @param {string} [initial] Default string value
* @param {function} [onState] On state change callback
* @param {string} opts.message Prompt message to display
* @param {string} [opts.initial] Default string value
* @param {function} [opts.onState] On state change callback
* @returns {Promise} Promise with user input
*/
const invisible = ({ message, initial, onState }) => text({ message, initial, onState, style: 'invisible' });
function invisible(opts) {
opts.style = 'invisible';
return text(opts);
}

/**
* Number prompt
* @param {string} message Prompt message to display
* @param {number} initial Default number value
* @param {number} [max] Max value
* @param {number} [min] Min value
* @param {string} [style="default"] Render style ('default', 'password', 'invisible')
* @param {function} [onState] On state change callback
* @param {string} args.message Prompt message to display
* @param {number} args.initial Default number value
* @param {number} [args.max] Max value
* @param {number} [args.min] Min value
* @param {string} [args.style="default"] Render style ('default', 'password', 'invisible')
* @param {function} [args.onState] On state change callback
* @returns {Promise} Promise with user input
*/
function number({ message, initial, max, min, style, onState = noop }) {
if (typeof message !== 'string') throw new Error('message is required');
return new Promise((resolve, reject) => {
const p = new el.NumberPrompt({ message, initial, max, min, style });
p.on('submit', resolve);
p.on('abort', reject);
p.on('state', onState)
});
function number(args) {
return toPrompt('NumberPrompt', args);
}

/**
* Classic yes/no prompt
* @param {string} message Prompt message to display
* @param {boolean} [initial=false] Default value
* @param {function} [onState] On state change callback
* @param {string} args.message Prompt message to display
* @param {boolean} [args.initial=false] Default value
* @param {function} [args.onState] On state change callback
* @returns {Promise} Promise with user input
*/
function confirm({ message, initial, onState = noop }) {
if (typeof message !== 'string') throw new Error('message is required');
return new Promise((resolve, reject) => {
const p = new el.ConfirmPrompt({ message, initial });
p.on('submit', resolve);
p.on('abort', reject);
p.on('state', onState);
});
function confirm(args) {
return toPrompt('ConfirmPrompt', args);
}

/**
* List prompt, split intput string by `seperator`
* @param {string} message Prompt message to display
* @param {string} [initial] Default string value
* @param {string} [style="default"] Render style ('default', 'password', 'invisible')
* @param {string} [separator] String separator
* @param {function} [onState] On state change callback
* @param {string} args.message Prompt message to display
* @param {string} [args.initial] Default string value
* @param {string} [args.style="default"] Render style ('default', 'password', 'invisible')
* @param {string} [args.separator] String separator
* @param {function} [args.onState] On state change callback
* @returns {Promise} Promise with user input, in form of an `Array`
*/
function list({ message, initial, style, separator = ',', onState = noop }) {
if (typeof message !== 'string') throw new Error('message is required');
return new Promise((resolve, reject) => {
const p = new el.TextPrompt({ message, initial, style });
p.on('submit', str => resolve(str.split(separator).map(s => s.trim())));
p.on('abort', reject);
p.on('state', onState);
function list(args) {
const sep = args.separator || ',';
return toPrompt('TextPrompt', args, {
onSubmit: str => str.split(sep).map(s => s.trim())
});
}

/**
* Toggle/switch prompt
* @param {string} message Prompt message to display
* @param {boolean} [initial=false] Default value
* @param {string} [active="on"] Text for `active` state
* @param {string} [inactive="off"] Text for `inactive` state
* @param {function} [onState] On state change callback
* @param {string} args.message Prompt message to display
* @param {boolean} [args.initial=false] Default value
* @param {string} [args.active="on"] Text for `active` state
* @param {string} [args.inactive="off"] Text for `inactive` state
* @param {function} [args.onState] On state change callback
* @returns {Promise} Promise with user input
*/
function toggle({ message, initial, active, inactive, onState = noop }) {
if (typeof message !== 'string') throw new Error('message is required');
return new Promise((resolve, reject) => {
const p = new el.TogglePrompt({ message, initial, active, inactive });
p.on('submit', resolve);
p.on('abort', reject);
p.on('state', onState);
});
function toggle(args) {
return toPrompt('TogglePrompt', args);
}

/**
* Interactive select prompt
* @param {string} message Prompt message to display
* @param {Array} choices Array of choices objects `[{ title, value }, ...]`
* @param {number} [initial] Index of default value
* @param {function} [onState] On state change callback
* @param {string} arr.message Prompt message to display
* @param {Array} arr.choices Array of choices objects `[{ title, value }, ...]`
* @param {number} [arr.initial] Index of default value
* @param {function} [arr.onState] On state change callback
* @returns {Promise} Promise with user input
*/
function select({ message, choices, initial, onState = noop }) {
if (typeof message !== 'string') throw new Error('message is required');
return new Promise((resolve, reject) => {
const p = new el.SelectPrompt({ message, choices, initial });
p.on('submit', resolve);
p.on('abort', reject);
p.on('state', onState);
});
function select(args) {
return toPrompt('SelectPrompt', args);
}

/**
* Interactive multi-select prompt
* @param {string} message Prompt message to display
* @param {Array} choices Array of choices objects `[{ title, value, [selected] }, ...]`
* @param {number} [max] Max select
* @param {string} [hint] Hint to display user
* @param {function} [onState] On state change callback
* @param {string} args.message Prompt message to display
* @param {Array} args.choices Array of choices objects `[{ title, value, [selected] }, ...]`
* @param {number} [args.max] Max select
* @param {string} [args.hint] Hint to display user
* @param {function} [args.onState] On state change callback
* @returns {Promise} Promise with user input
*/
function multiselect({ message, choices, max, hint, onState = noop }) {
if (typeof message !== 'string') throw new Error('message is required');
if (!Array.isArray(choices)) throw new Error('choices array is required');

return new Promise((resolve, reject) => {
const p = new el.MultiselectPrompt({ message, choices, max, hint });
const selected = items => items.filter(item => item.selected).map(item => item.value);
p.on('submit', items => resolve(selected(items)));
p.on('abort', items => reject(selected(items)));
p.on('state', onState);
function multiselect(args) {
if (!Array.isArray(args.choices)) throw new Error('choices array is required');
const toSelected = items => items.filter(item => item.selected).map(item => item.value);
return toPrompt('MultiselectPrompt', args, {
onAbort: toSelected,
onSubmit: toSelected
});
}

/**
* Interactive multi-select prompt
* @param {string} message Prompt message to display
* @param {Array} choices Array of auto-complete choices objects `[{ title, value }, ...]`
* @param {Function} [suggest] Function to filter results based on user input. Defaults to stort by `title`
* @param {number} [limit=10] Max number of results to show
* @param {string} [style="default"] Render style ('default', 'password', 'invisible')
* @param {function} [onState] On state change callback
* @param {string} args.message Prompt message to display
* @param {Array} args.choices Array of auto-complete choices objects `[{ title, value }, ...]`
* @param {Function} [args.suggest] Function to filter results based on user input. Defaults to sort by `title`
* @param {number} [args.limit=10] Max number of results to show
* @param {string} [args.style="default"] Render style ('default', 'password', 'invisible')
* @param {function} [args.onState] On state change callback
* @returns {Promise} Promise with user input
*/
function autocomplete({ message, choices, suggest, limit, style, onState = noop }) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is still destructed object, but not args as expected below.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old commit. Fixed in 6457a15

if (typeof message !== 'string') throw new Error('message is required');
if (!Array.isArray(choices)) throw new Error('choices array is required');
const suggestByTitle = (input, choices) =>
Promise.resolve(
choices.filter(
item => item.title.slice(0, input.length).toLowerCase() === input.toLowerCase()
)
);
suggest = suggest ? suggest : suggestByTitle;
return new Promise((resolve, reject) => {
const p = new el.AutocompletePrompt({ message, choices, suggest, limit, style });
p.on('submit', resolve);
p.on('abort', reject);
p.on('state', onState);
});
if (!Array.isArray(args.choices)) throw new Error('choices array is required');
args.suggest = args.suggest || byTitle;
return toPrompt('AutocompletePrompt', args);
}

function byTitle(input, choices) {
return Promise.resolve(
choices.filter(item => item.title.slice(0, input.length).toLowerCase() === input.toLowerCase())
);
}

module.exports = {
Expand Down