Added support for multiple pages

This commit is contained in:
Nathan Byrd 2022-01-16 12:12:41 -06:00
parent 3c71f88cb8
commit 4fe55e1e1b
2 changed files with 351 additions and 251 deletions

View file

@ -21,8 +21,15 @@ function FullMenuView(options) {
MenuView.call(this, options); MenuView.call(this, options);
// Initialize paging
this.pages = [];
this.currentPage = 0;
this.initDefaultWidth(); this.initDefaultWidth();
const self = this; const self = this;
// we want page up/page down by default // we want page up/page down by default
@ -39,7 +46,32 @@ function FullMenuView(options) {
this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row);
} }
// Calculate number of items visible after adjusting height this.positionCacheExpired = true;
};
this.autoAdjustHeightIfEnabled();
this.getSpacer = function() {
return new Array(self.itemHorizSpacing + 1).join(this.fillChar);
}
this.clearPage = function() {
for (var i = 0; i < this.itemsPerRow; i++) {
let text = `${strUtil.pad(' ', this.dimens.width, this.fillChar, 'left')}`;
self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${text}`);
}
}
this.cachePositions = function() {
if (this.positionCacheExpired) {
this.autoAdjustHeightIfEnabled();
this.pages = []; // reset
// Calculate number of items visible per column
this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1)); this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1));
// handle case where one can fit at the end // handle case where one can fit at the end
if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) { if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) {
@ -51,36 +83,74 @@ function FullMenuView(options) {
this.itemsPerRow = this.items.length; this.itemsPerRow = this.items.length;
} }
}; var col = this.position.col;
var row = this.position.row;
this.autoAdjustHeightIfEnabled(); var spacer = this.getSpacer();
this.getSpacer = function() {
return new Array(self.itemHorizSpacing + 1).join(this.fillChar);
}
this.cachePositions = function() {
if (this.positionCacheExpired) {
this.autoAdjustHeightIfEnabled();
var col = self.position.col;
var row = self.position.row;
var spacer = self.getSpacer();
var itemInRow = 0; var itemInRow = 0;
for (var i = 0; i < self.items.length; ++i) { this.viewWindow = {
start: this.focusedItemIndex,
end: this.items.length - 1, // this may be adjusted later
};
var pageStart = 0;
for (var i = 0; i < this.items.length; ++i) {
itemInRow++; itemInRow++;
self.items[i].row = row; this.items[i].row = row;
self.items[i].col = col; this.items[i].col = col;
row += this.itemSpacing + 1; row += this.itemSpacing + 1;
// handle going to next column // have to calculate the max length on the last entry
if (itemInRow == this.itemsPerRow) { if (i == this.items.length - 1) {
var maxLength = 0;
for (var j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != this.items[i].col) {
break;
}
var itemLength = this.items[i - j].text.length;
if (itemLength > maxLength) {
maxLength = itemLength;
}
}
// set length on each item in the column
for (var j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != this.items[i].col) {
break;
}
this.items[i - j].fixedLength = maxLength;
}
// Check if we have room for this column
if (col + maxLength + spacer.length + 1 > this.position.col + this.dimens.width) {
// save previous page
this.pages.push({ start: pageStart, end: i - this.itemsPerRow });
// fix the last column processed
for (var j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != col) {
break;
}
this.items[i - j].col = this.position.col;
pageStart = i - j;
}
}
// Since this is the last page, save the current page as well
this.pages.push({ start: pageStart, end: i });
}
// also handle going to next column
else if (itemInRow == this.itemsPerRow) {
itemInRow = 0; itemInRow = 0;
row = self.position.row; // restart row for next column
row = this.position.row;
var maxLength = 0; var maxLength = 0;
for (var j = 0; j < this.itemsPerRow; j++) { for (var j = 0; j < this.itemsPerRow; j++) {
// TODO: handle complex items // TODO: handle complex items
@ -92,34 +162,37 @@ function FullMenuView(options) {
// set length on each item in the column // set length on each item in the column
for (var j = 0; j < this.itemsPerRow; j++) { for (var j = 0; j < this.itemsPerRow; j++) {
self.items[i - j].fixedLength = maxLength; this.items[i - j].fixedLength = maxLength;
}
// Check if we have room for this column in the current page
if (col + maxLength > this.position.col + this.dimens.width) {
// save previous page
this.pages.push({ start: pageStart, end: i - this.itemsPerRow });
// restart page start for next page
pageStart = i - this.itemsPerRow + 1;
// reset
col = this.position.col;
itemInRow = 0;
// fix the last column processed
for (var j = 0; j < this.itemsPerRow; j++) {
this.items[i - j].col = col;
}
} }
// increment the column // increment the column
col += maxLength + spacer.length + 1; col += maxLength + spacer.length + 1;
} }
// also have to calculate the max length on the last column
else if (i == self.items.length - 1) {
var maxLength = 0;
for (var j = 0; j < this.itemsPerRow; j++) {
if (self.items[i - j].col != self.items[i].col) {
break;
}
var itemLength = this.items[i - j].text.length;
if (itemLength > maxLength) {
maxLength = itemLength;
}
}
// set length on each item in the column
for (var j = 0; j < this.itemsPerRow; j++) {
if (self.items[i - j].col != self.items[i].col) {
break;
}
self.items[i - j].fixedLength = maxLength;
}
// Set the current page if the current item is focused.
if (this.focusedItemIndex === i) {
this.currentPage = this.pages.length;
} }
} }
} }
@ -128,32 +201,32 @@ function FullMenuView(options) {
}; };
this.drawItem = function(index) { this.drawItem = function(index) {
const item = self.items[index]; const item = this.items[index];
if (!item) { if (!item) {
return; return;
} }
const cached = this.getRenderCacheItem(index, item.focused); const cached = this.getRenderCacheItem(index, item.focused);
if (cached) { if (cached) {
return self.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`); return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`);
} }
let text; let text;
let sgr; let sgr;
if (item.focused && self.hasFocusItems()) { if (item.focused && this.hasFocusItems()) {
const focusItem = self.focusItems[index]; const focusItem = this.focusItems[index];
text = focusItem ? focusItem.text : item.text; text = focusItem ? focusItem.text : item.text;
sgr = ''; sgr = '';
} else if (this.complexItems) { } else if (this.complexItems) {
text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item));
sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); sgr = this.focusItemFormat ? '' : (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
} else { } else {
text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle);
sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
} }
text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; text = `${sgr}${strUtil.pad(text, this.fixedLength, this.fillChar, this.justify)}`;
self.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`);
this.setRenderCacheItem(index, text, item.focused); this.setRenderCacheItem(index, text, item.focused);
}; };
} }
@ -166,11 +239,6 @@ FullMenuView.prototype.redraw = function() {
this.cachePositions(); this.cachePositions();
// :TODO: rename positionCacheExpired to something that makese sense; combine methods for such
if (this.positionCacheExpired) {
this.autoAdjustHeightIfEnabled();
this.positionCacheExpired = false;
}
// erase old items // erase old items
// :TODO: optimize this: only needed if a item is removed or new max width < old. // :TODO: optimize this: only needed if a item is removed or new max width < old.
@ -189,7 +257,7 @@ FullMenuView.prototype.redraw = function() {
} }
if (this.items.length) { if (this.items.length) {
for (let i = 0; i < this.items.length; ++i) { for (let i = this.pages[this.currentPage].start; i <= this.pages[this.currentPage].end; ++i) {
this.items[i].focused = this.focusedItemIndex === i; this.items[i].focused = this.focusedItemIndex === i;
this.drawItem(i); this.drawItem(i);
} }
@ -203,6 +271,12 @@ FullMenuView.prototype.setHeight = function(height) {
this.autoAdjustHeight = false; this.autoAdjustHeight = false;
}; };
FullMenuView.prototype.setWidth = function(width) {
FullMenuView.super_.prototype.setWidth.call(this, width);
this.positionCacheExpired = true;
};
FullMenuView.prototype.setPosition = function(pos) { FullMenuView.prototype.setPosition = function(pos) {
FullMenuView.super_.prototype.setPosition.call(this, pos); FullMenuView.super_.prototype.setPosition.call(this, pos);
@ -273,11 +347,16 @@ FullMenuView.prototype.removeItem = function(index) {
FullMenuView.prototype.focusNext = function() { FullMenuView.prototype.focusNext = function() {
if (this.items.length - 1 === this.focusedItemIndex) { if (this.items.length - 1 === this.focusedItemIndex) {
this.clearPage();
this.focusedItemIndex = 0; this.focusedItemIndex = 0;
this.currentPage = 0;
} else { }
else {
this.focusedItemIndex++; this.focusedItemIndex++;
if (this.focusedItemIndex > this.pages[this.currentPage].end) {
this.clearPage();
this.currentPage++;
}
} }
this.redraw(); this.redraw();
@ -288,10 +367,14 @@ FullMenuView.prototype.focusNext = function() {
FullMenuView.prototype.focusPrevious = function() { FullMenuView.prototype.focusPrevious = function() {
if (0 === this.focusedItemIndex) { if (0 === this.focusedItemIndex) {
this.focusedItemIndex = this.items.length - 1; this.focusedItemIndex = this.items.length - 1;
this.currentPage = this.pages.length - 1;
}
} else { else {
this.clearPage();
this.focusedItemIndex--; this.focusedItemIndex--;
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
this.currentPage--;
}
} }
this.redraw(); this.redraw();
@ -303,8 +386,17 @@ FullMenuView.prototype.focusPreviousColumn = function() {
this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow;
if (this.focusedItemIndex < 0) { if (this.focusedItemIndex < 0) {
this.clearPage();
// add the negative index to the end of the list // add the negative index to the end of the list
this.focusedItemIndex = this.items.length + this.focusedItemIndex; this.focusedItemIndex = this.items.length + this.focusedItemIndex;
// set to last page
this.currentPage = this.pages.length - 1;
}
else {
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
this.clearPage();
this.currentPage--;
}
} }
this.redraw(); this.redraw();
@ -319,6 +411,12 @@ FullMenuView.prototype.focusNextColumn = function() {
if (this.focusedItemIndex > this.items.length - 1) { if (this.focusedItemIndex > this.items.length - 1) {
// add the overflow to the beginning of the list // add the overflow to the beginning of the list
this.focusedItemIndex = this.focusedItemIndex - this.items.length; this.focusedItemIndex = this.focusedItemIndex - this.items.length;
this.currentPage = 0;
this.clearPage();
}
else if (this.focusedItemIndex > this.pages[this.currentPage].end) {
this.clearPage();
this.currentPage++;
} }
this.redraw(); this.redraw();

View file

@ -23,7 +23,7 @@ function MenuView(options) {
const self = this; const self = this;
if(options.items) { if (options.items) {
this.setItems(options.items); this.setItems(options.items);
} else { } else {
this.items = []; this.items = [];
@ -53,9 +53,9 @@ function MenuView(options) {
}; };
this.getHotKeyItemIndex = function(ch) { this.getHotKeyItemIndex = function(ch) {
if(ch && self.hotKeys) { if (ch && self.hotKeys) {
const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch];
if(_.isNumber(keyIndex)) { if (_.isNumber(keyIndex)) {
return keyIndex; return keyIndex;
} }
} }
@ -70,7 +70,7 @@ function MenuView(options) {
util.inherits(MenuView, View); util.inherits(MenuView, View);
MenuView.prototype.setItems = function(items) { MenuView.prototype.setItems = function(items) {
if(Array.isArray(items)) { if (Array.isArray(items)) {
this.sorted = false; this.sorted = false;
this.renderCache = {}; this.renderCache = {};
@ -89,7 +89,7 @@ MenuView.prototype.setItems = function(items) {
let stringItem; let stringItem;
this.items = items.map(item => { this.items = items.map(item => {
stringItem = _.isString(item); stringItem = _.isString(item);
if(stringItem) { if (stringItem) {
text = item; text = item;
} else { } else {
text = item.text || ''; text = item.text || '';
@ -97,10 +97,10 @@ MenuView.prototype.setItems = function(items) {
} }
text = this.disablePipe ? text : pipeToAnsi(text, this.client); text = this.disablePipe ? text : pipeToAnsi(text, this.client);
return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others return Object.assign({}, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others
}); });
if(this.complexItems) { if (this.complexItems) {
this.itemFormat = this.itemFormat || '{text}'; this.itemFormat = this.itemFormat || '{text}';
} }
@ -127,25 +127,25 @@ MenuView.prototype.invalidateRenderCache = function() {
}; };
MenuView.prototype.setSort = function(sort) { MenuView.prototype.setSort = function(sort) {
if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { if (this.sorted || !Array.isArray(this.items) || 0 === this.items.length) {
return; return;
} }
const key = true === sort ? 'text' : sort; const key = true === sort ? 'text' : sort;
if('text' !== sort && !this.complexItems) { if ('text' !== sort && !this.complexItems) {
return; // need a valid sort key return; // need a valid sort key
} }
this.items.sort( (a, b) => { this.items.sort((a, b) => {
const a1 = a[key]; const a1 = a[key];
const b1 = b[key]; const b1 = b[key];
if(!a1) { if (!a1) {
return -1; return -1;
} }
if(!b1) { if (!b1) {
return 1; return 1;
} }
return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); return a1.localeCompare(b1, { sensitivity: false, numeric: true });
}); });
this.sorted = true; this.sorted = true;
@ -155,11 +155,11 @@ MenuView.prototype.removeItem = function(index) {
this.sorted = false; this.sorted = false;
this.items.splice(index, 1); this.items.splice(index, 1);
if(this.focusItems) { if (this.focusItems) {
this.focusItems.splice(index, 1); this.focusItems.splice(index, 1);
} }
if(this.focusedItemIndex >= index) { if (this.focusedItemIndex >= index) {
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0);
} }
@ -173,17 +173,17 @@ MenuView.prototype.getCount = function() {
}; };
MenuView.prototype.getItems = function() { MenuView.prototype.getItems = function() {
if(this.complexItems) { if (this.complexItems) {
return this.items; return this.items;
} }
return this.items.map( item => { return this.items.map(item => {
return item.text; return item.text;
}); });
}; };
MenuView.prototype.getItem = function(index) { MenuView.prototype.getItem = function(index) {
if(this.complexItems) { if (this.complexItems) {
return this.items[index]; return this.items[index];
} }
@ -220,10 +220,10 @@ MenuView.prototype.setFocusItemIndex = function(index) {
MenuView.prototype.onKeyPress = function(ch, key) { MenuView.prototype.onKeyPress = function(ch, key) {
const itemIndex = this.getHotKeyItemIndex(ch); const itemIndex = this.getHotKeyItemIndex(ch);
if(itemIndex >= 0) { if (itemIndex >= 0) {
this.setFocusItemIndex(itemIndex); this.setFocusItemIndex(itemIndex);
if(true === this.hotKeySubmit) { if (true === this.hotKeySubmit) {
this.emit('action', 'accept'); this.emit('action', 'accept');
} }
} }
@ -234,12 +234,12 @@ MenuView.prototype.onKeyPress = function(ch, key) {
MenuView.prototype.setFocusItems = function(items) { MenuView.prototype.setFocusItems = function(items) {
const self = this; const self = this;
if(items) { if (items) {
this.focusItems = []; this.focusItems = [];
items.forEach( itemText => { items.forEach(itemText => {
this.focusItems.push( this.focusItems.push(
{ {
text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) text: self.disablePipe ? itemText : pipeToAnsi(itemText, self.client)
} }
); );
}); });
@ -263,32 +263,34 @@ MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) {
}; };
MenuView.prototype.setPropertyValue = function(propName, value) { MenuView.prototype.setPropertyValue = function(propName, value) {
switch(propName) { switch (propName) {
case 'itemSpacing' : this.setItemSpacing(value); break; case 'itemSpacing': this.setItemSpacing(value); break;
case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break; case 'itemHorizSpacing': this.setItemHorizSpacing(value); break;
case 'items' : this.setItems(value); break; case 'items': this.setItems(value); break;
case 'focusItems' : this.setFocusItems(value); break; case 'focusItems': this.setFocusItems(value); break;
case 'hotKeys' : this.setHotKeys(value); break; case 'hotKeys': this.setHotKeys(value); break;
case 'hotKeySubmit' : this.hotKeySubmit = value; break; case 'hotKeySubmit': this.hotKeySubmit = value; break;
case 'justify' : this.justify = value; break; case 'justify': this.justify = value; break;
case 'focusItemIndex' : this.focusedItemIndex = value; break; case 'focusItemIndex': this.focusedItemIndex = value; break;
case 'itemFormat' : case 'itemFormat':
case 'focusItemFormat' : case 'focusItemFormat':
this[propName] = value; this[propName] = value;
// if there is a cache currently, invalidate it
this.invalidateRenderCache();
break; break;
case 'sort' : this.setSort(value); break; case 'sort': this.setSort(value); break;
} }
MenuView.super_.prototype.setPropertyValue.call(this, propName, value); MenuView.super_.prototype.setPropertyValue.call(this, propName, value);
}; };
MenuView.prototype.setHotKeys = function(hotKeys) { MenuView.prototype.setHotKeys = function(hotKeys) {
if(_.isObject(hotKeys)) { if (_.isObject(hotKeys)) {
if(this.caseInsensitiveHotKeys) { if (this.caseInsensitiveHotKeys) {
this.hotKeys = {}; this.hotKeys = {};
for(var key in hotKeys) { for (var key in hotKeys) {
this.hotKeys[key.toLowerCase()] = hotKeys[key]; this.hotKeys[key.toLowerCase()] = hotKeys[key];
} }
} else { } else {