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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to set hotkeys for multiselect #234

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ script:
- npm run build
- npm test
node_js:
- "stable"
- "8"
- "6"
Copy link
Author

Choose a reason for hiding this comment

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

Node 6 was already unsupported, because async/await was being used.

Copy link
Owner

Choose a reason for hiding this comment

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

Can you revert this please 👍

26 changes: 25 additions & 1 deletion lib/elements/multiselect.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class MultiselectPrompt extends Prompt {
this.showMinError = false;
this.maxChoices = opts.max;
this.instructions = opts.instructions;
this.hotkeys = opts.hotkeys || {};
this.optionsPerPage = opts.optionsPerPage || 10;
this.value = opts.choices.map((ch, idx) => {
if (typeof ch === 'string')
Expand Down Expand Up @@ -150,11 +151,27 @@ class MultiselectPrompt extends Prompt {
this.render();
}

_(c, key) {
Copy link
Author

Choose a reason for hiding this comment

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

This var was unused.

Copy link
Owner

Choose a reason for hiding this comment

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

Nice one!

_(c) {
if (c === ' ') {
this.handleSpaceToggle();
} else if (c === 'a') {
this.toggleAll();
} else if (Object.keys(this.hotkeys).includes(c)) {
const hotkeyResponse = this.hotkeys[c].handle() || {};
if (hotkeyResponse.answers) {
this.value.forEach(value => {
const answerForValue = hotkeyResponse.answers[value.title];
if (answerForValue !== undefined) {
value.selected = answerForValue;
this.render();
}
});
}

if (['abort', 'submit'].includes(hotkeyResponse.command)) {
this[hotkeyResponse.command]();
}

} else {
return this.bell();
}
Expand All @@ -165,10 +182,17 @@ class MultiselectPrompt extends Prompt {
if (typeof this.instructions === 'string') {
return this.instructions;
}

let hotkeyInstructions = Object.keys(this.hotkeys)
.map(key => [key, this.hotkeys[key]])
.reduce((instructions, [hotkeyChar, hotkeyDescriptor]) =>
instructions += ` ${hotkeyChar}: ${hotkeyDescriptor.instruction}\n`, '')

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` : '')
+ hotkeyInstructions
+ ` enter/return: Complete answer`;
}
return '';
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"start": "node lib/index.js",
"build": "babel lib -d dist",
"prepublishOnly": "npm run build",
"test": "tape test/*.js | tap-spec"
"test": "npm run build && tape test/*.js | tap-spec"
},
"keywords": [
"ui",
Expand Down
78 changes: 57 additions & 21 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<p align="center">
<img src="https://github.com/terkelg/prompts/raw/master/prompts.png" alt="Prompts" width="500" />
<img src="./prompts.png" alt="Prompts" width="500" />
Copy link
Author

Choose a reason for hiding this comment

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

This change:

  1. Allows offline local rendering.
  2. Improves portability if the repo were ever to move.
  3. Makes the markdown more concise.

Copy link
Owner

Choose a reason for hiding this comment

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

Nice one! How does this work with the readme shown on NPM? @lukeed you spent some time researching this right? I would be nice to have these readme updates in their own PR

Copy link
Author

Choose a reason for hiding this comment

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

The images still work on the npm registry page. For example, see: https://www.npmjs.com/package/list-maintainers.

Copy link
Author

Choose a reason for hiding this comment

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

I'm happy to split these changes out. I assume you only want the image href updates split out, but the docs for this feature should stay in this PR?

</p>

<h1 align="center">❯ Prompts</h1>
Expand Down Expand Up @@ -36,7 +36,7 @@
* **Unified**: consistent experience across all [prompts](#-types).


![split](https://github.com/terkelg/prompts/raw/master/media/split.png)
![split](./media/split.png)


## ❯ Install
Expand All @@ -47,11 +47,11 @@ $ npm install --save prompts

> This package supports Node 6 and above

![split](https://github.com/terkelg/prompts/raw/master/media/split.png)
![split](./media/split.png)

## ❯ Usage

<img src="https://github.com/terkelg/prompts/raw/master/media/example.gif" alt="example prompt" width="499" height="103" />
<img src="./media/example.gif" alt="example prompt" width="499" height="103" />

```js
const prompts = require('prompts');
Expand All @@ -71,7 +71,7 @@ const prompts = require('prompts');
> See [`example.js`](https://github.com/terkelg/prompts/blob/master/example.js) for more options.


![split](https://github.com/terkelg/prompts/raw/master/media/split.png)
![split](./media/split.png)


## ❯ Examples
Expand Down Expand Up @@ -155,7 +155,7 @@ const questions = [
```


![split](https://github.com/terkelg/prompts/raw/master/media/split.png)
![split](./media/split.png)


## ❯ API
Expand Down Expand Up @@ -299,7 +299,7 @@ prompts.inject([ '@terkelg', ['#ff0000', '#0000ff'] ]);
})();
```

![split](https://github.com/terkelg/prompts/raw/master/media/split.png)
![split](./media/split.png)


## ❯ Prompt Objects
Expand Down Expand Up @@ -418,7 +418,7 @@ The function signature is `(state)` where `state` is an object with a snapshot o
The state object has two properties `value` and `aborted`. E.g `{ value: 'This is ', aborted: false }`


![split](https://github.com/terkelg/prompts/raw/master/media/split.png)
![split](./media/split.png)


## ❯ Types
Expand All @@ -444,7 +444,7 @@ The state object has two properties `value` and `aborted`. E.g `{ value: 'This i
Hit <kbd>tab</kbd> to autocomplete to `initial` value when provided.

#### Example
<img src="https://github.com/terkelg/prompts/raw/master/media/text.gif" alt="text prompt" width="499" height="103" />
<img src="./media/text.gif" alt="text prompt" width="499" height="103" />

```js
{
Expand Down Expand Up @@ -475,7 +475,7 @@ Hit <kbd>tab</kbd> to autocomplete to `initial` value when provided.
This prompt is a similar to a prompt of type `'text'` with `style` set to `'password'`.

#### Example
<img src="https://github.com/terkelg/prompts/raw/master/media/password.gif" alt="password prompt" width="499" height="103" />
<img src="./media/password.gif" alt="password prompt" width="499" height="103" />

```js
{
Expand Down Expand Up @@ -506,7 +506,7 @@ This prompt is working like `sudo` where the input is invisible.
This prompt is a similar to a prompt of type `'text'` with style set to `'invisible'`.

#### Example
<img src="https://github.com/terkelg/prompts/raw/master/media/invisible.gif" alt="invisible prompt" width="499" height="103" />
<img src="./media/invisible.gif" alt="invisible prompt" width="499" height="103" />

```js
{
Expand Down Expand Up @@ -536,7 +536,7 @@ This prompt is a similar to a prompt of type `'text'` with style set to `'invisi
You can type in numbers and use <kbd>up</kbd>/<kbd>down</kbd> to increase/decrease the value. Only numbers are allowed as input. Hit <kbd>tab</kbd> to autocomplete to `initial` value when provided.

#### Example
<img src="https://github.com/terkelg/prompts/raw/master/media/number.gif" alt="number prompt" width="499" height="103" />
<img src="./media/number.gif" alt="number prompt" width="499" height="103" />

```js
{
Expand Down Expand Up @@ -576,7 +576,7 @@ You can type in numbers and use <kbd>up</kbd>/<kbd>down</kbd> to increase/decrea
Hit <kbd>y</kbd> or <kbd>n</kbd> to confirm/reject.

#### Example
<img src="https://github.com/terkelg/prompts/raw/master/media/confirm.gif" alt="confirm prompt" width="499" height="103" />
<img src="./media/confirm.gif" alt="confirm prompt" width="499" height="103" />

```js
{
Expand Down Expand Up @@ -617,7 +617,7 @@ string separated by `separator`.
}
```

<img src="https://github.com/terkelg/prompts/raw/master/media/list.gif" alt="list prompt" width="499" height="103" />
<img src="./media/list.gif" alt="list prompt" width="499" height="103" />


| Param | Type | Description |
Expand All @@ -639,7 +639,7 @@ string separated by `separator`.
Use tab or <kbd>arrow keys</kbd>/<kbd>tab</kbd>/<kbd>space</kbd> to switch between options.

#### Example
<img src="https://github.com/terkelg/prompts/raw/master/media/toggle.gif" alt="toggle prompt" width="499" height="103" />
<img src="./media/toggle.gif" alt="toggle prompt" width="499" height="103" />

```js
{
Expand Down Expand Up @@ -673,7 +673,7 @@ Use tab or <kbd>arrow keys</kbd>/<kbd>tab</kbd>/<kbd>space</kbd> to switch betwe
Use <kbd>up</kbd>/<kbd>down</kbd> to navigate. Use <kbd>tab</kbd> to cycle the list.

#### Example
<img src="https://github.com/terkelg/prompts/raw/master/media/select.gif" alt="select prompt" width="499" height="130" />
<img src="./media/select.gif" alt="select prompt" width="499" height="130" />

```js
{
Expand Down Expand Up @@ -714,7 +714,7 @@ Use <kbd>space</kbd> to toggle select/unselect and <kbd>up</kbd>/<kbd>down</kbd>
By default this prompt returns an `array` containing the **values** of the selected items - not their display title.

#### Example
<img src="https://github.com/terkelg/prompts/raw/master/media/multiselect.gif" alt="multiselect prompt" width="499" height="130" />
<img src="./media/multiselect.gif" alt="multiselect prompt" width="499" height="130" />

```js
{
Expand All @@ -727,6 +727,17 @@ By default this prompt returns an `array` containing the **values** of the selec
{ title: 'Blue', value: '#0000ff', selected: true }
],
max: 2,
hotkeys: {
d: {
handle() {
return {
answer: {Red: true, Green: false},
command: 'submit'
}
},
instruction: 'Enable red, disable green, and submit the answer.'
}
}
hint: '- Space to select. Return to submit'
}
```
Expand All @@ -745,10 +756,35 @@ By default this prompt returns an `array` containing the **values** of the selec
| warn | `string` | Message to display when selecting a disabled option |
| onRender | `function` | On render callback. Keyword `this` refers to the current prompt |
| onState | `function` | On state change callback. Function signature is an `object` with two properties: `value` and `aborted` |
| hotkeys | `{[hotkeyCharacter]: hotkeyDescriptor}` | Define hotkeys that can select multiple answers at once. |

This is one of the few prompts that don't take a initial value.
If you want to predefine selected values, give the choice object an `selected` property of `true`.

The type of `hotkeyDescriptor` is:

```ts
{
// You can do whatever you want in this function (e.g. trigger a side effect). You can also customize the choices object.
handle: () => {

// Pass "submit" to have this question be submitted when the user presses the hotkey.
// Pass "abort" to have the question be aborted (as if the user had hit control+c).
command?: 'submit' | 'abort',

answers: {
// The key in this object is the `title` field in the `choices` array passed above.
// The value is whether this choice will be selected after the hotkey is pressed.
// Pass true to select the choice. False deselects the choice. Undefined leaves the choice as-is.
[answerTitle]: boolean
}
},
instruction: string
}
```

See the [multiselect test fixture](./test/fixtures/multiselect.js) for more examples of using hotkeys.

**↑ back to:** [Prompt types](#-types)

***
Expand All @@ -763,7 +799,7 @@ The default suggests function is sorting based on the `title` property of the ch
You can overwrite how choices are being filtered by passing your own suggest function.

#### Example
<img src="https://github.com/terkelg/prompts/raw/master/media/autocomplete.gif" alt="auto complete prompt" width="499" height="163" />
<img src="./media/autocomplete.gif" alt="auto complete prompt" width="499" height="163" />

```js
{
Expand Down Expand Up @@ -810,7 +846,7 @@ const suggestByTitle = (input, choices) =>
Use <kbd>left</kbd>/<kbd>right</kbd>/<kbd>tab</kbd> to navigate. Use <kbd>up</kbd>/<kbd>down</kbd> to change date.

#### Example
<img src="https://github.com/terkelg/prompts/raw/master/media/date.gif" alt="date prompt" width="499" height="103" />
<img src="./media/date.gif" alt="date prompt" width="499" height="103" />

```js
{
Expand All @@ -835,7 +871,7 @@ Use <kbd>left</kbd>/<kbd>right</kbd>/<kbd>tab</kbd> to navigate. Use <kbd>up</kb

Default locales:

```javascript
```js
{
months: [
'January', 'February', 'March', 'April',
Expand All @@ -857,7 +893,7 @@ Default locales:
```
>**Formatting**: See full list of formatting options in the [wiki](https://github.com/terkelg/prompts/wiki/Date-Time-Formatting)

![split](https://github.com/terkelg/prompts/raw/master/media/split.png)
![split](./media/split.png)

**↑ back to:** [Prompt types](#-types)

Expand Down
40 changes: 40 additions & 0 deletions test/fixtures/multiselect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const prompts = require('../..');

prompts([
{
type: 'multiselect',
name: 'color',
message: 'Pick colors',
choices: [
{ title: 'Red', value: '#ff0000' },
{ title: 'Green', value: '#00ff00' },
{ title: 'Blue', value: '#0000ff' }
],
hotkeys: {
r: {
handle() {
return {answers: {Red: true, Green: true}};
},
instruction: 'Choose Red and Green'
},
d: {
handle() {
return {command: 'abort'};
},
instruction: 'Abort'
},
e: {
handle() {
return {answers: {Green: true, Blue: true}, command: 'submit'}
},
instruction: 'Select Green and Blue, and move on the to the next question'
},
f: {
handle() {
return {answers: {Red: false, Blue: true}, command: 'submit'}
},
instruction: 'Pay respect. Also, enable Blue and disable Red.'
}
}
}
]).then(response => console.error(JSON.stringify(response, null, 2)));
Loading