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 maxVisible option for select prompts #225

Merged
merged 3 commits into from
Nov 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 21 additions & 22 deletions lib/elements/multiselect.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const color = require('kleur');
const { cursor } = require('sisteransi');
const Prompt = require('./prompt');
const { clear, figures, style, wrap, strip } = require('../util');
const { clear, figures, style, wrap, entriesToDisplay } = require('../util');

/**
* MultiselectPrompt Base Element
Expand All @@ -14,6 +14,7 @@ const { clear, figures, style, wrap, strip } = require('../util');
* @param {String} [opts.warn] Hint shown for disabled choices
* @param {Number} [opts.max] Max choices
* @param {Number} [opts.cursor=0] Cursor start position
* @param {Number} [opts.optionsPerPage=10] Max options to display at once
* @param {Stream} [opts.stdin] The Readable stream to listen to
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
*/
Expand Down Expand Up @@ -173,11 +174,11 @@ class MultiselectPrompt extends Prompt {
return '';
}

renderOption(cursor, v, i) {
const prefix = (v.selected ? color.green(figures.radioOn) : figures.radioOff) + ' ';
renderOption(cursor, v, i, arrowIndicator) {
const prefix = (v.selected ? color.green(figures.radioOn) : figures.radioOff) + ' ' + arrowIndicator + ' ';
let title, desc;

if(v.disabled) {
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;
Expand All @@ -195,27 +196,25 @@ class MultiselectPrompt extends Prompt {

// shared with autocompleteMultiselect
paginateOptions(options) {
const c = this.cursor;
let styledOptions = options.map((v, i) => this.renderOption(c, v, i));

let scopedOptions = styledOptions;
let hint = '';
if (styledOptions.length === 0) {
if (options.length === 0) {
return color.red('No matches for this query.');
} else if (styledOptions.length > this.optionsPerPage) {
let startIndex = c - (this.optionsPerPage / 2);
let endIndex = c + (this.optionsPerPage / 2);
if (startIndex < 0) {
startIndex = 0;
endIndex = this.optionsPerPage;
} else if (endIndex > options.length) {
endIndex = options.length;
startIndex = endIndex - this.optionsPerPage;
}

let { startIndex, endIndex } = entriesToDisplay(this.cursor, options.length, this.optionsPerPage);
let prefix, styledOptions = [];

for (let i = startIndex; i < endIndex; i++) {
if (i === startIndex && startIndex > 0) {
prefix = figures.arrowUp;
} else if (i === endIndex - 1 && endIndex < options.length) {
prefix = figures.arrowDown;
} else {
prefix = ' ';
}
scopedOptions = styledOptions.slice(startIndex, endIndex);
hint = color.dim('(Move up and down to reveal more choices)');
styledOptions.push(this.renderOption(this.cursor, options[i], i, prefix));
}
return '\n' + scopedOptions.join('\n') + '\n' + hint;

return '\n' + styledOptions.join('\n');
}

// shared with autocomleteMultiselect
Expand Down
57 changes: 35 additions & 22 deletions lib/elements/select.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const color = require('kleur');
const Prompt = require('./prompt');
const { style, clear, figures, wrap } = require('../util');
const { style, clear, figures, wrap, entriesToDisplay } = require('../util');
const { cursor } = require('sisteransi');

/**
Expand All @@ -14,6 +14,7 @@ const { cursor } = require('sisteransi');
* @param {Number} [opts.initial] Index of default value
* @param {Stream} [opts.stdin] The Readable stream to listen to
* @param {Stream} [opts.stdout] The Writable stream to write readline data to
* @param {Number} [opts.optionsPerPage=10] Max options to display at once
*/
class SelectPrompt extends Prompt {
constructor(opts={}) {
Expand All @@ -33,6 +34,7 @@ class SelectPrompt extends Prompt {
disabled: ch && ch.disabled
};
});
this.optionsPerPage = opts.optionsPerPage || 10;
this.value = (this.choices[this.cursor] || {}).value;
this.clear = clear('');
this.render();
Expand Down Expand Up @@ -111,6 +113,8 @@ class SelectPrompt extends Prompt {
else this.out.write(clear(this.outputText));
super.render();

let { startIndex, endIndex } = entriesToDisplay(this.cursor, this.choices.length, this.optionsPerPage);

// Print prompt
this.outputText = [
style.symbol(this.done, this.aborted),
Expand All @@ -122,27 +126,36 @@ class SelectPrompt extends Prompt {

// Print choices
if (!this.done) {
this.outputText += '\n' +
this.choices
.map((v, i) => {
let title, prefix, desc = '';
if (v.disabled) {
title = this.cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
prefix = this.cursor === i ? color.bold().gray(figures.pointer) + ' ' : ' ';
} else {
title = this.cursor === i ? color.cyan().underline(v.title) : v.title;
prefix = this.cursor === i ? color.cyan(figures.pointer) + ' ' : ' ';
if (v.description && this.cursor === i) {
desc = ` - ${v.description}`;
if (prefix.length + title.length + desc.length >= this.out.columns
|| v.description.split(/\r?\n/).length > 1) {
desc = '\n' + wrap(v.description, { margin: 3, width: this.out.columns });
}
}
}
return `${prefix} ${title}${color.gray(desc)}`;
})
.join('\n');
this.outputText += '\n';
for (let i = startIndex; i < endIndex; i++) {
let title, prefix, desc = '', v = this.choices[i];

// Determine whether to display "more choices" indicators
if (i === startIndex && startIndex > 0) {
prefix = figures.arrowUp;
} else if (i === endIndex - 1 && endIndex < this.choices.length) {
prefix = figures.arrowDown;
} else {
prefix = ' ';
}

if (v.disabled) {
title = this.cursor === i ? color.gray().underline(v.title) : color.strikethrough().gray(v.title);
prefix = (this.cursor === i ? color.bold().gray(figures.pointer) + ' ' : ' ') + prefix;
} else {
title = this.cursor === i ? color.cyan().underline(v.title) : v.title;
prefix = (this.cursor === i ? color.cyan(figures.pointer) + ' ' : ' ') + prefix;
if (v.description && this.cursor === i) {
desc = ` - ${v.description}`;
if (prefix.length + title.length + desc.length >= this.out.columns
|| v.description.split(/\r?\n/).length > 1) {
desc = '\n' + wrap(v.description, { margin: 3, width: this.out.columns });
}
}
}

this.outputText += `${prefix} ${title}${color.gray(desc)}\n`;
}
}

this.out.write(this.outputText);
Expand Down
21 changes: 21 additions & 0 deletions lib/util/entriesToDisplay.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

/**
* Determine what entries should be displayed on the screen, based on the
* currently selected index and the maximum visible. Used in list-based
* prompts like `select` and `multiselect`.
*
* @param {number} cursor the currently selected entry
* @param {number} total the total entries available to display
* @param {number} [maxVisible] the number of entries that can be displayed
*/
module.exports = (cursor, total, maxVisible) => {
maxVisible = maxVisible || total;

let startIndex = Math.min(total- maxVisible, cursor - Math.floor(maxVisible / 2));
if (startIndex < 0) startIndex = 0;

let endIndex = Math.min(startIndex + maxVisible, total);

return { startIndex, endIndex };
};
3 changes: 2 additions & 1 deletion lib/util/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ module.exports = {
strip: require('./strip'),
figures: require('./figures'),
lines: require('./lines'),
wrap: require('./wrap')
wrap: require('./wrap'),
entriesToDisplay: require('./entriesToDisplay')
};
21 changes: 21 additions & 0 deletions test/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

const test = require('tape');
const { entriesToDisplay } = require('../lib/util');

test('entriesToDisplay', t => {
t.plan(11);
t.deepEqual(entriesToDisplay(0, 8, 5), { startIndex: 0, endIndex: 5 }, 'top of list');
t.deepEqual(entriesToDisplay(1, 8, 5), { startIndex: 0, endIndex: 5 }, '+1 from top');
t.deepEqual(entriesToDisplay(2, 8, 5), { startIndex: 0, endIndex: 5 }, '+2 from top');
t.deepEqual(entriesToDisplay(3, 8, 5), { startIndex: 1, endIndex: 6 }, '+3 from top');
t.deepEqual(entriesToDisplay(4, 8, 5), { startIndex: 2, endIndex: 7 }, '-3 from bottom');
t.deepEqual(entriesToDisplay(5, 8, 5), { startIndex: 3, endIndex: 8 }, '-2 from bottom');
t.deepEqual(entriesToDisplay(6, 8, 5), { startIndex: 3, endIndex: 8 }, '-1 from bottom');
t.deepEqual(entriesToDisplay(7, 8, 5), { startIndex: 3, endIndex: 8 }, 'bottom of list');

t.deepEqual(entriesToDisplay(0, 10, 11), { startIndex: 0, endIndex: 10 }, 'top of list when maxVisible greater than total');
t.deepEqual(entriesToDisplay(9, 10, 11), { startIndex: 0, endIndex: 10 }, 'bottom of list maxVisible greater than total');

t.deepEqual(entriesToDisplay(0, 10), { startIndex: 0, endIndex:10 }, 'maxVisible is optional');
});