-
-
Notifications
You must be signed in to change notification settings - Fork 302
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: autocomplete multiselect (#157)
- Loading branch information
1 parent
496f58d
commit 91be062
Showing
8 changed files
with
394 additions
and
230 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
'use strict'; | ||
|
||
const color = require('kleur'); | ||
const { cursor } = require('sisteransi'); | ||
const MultiselectPrompt = require('./multiselect'); | ||
const { clear, style, figures } = require('../util'); | ||
/** | ||
* MultiselectPrompt Base Element | ||
* @param {Object} opts Options | ||
* @param {String} opts.message Message | ||
* @param {Array} opts.choices Array of choice objects | ||
* @param {String} [opts.hint] Hint to display | ||
* @param {String} [opts.warn] Hint shown for disabled choices | ||
* @param {Number} [opts.max] Max choices | ||
* @param {Number} [opts.cursor=0] Cursor start position | ||
* @param {Stream} [opts.stdin] The Readable stream to listen to | ||
* @param {Stream} [opts.stdout] The Writable stream to write readline data to | ||
*/ | ||
class AutocompleteMultiselectPrompt extends MultiselectPrompt { | ||
constructor(opts={}) { | ||
opts.overrideRender = true; | ||
super(opts); | ||
this.inputValue = ''; | ||
this.clear = clear(''); | ||
this.filteredOptions = this.value; | ||
this.render(); | ||
} | ||
|
||
last() { | ||
this.cursor = this.filteredOptions.length - 1; | ||
this.render(); | ||
} | ||
next() { | ||
this.cursor = (this.cursor + 1) % this.filteredOptions.length; | ||
this.render(); | ||
} | ||
|
||
up() { | ||
if (this.cursor === 0) { | ||
this.cursor = this.filteredOptions.length - 1; | ||
} else { | ||
this.cursor--; | ||
} | ||
this.render(); | ||
} | ||
|
||
down() { | ||
if (this.cursor === this.filteredOptions.length - 1) { | ||
this.cursor = 0; | ||
} else { | ||
this.cursor++; | ||
} | ||
this.render(); | ||
} | ||
|
||
left() { | ||
this.filteredOptions[this.cursor].selected = false; | ||
this.render(); | ||
} | ||
|
||
right() { | ||
if (this.value.filter(e => e.selected).length >= this.maxChoices) return this.bell(); | ||
this.filteredOptions[this.cursor].selected = true; | ||
this.render(); | ||
} | ||
|
||
delete() { | ||
if (this.inputValue.length) { | ||
this.inputValue = this.inputValue.substr(0, this.inputValue.length - 1); | ||
this.updateFilteredOptions(); | ||
} | ||
} | ||
|
||
updateFilteredOptions() { | ||
const currentHighlight = this.filteredOptions[this.cursor]; | ||
this.filteredOptions = this.value | ||
.filter(v => { | ||
if (this.inputValue) { | ||
if (typeof v.title === 'string') { | ||
if (v.title.toLowerCase().includes(this.inputValue.toLowerCase())) { | ||
return true; | ||
} | ||
} | ||
if (typeof v.value === 'string') { | ||
if (v.value.toLowerCase().includes(this.inputValue.toLowerCase())) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
return true; | ||
}); | ||
const newHighlightIndex = this.filteredOptions.findIndex(v => v === currentHighlight) | ||
this.cursor = newHighlightIndex < 0 ? 0 : newHighlightIndex; | ||
this.render(); | ||
} | ||
|
||
handleSpaceToggle() { | ||
const v = this.filteredOptions[this.cursor]; | ||
|
||
if (v.selected) { | ||
v.selected = false; | ||
this.render(); | ||
} else if (v.disabled || this.value.filter(e => e.selected).length >= this.maxChoices) { | ||
return this.bell(); | ||
} else { | ||
v.selected = true; | ||
this.render(); | ||
} | ||
} | ||
|
||
handleInputChange(c) { | ||
this.inputValue = this.inputValue + c; | ||
this.updateFilteredOptions(); | ||
} | ||
|
||
_(c, key) { | ||
if (c === ' ') { | ||
this.handleSpaceToggle(); | ||
} else { | ||
this.handleInputChange(c); | ||
} | ||
} | ||
|
||
renderInstructions() { | ||
return ` | ||
Instructions: | ||
${figures.arrowUp}/${figures.arrowDown}: Highlight option | ||
${figures.arrowLeft}/${figures.arrowRight}/[space]: Toggle selection | ||
[a,b,c]/delete: Filter choices | ||
enter/return: Complete answer | ||
` | ||
} | ||
|
||
renderCurrentInput() { | ||
return ` | ||
Filtered results for: ${this.inputValue ? this.inputValue : color.gray('Enter something to filter')}\n`; | ||
} | ||
|
||
renderOption(cursor, v, i) { | ||
let title; | ||
if (v.disabled) title = cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title); | ||
else title = cursor === i ? color.cyan().underline(v.title) : v.title; | ||
return (v.selected ? color.green(figures.radioOn) : figures.radioOff) + ' ' + title | ||
} | ||
|
||
renderDoneOrInstructions() { | ||
if (this.done) { | ||
const selected = this.value | ||
.filter(e => e.selected) | ||
.map(v => v.title) | ||
.join(', '); | ||
return selected; | ||
} | ||
|
||
const output = [color.gray(this.hint), this.renderInstructions(), this.renderCurrentInput()]; | ||
|
||
if (this.filteredOptions.length && this.filteredOptions[this.cursor].disabled) { | ||
output.push(color.yellow(this.warn)); | ||
} | ||
return output.join(' '); | ||
} | ||
|
||
render() { | ||
if (this.closed) return; | ||
if (this.firstRender) this.out.write(cursor.hide); | ||
super.render(); | ||
|
||
// print prompt | ||
|
||
let prompt = [ | ||
style.symbol(this.done, this.aborted), | ||
color.bold(this.msg), | ||
style.delimiter(false), | ||
this.renderDoneOrInstructions() | ||
].join(' '); | ||
|
||
if (this.showMinError) { | ||
prompt += color.red(`You must select a minimum of ${this.minSelected} choices.`); | ||
this.showMinError = false; | ||
} | ||
prompt += this.renderOptions(this.filteredOptions); | ||
|
||
this.out.write(this.clear + prompt); | ||
this.clear = clear(prompt); | ||
} | ||
} | ||
|
||
module.exports = AutocompleteMultiselectPrompt; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.