From 3c71f88cb83db88bcd5d6a0357bb171b1bba749d Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Wed, 12 Jan 2022 10:38:47 -0600 Subject: [PATCH 01/14] Initial support for full menu --- core/full_menu_view.js | 392 +++++++++++++++++++++++++++++++++++++++ core/mci_view_factory.js | 15 +- core/menu_view.js | 12 +- 3 files changed, 417 insertions(+), 2 deletions(-) create mode 100644 core/full_menu_view.js diff --git a/core/full_menu_view.js b/core/full_menu_view.js new file mode 100644 index 00000000..87346b9d --- /dev/null +++ b/core/full_menu_view.js @@ -0,0 +1,392 @@ +/* jslint node: true */ +'use strict'; + +// ENiGMA½ +const MenuView = require('./menu_view.js').MenuView; +const ansi = require('./ansi_term.js'); +const strUtil = require('./string_util.js'); +const formatString = require('./string_format'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; + +// deps +const util = require('util'); +const _ = require('lodash'); + +exports.FullMenuView = FullMenuView; + +function FullMenuView(options) { + options.cursor = options.cursor || 'hide'; + options.justify = options.justify || 'left'; + + + MenuView.call(this, options); + + this.initDefaultWidth(); + + const self = this; + + // we want page up/page down by default + if (!_.isObject(options.specialKeyMap)) { + Object.assign(this.specialKeyMap, { + 'page up': ['page up'], + 'page down': ['page down'], + }); + } + + this.autoAdjustHeightIfEnabled = function() { + if (this.autoAdjustHeight) { + this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing); + this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); + } + + // Calculate number of items visible after adjusting height + this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1)); + // handle case where one can fit at the end + if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) { + this.itemsPerRow++; + } + + // Final check to make sure we don't try to display more than we have + if (this.itemsPerRow > this.items.length) { + this.itemsPerRow = this.items.length; + } + + }; + + this.autoAdjustHeightIfEnabled(); + + 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; + + for (var i = 0; i < self.items.length; ++i) { + itemInRow++; + self.items[i].row = row; + self.items[i].col = col; + + row += this.itemSpacing + 1; + + // handle going to next column + if (itemInRow == this.itemsPerRow) { + itemInRow = 0; + + row = self.position.row; + var maxLength = 0; + for (var j = 0; j < this.itemsPerRow; j++) { + // TODO: handle complex items + 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++) { + self.items[i - j].fixedLength = maxLength; + } + + // increment the column + 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; + } + + } + } + } + + this.positionCacheExpired = false; + }; + + this.drawItem = function(index) { + const item = self.items[index]; + if (!item) { + return; + } + + const cached = this.getRenderCacheItem(index, item.focused); + if (cached) { + return self.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`); + } + + let text; + let sgr; + if (item.focused && self.hasFocusItems()) { + const focusItem = self.focusItems[index]; + text = focusItem ? focusItem.text : item.text; + sgr = ''; + } else if (this.complexItems) { + text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item)); + sgr = this.focusItemFormat ? '' : (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } else { + text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); + sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + } + + text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; + self.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); + this.setRenderCacheItem(index, text, item.focused); + }; +} + +util.inherits(FullMenuView, MenuView); + + +FullMenuView.prototype.redraw = function() { + FullMenuView.super_.prototype.redraw.call(this); + + 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 + // :TODO: optimize this: only needed if a item is removed or new max width < old. + if (this.oldDimens) { + const blank = new Array(Math.max(this.oldDimens.width, this.dimens.width)).join(' '); + let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank; + let row = this.position.row + 1; + const endRow = (row + this.oldDimens.height) - 2; + + while (row <= endRow) { + seq += ansi.goto(row, this.position.col) + blank; + row += 1; + } + this.client.term.write(seq); + delete this.oldDimens; + } + + if (this.items.length) { + for (let i = 0; i < this.items.length; ++i) { + this.items[i].focused = this.focusedItemIndex === i; + this.drawItem(i); + } + } +}; + +FullMenuView.prototype.setHeight = function(height) { + FullMenuView.super_.prototype.setHeight.call(this, height); + + this.positionCacheExpired = true; + this.autoAdjustHeight = false; +}; + +FullMenuView.prototype.setPosition = function(pos) { + FullMenuView.super_.prototype.setPosition.call(this, pos); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.setFocus = function(focused) { + FullMenuView.super_.prototype.setFocus.call(this, focused); + + this.redraw(); +}; + +FullMenuView.prototype.setFocusItemIndex = function(index) { + FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex + + this.redraw(); +}; + +FullMenuView.prototype.onKeyPress = function(ch, key) { + if (key) { + if (this.isKeyMapped('up', key.name)) { + this.focusPrevious(); + } else if (this.isKeyMapped('down', key.name)) { + this.focusNext(); + } else if (this.isKeyMapped('left', key.name)) { + this.focusPreviousColumn(); + } else if (this.isKeyMapped('right', key.name)) { + this.focusNextColumn(); + } else if (this.isKeyMapped('page up', key.name)) { + this.focusPreviousPageItem(); + } else if (this.isKeyMapped('page down', key.name)) { + this.focusNextPageItem(); + } else if (this.isKeyMapped('home', key.name)) { + this.focusFirst(); + } else if (this.isKeyMapped('end', key.name)) { + this.focusLast(); + } + } + + FullMenuView.super_.prototype.onKeyPress.call(this, ch, key); +}; + +FullMenuView.prototype.getData = function() { + const item = this.getItem(this.focusedItemIndex); + return _.isString(item.data) ? item.data : this.focusedItemIndex; +}; + +FullMenuView.prototype.setItems = function(items) { + // if we have items already, save off their drawing area so we don't leave fragments at redraw + if (this.items && this.items.length) { + this.oldDimens = Object.assign({}, this.dimens); + } + + FullMenuView.super_.prototype.setItems.call(this, items); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.removeItem = function(index) { + if (this.items && this.items.length) { + this.oldDimens = Object.assign({}, this.dimens); + } + + FullMenuView.super_.prototype.removeItem.call(this, index); +}; + +// :TODO: Apply draw optimizaitons when only two items need drawn vs entire view! + +FullMenuView.prototype.focusNext = function() { + if (this.items.length - 1 === this.focusedItemIndex) { + this.focusedItemIndex = 0; + + } else { + this.focusedItemIndex++; + + } + + this.redraw(); + + FullMenuView.super_.prototype.focusNext.call(this); +}; + +FullMenuView.prototype.focusPrevious = function() { + if (0 === this.focusedItemIndex) { + this.focusedItemIndex = this.items.length - 1; + + + } else { + this.focusedItemIndex--; + } + + this.redraw(); + + FullMenuView.super_.prototype.focusPrevious.call(this); +}; + +FullMenuView.prototype.focusPreviousColumn = function() { + + this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; + if (this.focusedItemIndex < 0) { + // add the negative index to the end of the list + this.focusedItemIndex = this.items.length + this.focusedItemIndex; + } + + this.redraw(); + + // TODO: This isn't specific to Previous, may want to replace in the future + FullMenuView.super_.prototype.focusPrevious.call(this); +}; + +FullMenuView.prototype.focusNextColumn = function() { + + this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; + if (this.focusedItemIndex > this.items.length - 1) { + // add the overflow to the beginning of the list + this.focusedItemIndex = this.focusedItemIndex - this.items.length; + } + + this.redraw(); + + // TODO: This isn't specific to Next, may want to replace in the future + FullMenuView.super_.prototype.focusNext.call(this); +}; + + +FullMenuView.prototype.focusPreviousPageItem = function() { + // + // Jump to current - up to page size or top + // If already at the top, jump to bottom + // + if (0 === this.focusedItemIndex) { + return this.focusPrevious(); // will jump to bottom + } + + const index = Math.max(this.focusedItemIndex - this.dimens.height, 0); + + this.setFocusItemIndex(index); + + return FullMenuView.super_.prototype.focusPreviousPageItem.call(this); +}; + +FullMenuView.prototype.focusNextPageItem = function() { + // + // Jump to current + up to page size or bottom + // If already at the bottom, jump to top + // + if (this.items.length - 1 === this.focusedItemIndex) { + return this.focusNext(); // will jump to top + } + + const index = Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length - 1); + + this.setFocusItemIndex(index); + + return FullMenuView.super_.prototype.focusNextPageItem.call(this); +}; + +FullMenuView.prototype.focusFirst = function() { + this.setFocusItemIndex(0); + return FullMenuView.super_.prototype.focusFirst.call(this); +}; + +FullMenuView.prototype.focusLast = function() { + const index = this.items.length - 1; + + this.setFocusItemIndex(index); + + return FullMenuView.super_.prototype.focusLast.call(this); +}; + +FullMenuView.prototype.setFocusItems = function(items) { + FullMenuView.super_.prototype.setFocusItems.call(this, items); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.setItemSpacing = function(itemSpacing) { + FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing); + + this.positionCacheExpired = true; +}; + +FullMenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { + FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing); + + this.positionCacheExpired = true; +}; diff --git a/core/mci_view_factory.js b/core/mci_view_factory.js index 037121e5..d6c37865 100644 --- a/core/mci_view_factory.js +++ b/core/mci_view_factory.js @@ -8,6 +8,7 @@ const EditTextView = require('./edit_text_view.js').EditTextView; const ButtonView = require('./button_view.js').ButtonView; const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView; const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView; +const FullMenuView = require('./full_menu_view.js').FullMenuView; const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView; const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView; const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView; @@ -27,7 +28,7 @@ function MCIViewFactory(client) { } MCIViewFactory.UserViewCodes = [ - 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE', + 'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'FM', 'SM', 'TM', 'KE', // // XY is a special MCI code that allows finding positions @@ -164,6 +165,18 @@ MCIViewFactory.prototype.createFromMCI = function(mci) { view = new HorizontalMenuView(options); break; + // Full Menu + case 'FM' : + setOption(0, 'itemSpacing'); + setOption(1, 'itemHorizSpacing'); + setOption(2, 'justify'); + setOption(3, 'textStyle'); + + setFocusOption(0, 'focusTextStyle'); + + view = new FullMenuView(options); + break; + case 'SM' : setOption(0, 'textStyle'); setOption(1, 'justify'); diff --git a/core/menu_view.js b/core/menu_view.js index d9016153..e9c39900 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -38,7 +38,8 @@ function MenuView(options) { this.focusedItemIndex = options.focusedItemIndex || 0; this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization this.focusPrefix = options.focusPrefix || ''; @@ -253,9 +254,18 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) { this.positionCacheExpired = true; }; +MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { + itemSpacing = parseInt(itemHorizSpacing); + assert(_.isNumber(itemHorizSpacing)); + + this.itemHorizSpacing = itemHorizSpacing; + this.positionCacheExpired = true; +}; + MenuView.prototype.setPropertyValue = function(propName, value) { switch(propName) { case 'itemSpacing' : this.setItemSpacing(value); break; + case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break; case 'items' : this.setItems(value); break; case 'focusItems' : this.setFocusItems(value); break; case 'hotKeys' : this.setHotKeys(value); break; From 4fe55e1e1b9c4b777238b24091f90e45d8230dd9 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Sun, 16 Jan 2022 12:12:41 -0600 Subject: [PATCH 02/14] Added support for multiple pages --- core/full_menu_view.js | 224 +++++++++++++++++------- core/menu_view.js | 378 +++++++++++++++++++++-------------------- 2 files changed, 351 insertions(+), 251 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 87346b9d..3350d567 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -21,8 +21,15 @@ function FullMenuView(options) { MenuView.call(this, options); + + // Initialize paging + this.pages = []; + this.currentPage = 0; + this.initDefaultWidth(); + + const self = this; // we want page up/page down by default @@ -39,48 +46,111 @@ function FullMenuView(options) { this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); } - // Calculate number of items visible after adjusting height - this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1)); - // handle case where one can fit at the end - if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) { - this.itemsPerRow++; - } - - // Final check to make sure we don't try to display more than we have - if (this.itemsPerRow > this.items.length) { - this.itemsPerRow = this.items.length; - } - + 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(); - var col = self.position.col; - var row = self.position.row; - var spacer = self.getSpacer(); + this.pages = []; // reset + + // Calculate number of items visible per column + this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1)); + // handle case where one can fit at the end + if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) { + this.itemsPerRow++; + } + + // Final check to make sure we don't try to display more than we have + if (this.itemsPerRow > this.items.length) { + this.itemsPerRow = this.items.length; + } + + var col = this.position.col; + var row = this.position.row; + var spacer = this.getSpacer(); 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++; - self.items[i].row = row; - self.items[i].col = col; + this.items[i].row = row; + this.items[i].col = col; row += this.itemSpacing + 1; - // handle going to next column - if (itemInRow == this.itemsPerRow) { + // have to calculate the max length on the last entry + 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; - row = self.position.row; + // restart row for next column + row = this.position.row; var maxLength = 0; for (var j = 0; j < this.itemsPerRow; j++) { // TODO: handle complex items @@ -92,34 +162,37 @@ function FullMenuView(options) { // set length on each item in the column 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 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) { - const item = self.items[index]; + const item = this.items[index]; if (!item) { return; } const cached = this.getRenderCacheItem(index, item.focused); 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 sgr; - if (item.focused && self.hasFocusItems()) { - const focusItem = self.focusItems[index]; + if (item.focused && this.hasFocusItems()) { + const focusItem = this.focusItems[index]; text = focusItem ? focusItem.text : item.text; sgr = ''; } else if (this.complexItems) { 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 { - text = strUtil.stylizeString(item.text, item.focused ? self.focusTextStyle : self.textStyle); - sgr = (index === self.focusedItemIndex ? self.getFocusSGR() : self.getSGR()); + text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle); + sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); } - text = `${sgr}${strUtil.pad(text, this.dimens.width, this.fillChar, this.justify)}`; - self.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); + text = `${sgr}${strUtil.pad(text, this.fixedLength, this.fillChar, this.justify)}`; + this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); this.setRenderCacheItem(index, text, item.focused); }; } @@ -166,11 +239,6 @@ FullMenuView.prototype.redraw = function() { 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 // :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) { - 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.drawItem(i); } @@ -203,6 +271,12 @@ FullMenuView.prototype.setHeight = function(height) { this.autoAdjustHeight = false; }; +FullMenuView.prototype.setWidth = function(width) { + FullMenuView.super_.prototype.setWidth.call(this, width); + + this.positionCacheExpired = true; +}; + FullMenuView.prototype.setPosition = function(pos) { FullMenuView.super_.prototype.setPosition.call(this, pos); @@ -273,11 +347,16 @@ FullMenuView.prototype.removeItem = function(index) { FullMenuView.prototype.focusNext = function() { if (this.items.length - 1 === this.focusedItemIndex) { + this.clearPage(); this.focusedItemIndex = 0; - - } else { + this.currentPage = 0; + } + else { this.focusedItemIndex++; - + if (this.focusedItemIndex > this.pages[this.currentPage].end) { + this.clearPage(); + this.currentPage++; + } } this.redraw(); @@ -288,10 +367,14 @@ FullMenuView.prototype.focusNext = function() { FullMenuView.prototype.focusPrevious = function() { if (0 === this.focusedItemIndex) { this.focusedItemIndex = this.items.length - 1; - - - } else { + this.currentPage = this.pages.length - 1; + } + else { + this.clearPage(); this.focusedItemIndex--; + if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.currentPage--; + } } this.redraw(); @@ -303,8 +386,17 @@ FullMenuView.prototype.focusPreviousColumn = function() { this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; if (this.focusedItemIndex < 0) { + this.clearPage(); // add the negative index to the end of the list 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(); @@ -319,6 +411,12 @@ FullMenuView.prototype.focusNextColumn = function() { if (this.focusedItemIndex > this.items.length - 1) { // add the overflow to the beginning of the list 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(); diff --git a/core/menu_view.js b/core/menu_view.js index e9c39900..26b3467b 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -2,298 +2,300 @@ 'use strict'; // ENiGMA½ -const View = require('./view.js').View; -const miscUtil = require('./misc_util.js'); -const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +const View = require('./view.js').View; +const miscUtil = require('./misc_util.js'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; // deps -const util = require('util'); -const assert = require('assert'); -const _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); +const _ = require('lodash'); -exports.MenuView = MenuView; +exports.MenuView = MenuView; function MenuView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - View.call(this, options); + View.call(this, options); - this.disablePipe = options.disablePipe || false; + this.disablePipe = options.disablePipe || false; - const self = this; + const self = this; - if(options.items) { - this.setItems(options.items); - } else { - this.items = []; + if (options.items) { + this.setItems(options.items); + } else { + this.items = []; + } + + this.renderCache = {}; + + this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); + + this.setHotKeys(options.hotKeys); + + this.focusedItemIndex = options.focusedItemIndex || 0; + this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; + + this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; + + // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization + this.focusPrefix = options.focusPrefix || ''; + this.focusSuffix = options.focusSuffix || ''; + + this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); + this.justify = options.justify || 'none'; + + this.hasFocusItems = function() { + return !_.isUndefined(self.focusItems); + }; + + this.getHotKeyItemIndex = function(ch) { + if (ch && self.hotKeys) { + const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; + if (_.isNumber(keyIndex)) { + return keyIndex; + } } + return -1; + }; - this.renderCache = {}; - - this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); - - this.setHotKeys(options.hotKeys); - - this.focusedItemIndex = options.focusedItemIndex || 0; - this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; - this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; - - // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization - this.focusPrefix = options.focusPrefix || ''; - this.focusSuffix = options.focusSuffix || ''; - - this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - this.justify = options.justify || 'none'; - - this.hasFocusItems = function() { - return !_.isUndefined(self.focusItems); - }; - - this.getHotKeyItemIndex = function(ch) { - if(ch && self.hotKeys) { - const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; - if(_.isNumber(keyIndex)) { - return keyIndex; - } - } - return -1; - }; - - this.emitIndexUpdate = function() { - self.emit('index update', self.focusedItemIndex); - }; + this.emitIndexUpdate = function() { + self.emit('index update', self.focusedItemIndex); + }; } util.inherits(MenuView, View); MenuView.prototype.setItems = function(items) { - if(Array.isArray(items)) { - this.sorted = false; - this.renderCache = {}; + if (Array.isArray(items)) { + this.sorted = false; + this.renderCache = {}; - // - // Items can be an array of strings or an array of objects. - // - // In the case of objects, items are considered complex and - // may have one or more members that can later be formatted - // against. The default member is 'text'. The member 'data' - // may be overridden to provide a form value other than the - // item's index. - // - // Items can be formatted with 'itemFormat' and 'focusItemFormat' - // - let text; - let stringItem; - this.items = items.map(item => { - stringItem = _.isString(item); - if(stringItem) { - text = item; - } else { - text = item.text || ''; - this.complexItems = true; - } + // + // Items can be an array of strings or an array of objects. + // + // In the case of objects, items are considered complex and + // may have one or more members that can later be formatted + // against. The default member is 'text'. The member 'data' + // may be overridden to provide a form value other than the + // item's index. + // + // Items can be formatted with 'itemFormat' and 'focusItemFormat' + // + let text; + let stringItem; + this.items = items.map(item => { + stringItem = _.isString(item); + if (stringItem) { + text = item; + } else { + text = item.text || ''; + this.complexItems = true; + } - text = this.disablePipe ? text : pipeToAnsi(text, this.client); - return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others - }); + text = this.disablePipe ? text : pipeToAnsi(text, this.client); + return Object.assign({}, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others + }); - if(this.complexItems) { - this.itemFormat = this.itemFormat || '{text}'; - } - - this.invalidateRenderCache(); + if (this.complexItems) { + this.itemFormat = this.itemFormat || '{text}'; } + + this.invalidateRenderCache(); + } }; MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { - const item = this.renderCache[index]; - return item && item[focusItem ? 'focus' : 'standard']; + const item = this.renderCache[index]; + return item && item[focusItem ? 'focus' : 'standard']; }; MenuView.prototype.removeRenderCacheItem = function(index) { - delete this.renderCache[index]; + delete this.renderCache[index]; }; MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { - this.renderCache[index] = this.renderCache[index] || {}; - this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; + this.renderCache[index] = this.renderCache[index] || {}; + this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; }; MenuView.prototype.invalidateRenderCache = function() { - this.renderCache = {}; + this.renderCache = {}; }; MenuView.prototype.setSort = function(sort) { - if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { - return; + if (this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { + return; + } + + const key = true === sort ? 'text' : sort; + if ('text' !== sort && !this.complexItems) { + return; // need a valid sort key + } + + this.items.sort((a, b) => { + const a1 = a[key]; + const b1 = b[key]; + if (!a1) { + return -1; } - - const key = true === sort ? 'text' : sort; - if('text' !== sort && !this.complexItems) { - return; // need a valid sort key + if (!b1) { + return 1; } + return a1.localeCompare(b1, { sensitivity: false, numeric: true }); + }); - this.items.sort( (a, b) => { - const a1 = a[key]; - const b1 = b[key]; - if(!a1) { - return -1; - } - if(!b1) { - return 1; - } - return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); - }); - - this.sorted = true; + this.sorted = true; }; MenuView.prototype.removeItem = function(index) { - this.sorted = false; - this.items.splice(index, 1); + this.sorted = false; + this.items.splice(index, 1); - if(this.focusItems) { - this.focusItems.splice(index, 1); - } + if (this.focusItems) { + this.focusItems.splice(index, 1); + } - if(this.focusedItemIndex >= index) { - this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); - } + if (this.focusedItemIndex >= index) { + this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); + } - this.removeRenderCacheItem(index); + this.removeRenderCacheItem(index); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; MenuView.prototype.getCount = function() { - return this.items.length; + return this.items.length; }; MenuView.prototype.getItems = function() { - if(this.complexItems) { - return this.items; - } + if (this.complexItems) { + return this.items; + } - return this.items.map( item => { - return item.text; - }); + return this.items.map(item => { + return item.text; + }); }; MenuView.prototype.getItem = function(index) { - if(this.complexItems) { - return this.items[index]; - } + if (this.complexItems) { + return this.items[index]; + } - return this.items[index].text; + return this.items[index].text; }; MenuView.prototype.focusNext = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPrevious = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusNextPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPreviousPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusFirst = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusLast = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.setFocusItemIndex = function(index) { - this.focusedItemIndex = index; + this.focusedItemIndex = index; }; MenuView.prototype.onKeyPress = function(ch, key) { - const itemIndex = this.getHotKeyItemIndex(ch); - if(itemIndex >= 0) { - this.setFocusItemIndex(itemIndex); + const itemIndex = this.getHotKeyItemIndex(ch); + if (itemIndex >= 0) { + this.setFocusItemIndex(itemIndex); - if(true === this.hotKeySubmit) { - this.emit('action', 'accept'); - } + if (true === this.hotKeySubmit) { + this.emit('action', 'accept'); } + } - MenuView.super_.prototype.onKeyPress.call(this, ch, key); + MenuView.super_.prototype.onKeyPress.call(this, ch, key); }; MenuView.prototype.setFocusItems = function(items) { - const self = this; + const self = this; - if(items) { - this.focusItems = []; - items.forEach( itemText => { - this.focusItems.push( - { - text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) - } - ); - }); - } + if (items) { + this.focusItems = []; + items.forEach(itemText => { + this.focusItems.push( + { + text: self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) + } + ); + }); + } }; MenuView.prototype.setItemSpacing = function(itemSpacing) { - itemSpacing = parseInt(itemSpacing); - assert(_.isNumber(itemSpacing)); + itemSpacing = parseInt(itemSpacing); + assert(_.isNumber(itemSpacing)); - this.itemSpacing = itemSpacing; - this.positionCacheExpired = true; + this.itemSpacing = itemSpacing; + this.positionCacheExpired = true; }; MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { - itemSpacing = parseInt(itemHorizSpacing); - assert(_.isNumber(itemHorizSpacing)); + itemSpacing = parseInt(itemHorizSpacing); + assert(_.isNumber(itemHorizSpacing)); - this.itemHorizSpacing = itemHorizSpacing; - this.positionCacheExpired = true; + this.itemHorizSpacing = itemHorizSpacing; + this.positionCacheExpired = true; }; MenuView.prototype.setPropertyValue = function(propName, value) { - switch(propName) { - case 'itemSpacing' : this.setItemSpacing(value); break; - case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break; - case 'items' : this.setItems(value); break; - case 'focusItems' : this.setFocusItems(value); break; - case 'hotKeys' : this.setHotKeys(value); break; - case 'hotKeySubmit' : this.hotKeySubmit = value; break; - case 'justify' : this.justify = value; break; - case 'focusItemIndex' : this.focusedItemIndex = value; break; + switch (propName) { + case 'itemSpacing': this.setItemSpacing(value); break; + case 'itemHorizSpacing': this.setItemHorizSpacing(value); break; + case 'items': this.setItems(value); break; + case 'focusItems': this.setFocusItems(value); break; + case 'hotKeys': this.setHotKeys(value); break; + case 'hotKeySubmit': this.hotKeySubmit = value; break; + case 'justify': this.justify = value; break; + case 'focusItemIndex': this.focusedItemIndex = value; break; - case 'itemFormat' : - case 'focusItemFormat' : - this[propName] = value; - break; + case 'itemFormat': + case 'focusItemFormat': + this[propName] = value; + // if there is a cache currently, invalidate it + this.invalidateRenderCache(); + 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) { - if(_.isObject(hotKeys)) { - if(this.caseInsensitiveHotKeys) { - this.hotKeys = {}; - for(var key in hotKeys) { - this.hotKeys[key.toLowerCase()] = hotKeys[key]; - } - } else { - this.hotKeys = hotKeys; - } + if (_.isObject(hotKeys)) { + if (this.caseInsensitiveHotKeys) { + this.hotKeys = {}; + for (var key in hotKeys) { + this.hotKeys[key.toLowerCase()] = hotKeys[key]; + } + } else { + this.hotKeys = hotKeys; } + } }; From d77de9c388305cc79a715c22defa2db9559013bb Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Wed, 19 Jan 2022 13:07:02 -0600 Subject: [PATCH 03/14] Cleaned up issues with length --- core/full_menu_view.js | 49 +++++++++++++++++++++++++++++++----------- core/menu_view.js | 10 +++++++++ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 3350d567..7f9914a9 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -59,7 +59,7 @@ function FullMenuView(options) { this.clearPage = function() { - for (var i = 0; i < this.itemsPerRow; i++) { + for (var i = 0; i < this.dimens.height; 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}`); } @@ -88,11 +88,7 @@ function FullMenuView(options) { var spacer = this.getSpacer(); var itemInRow = 0; - - this.viewWindow = { - start: this.focusedItemIndex, - end: this.items.length - 1, // this may be adjusted later - }; + var itemInCol = 0; var pageStart = 0; @@ -100,6 +96,7 @@ function FullMenuView(options) { itemInRow++; this.items[i].row = row; this.items[i].col = col; + this.items[i].itemInRow = itemInRow; row += this.itemSpacing + 1; @@ -126,7 +123,8 @@ function FullMenuView(options) { // Check if we have room for this column - if (col + maxLength + spacer.length + 1 > this.position.col + this.dimens.width) { + // skip for column 0, we need at least one + if (itemInCol != 0 && (col + maxLength + spacer.length + 1 > this.position.col + this.dimens.width)) { // save previous page this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); @@ -166,7 +164,8 @@ function FullMenuView(options) { } // Check if we have room for this column in the current page - if (col + maxLength > this.position.col + this.dimens.width) { + // skip for first column, we need at least one + if (itemInCol != 0 && (col + maxLength > this.position.col + this.dimens.width)) { // save previous page this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); @@ -183,10 +182,14 @@ function FullMenuView(options) { } + + + } // increment the column col += maxLength + spacer.length + 1; + itemInCol++; } @@ -225,6 +228,11 @@ function FullMenuView(options) { sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR()); } + let renderLength = strUtil.renderStringLength(text); + if (this.hasTextOverflow() && (item.col + renderLength) > this.dimens.width) { + text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow; + } + text = `${sgr}${strUtil.pad(text, this.fixedLength, this.fillChar, this.justify)}`; this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); this.setRenderCacheItem(index, text, item.focused); @@ -277,6 +285,13 @@ FullMenuView.prototype.setWidth = function(width) { this.positionCacheExpired = true; }; +FullMenuView.prototype.setTextOverflow = function(overflow) { + FullMenuView.super_.prototype.setTextOverflow.call(this, overflow); + + this.positionCacheExpired = true; + +} + FullMenuView.prototype.setPosition = function(pos) { FullMenuView.super_.prototype.setPosition.call(this, pos); @@ -366,13 +381,14 @@ FullMenuView.prototype.focusNext = function() { FullMenuView.prototype.focusPrevious = function() { if (0 === this.focusedItemIndex) { + this.clearPage(); this.focusedItemIndex = this.items.length - 1; this.currentPage = this.pages.length - 1; } else { - this.clearPage(); this.focusedItemIndex--; if (this.focusedItemIndex < this.pages[this.currentPage].start) { + this.clearPage(); this.currentPage--; } } @@ -384,11 +400,20 @@ FullMenuView.prototype.focusPrevious = function() { FullMenuView.prototype.focusPreviousColumn = function() { + var currentRow = this.items[this.focusedItemIndex].itemInRow; this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; if (this.focusedItemIndex < 0) { this.clearPage(); + var lastItemRow = this.items[this.items.length - 1].itemInRow; + if (lastItemRow > currentRow) { + this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1; + } + else { + // can't go to same column, so go to last item + this.focusedItemIndex = this.items.length - 1; + } // 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; } @@ -407,10 +432,10 @@ FullMenuView.prototype.focusPreviousColumn = function() { FullMenuView.prototype.focusNextColumn = function() { + var currentRow = this.items[this.focusedItemIndex].itemInRow; this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; if (this.focusedItemIndex > this.items.length - 1) { - // add the overflow to the beginning of the list - this.focusedItemIndex = this.focusedItemIndex - this.items.length; + this.focusedItemIndex = currentRow - 1; this.currentPage = 0; this.clearPage(); } diff --git a/core/menu_view.js b/core/menu_view.js index 26b3467b..e9c878df 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -69,6 +69,15 @@ function MenuView(options) { util.inherits(MenuView, View); +MenuView.prototype.setTextOverflow = function(overflow) { + this.textOverflow = overflow; + this.invalidateRenderCache(); +} + +MenuView.prototype.hasTextOverflow = function() { + return this.textOverflow !== undefined; +} + MenuView.prototype.setItems = function(items) { if (Array.isArray(items)) { this.sorted = false; @@ -269,6 +278,7 @@ MenuView.prototype.setPropertyValue = function(propName, value) { case 'items': this.setItems(value); break; case 'focusItems': this.setFocusItems(value); break; case 'hotKeys': this.setHotKeys(value); break; + case 'textOverflow': this.setTextOverflow(value); break; case 'hotKeySubmit': this.hotKeySubmit = value; break; case 'justify': this.justify = value; break; case 'focusItemIndex': this.focusedItemIndex = value; break; From c82497b75ea5f64d495da84bfcab1443c55f25f3 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Wed, 19 Jan 2022 14:36:43 -0600 Subject: [PATCH 04/14] Fixed justification --- core/full_menu_view.js | 20 ++++++++++++-------- core/menu_view.js | 9 +++++++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 7f9914a9..2869c812 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -124,9 +124,9 @@ function FullMenuView(options) { // Check if we have room for this column // skip for column 0, we need at least one - if (itemInCol != 0 && (col + maxLength + spacer.length + 1 > this.position.col + this.dimens.width)) { + if (itemInCol != 0 && (col + maxLength > this.dimens.width)) { // save previous page - this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); + this.pages.push({ start: pageStart, end: i - itemInRow }); // fix the last column processed for (var j = 0; j < this.itemsPerRow; j++) { @@ -165,7 +165,7 @@ function FullMenuView(options) { // Check if we have room for this column in the current page // skip for first column, we need at least one - if (itemInCol != 0 && (col + maxLength > this.position.col + this.dimens.width)) { + if (itemInCol != 0 && (col + maxLength > this.dimens.width)) { // save previous page this.pages.push({ start: pageStart, end: i - this.itemsPerRow }); @@ -181,10 +181,6 @@ function FullMenuView(options) { this.items[i - j].col = col; } - - - - } // increment the column @@ -233,7 +229,9 @@ function FullMenuView(options) { text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow; } - text = `${sgr}${strUtil.pad(text, this.fixedLength, this.fillChar, this.justify)}`; + let padLength = Math.min(item.fixedLength + 1, this.dimens.width); + + text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}`; this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); this.setRenderCacheItem(index, text, item.focused); }; @@ -508,6 +506,12 @@ FullMenuView.prototype.setItemSpacing = function(itemSpacing) { this.positionCacheExpired = true; }; +FullMenuView.prototype.setJustify = function(justify) { + FullMenuView.super_.prototype.setJustify.call(this, justify); + this.positionCacheExpired = true; +}; + + FullMenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing); diff --git a/core/menu_view.js b/core/menu_view.js index e9c878df..ea0b0b4d 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -46,7 +46,6 @@ function MenuView(options) { this.focusSuffix = options.focusSuffix || ''; this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - this.justify = options.justify || 'none'; this.hasFocusItems = function() { return !_.isUndefined(self.focusItems); @@ -280,7 +279,7 @@ MenuView.prototype.setPropertyValue = function(propName, value) { case 'hotKeys': this.setHotKeys(value); break; case 'textOverflow': this.setTextOverflow(value); break; case 'hotKeySubmit': this.hotKeySubmit = value; break; - case 'justify': this.justify = value; break; + case 'justify': this.setJustify(value); break; case 'focusItemIndex': this.focusedItemIndex = value; break; case 'itemFormat': @@ -296,6 +295,12 @@ MenuView.prototype.setPropertyValue = function(propName, value) { MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; +MenuView.prototype.setJustify = function(justify) { + this.justify = justify; + this.invalidateRenderCache(); + this.positionCacheExpired = true; +}; + MenuView.prototype.setHotKeys = function(hotKeys) { if (_.isObject(hotKeys)) { if (this.caseInsensitiveHotKeys) { From c4ed09a8bb6edf7126b70c6a1cb214c847e1269c Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Wed, 19 Jan 2022 15:11:33 -0600 Subject: [PATCH 05/14] Fixed fill character --- core/full_menu_view.js | 17 ++++++++++++++--- core/menu_view.js | 8 +++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 2869c812..30b384b4 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -59,8 +59,9 @@ function FullMenuView(options) { this.clearPage = function() { + console.log("Clear page called with f: ~%s~, h: %d, w: %d, r: %d, c: %d", this.fillChar, this.dimens.height, this.dimens.width, this.position.row, this.position.col); for (var i = 0; i < this.dimens.height; i++) { - let text = `${strUtil.pad(' ', this.dimens.width, this.fillChar, 'left')}`; + let text = `${strUtil.pad(this.fillChar, this.dimens.width, this.fillChar, 'left')}`; self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${text}`); } } @@ -184,7 +185,7 @@ function FullMenuView(options) { } // increment the column - col += maxLength + spacer.length + 1; + col += maxLength + spacer.length; itemInCol++; } @@ -231,8 +232,9 @@ function FullMenuView(options) { let padLength = Math.min(item.fixedLength + 1, this.dimens.width); - text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}`; + text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`; this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); + // this.client.term.write(`${ansi.goto(item.row, item.col)}${text}${this.getSpacer()}`); this.setRenderCacheItem(index, text, item.focused); }; } @@ -275,6 +277,7 @@ FullMenuView.prototype.setHeight = function(height) { this.positionCacheExpired = true; this.autoAdjustHeight = false; + this.clearPage(); }; FullMenuView.prototype.setWidth = function(width) { @@ -296,6 +299,13 @@ FullMenuView.prototype.setPosition = function(pos) { this.positionCacheExpired = true; }; +FullMenuView.prototype.setFillChar = function(fillChar) { + FullMenuView.super_.prototype.setFillChar.call(this, fillChar); + + this.clearPage(); + this.redraw(); +}; + FullMenuView.prototype.setFocus = function(focused) { FullMenuView.super_.prototype.setFocus.call(this, focused); @@ -354,6 +364,7 @@ FullMenuView.prototype.removeItem = function(index) { } FullMenuView.super_.prototype.removeItem.call(this, index); + this.positionCacheExpired = true; }; // :TODO: Apply draw optimizaitons when only two items need drawn vs entire view! diff --git a/core/menu_view.js b/core/menu_view.js index ea0b0b4d..f2bca3cf 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -263,7 +263,7 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) { }; MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { - itemSpacing = parseInt(itemHorizSpacing); + itemHorizSpacing = parseInt(itemHorizSpacing); assert(_.isNumber(itemHorizSpacing)); this.itemHorizSpacing = itemHorizSpacing; @@ -280,6 +280,7 @@ MenuView.prototype.setPropertyValue = function(propName, value) { case 'textOverflow': this.setTextOverflow(value); break; case 'hotKeySubmit': this.hotKeySubmit = value; break; case 'justify': this.setJustify(value); break; + case 'fillChar': this.setFillChar(value); break; case 'focusItemIndex': this.focusedItemIndex = value; break; case 'itemFormat': @@ -295,6 +296,11 @@ MenuView.prototype.setPropertyValue = function(propName, value) { MenuView.super_.prototype.setPropertyValue.call(this, propName, value); }; +MenuView.prototype.setFillChar = function(fillChar) { + this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1); + this.invalidateRenderCache(); +} + MenuView.prototype.setJustify = function(justify) { this.justify = justify; this.invalidateRenderCache(); From 358a778b0a3f0d70a38cc55d094856331ec2fa6b Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 21 Jan 2022 13:53:32 -0600 Subject: [PATCH 06/14] Fixed uneccessary call to setFocus --- core/full_menu_view.js | 63 +++++++++++++++++++----------------------- core/view.js | 14 +++++++--- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 30b384b4..e3c6c8de 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -59,15 +59,26 @@ function FullMenuView(options) { this.clearPage = function() { - console.log("Clear page called with f: ~%s~, h: %d, w: %d, r: %d, c: %d", this.fillChar, this.dimens.height, this.dimens.width, this.position.row, this.position.col); + var width = this.dimens.width; + if (this.oldDimens) { + if (this.oldDimens.width > width) { + width = this.oldDimens.width; + } + delete this.oldDimens; + } + for (var i = 0; i < this.dimens.height; i++) { - let text = `${strUtil.pad(this.fillChar, this.dimens.width, this.fillChar, 'left')}`; - self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${text}`); + let text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; + self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`); } } this.cachePositions = function() { if (this.positionCacheExpired) { + // first, clear the page + this.clearPage(); + + this.autoAdjustHeightIfEnabled(); this.pages = []; // reset @@ -234,7 +245,6 @@ function FullMenuView(options) { text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`; this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`); - // this.client.term.write(`${ansi.goto(item.row, item.col)}${text}${this.getSpacer()}`); this.setRenderCacheItem(index, text, item.focused); }; } @@ -242,28 +252,20 @@ function FullMenuView(options) { util.inherits(FullMenuView, MenuView); +//TODO: FIXME +function debugLine(message) { + let e = new Error(); + let frame = e.stack.split("\n")[3]; // change to 3 for grandparent func + let lineNumber = frame.split(":").reverse()[1]; + let functionName = frame.split(" ")[5]; + return functionName + ":" + lineNumber + " " + message; +} + FullMenuView.prototype.redraw = function() { FullMenuView.super_.prototype.redraw.call(this); this.cachePositions(); - - // erase old items - // :TODO: optimize this: only needed if a item is removed or new max width < old. - if (this.oldDimens) { - const blank = new Array(Math.max(this.oldDimens.width, this.dimens.width)).join(' '); - let seq = ansi.goto(this.position.row, this.position.col) + this.getSGR() + blank; - let row = this.position.row + 1; - const endRow = (row + this.oldDimens.height) - 2; - - while (row <= endRow) { - seq += ansi.goto(row, this.position.col) + blank; - row += 1; - } - this.client.term.write(seq); - delete this.oldDimens; - } - if (this.items.length) { for (let i = this.pages[this.currentPage].start; i <= this.pages[this.currentPage].end; ++i) { this.items[i].focused = this.focusedItemIndex === i; @@ -273,14 +275,17 @@ FullMenuView.prototype.redraw = function() { }; FullMenuView.prototype.setHeight = function(height) { + this.oldDimens = Object.assign({}, this.dimens); + FullMenuView.super_.prototype.setHeight.call(this, height); this.positionCacheExpired = true; this.autoAdjustHeight = false; - this.clearPage(); }; FullMenuView.prototype.setWidth = function(width) { + this.oldDimens = Object.assign({}, this.dimens); + FullMenuView.super_.prototype.setWidth.call(this, width); this.positionCacheExpired = true; @@ -299,23 +304,16 @@ FullMenuView.prototype.setPosition = function(pos) { this.positionCacheExpired = true; }; -FullMenuView.prototype.setFillChar = function(fillChar) { - FullMenuView.super_.prototype.setFillChar.call(this, fillChar); - - this.clearPage(); - this.redraw(); -}; - FullMenuView.prototype.setFocus = function(focused) { FullMenuView.super_.prototype.setFocus.call(this, focused); + this.positionCacheExpired = true; + this.autoAdjustHeight = false; this.redraw(); }; FullMenuView.prototype.setFocusItemIndex = function(index) { FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex - - this.redraw(); }; FullMenuView.prototype.onKeyPress = function(ch, key) { @@ -421,8 +419,6 @@ FullMenuView.prototype.focusPreviousColumn = function() { // can't go to same column, so go to last item this.focusedItemIndex = this.items.length - 1; } - // add the negative index to the end of the list - // this.focusedItemIndex = this.items.length + this.focusedItemIndex; // set to last page this.currentPage = this.pages.length - 1; } @@ -459,7 +455,6 @@ FullMenuView.prototype.focusNextColumn = function() { FullMenuView.super_.prototype.focusNext.call(this); }; - FullMenuView.prototype.focusPreviousPageItem = function() { // // Jump to current - up to page size or top diff --git a/core/view.js b/core/view.js index fdf78916..c31ccca1 100644 --- a/core/view.js +++ b/core/view.js @@ -186,7 +186,7 @@ View.prototype.setPropertyValue = function(propName, value) { case 'height' : this.setHeight(value); break; case 'width' : this.setWidth(value); break; - case 'focus' : this.setFocus(value); break; + case 'focus' : this.setFocusProperty(value); break; case 'text' : if('setText' in this) { @@ -252,10 +252,16 @@ View.prototype.redraw = function() { this.client.term.write(ansi.goto(this.position.row, this.position.col)); }; -View.prototype.setFocus = function(focused) { - enigAssert(this.acceptsFocus, 'View does not accept focus'); - +View.prototype.setFocusProperty = function(focused) { + // Either this should accept focus, or the focus should be false + enigAssert(this.acceptsFocus || !focused, 'View does not accept focus'); this.hasFocus = focused; +}; + +View.prototype.setFocus = function(focused) { + // Call separate method to differentiate between a value set as a + // property vs focus programmatically called. + this.setFocusProperty(focused); this.restoreCursor(); }; From 22808cf2e2f33dc0679eee7222d7131ec9b40b3a Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 21 Jan 2022 14:27:19 -0600 Subject: [PATCH 07/14] Fixed page up, page down, home, and end --- core/full_menu_view.js | 59 ++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index e3c6c8de..bbdda4b6 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -251,16 +251,6 @@ function FullMenuView(options) { util.inherits(FullMenuView, MenuView); - -//TODO: FIXME -function debugLine(message) { - let e = new Error(); - let frame = e.stack.split("\n")[3]; // change to 3 for grandparent func - let lineNumber = frame.split(":").reverse()[1]; - let functionName = frame.split(" ")[5]; - return functionName + ":" + lineNumber + " " + message; -} - FullMenuView.prototype.redraw = function() { FullMenuView.super_.prototype.redraw.call(this); @@ -365,8 +355,6 @@ FullMenuView.prototype.removeItem = function(index) { this.positionCacheExpired = true; }; -// :TODO: Apply draw optimizaitons when only two items need drawn vs entire view! - FullMenuView.prototype.focusNext = function() { if (this.items.length - 1 === this.focusedItemIndex) { this.clearPage(); @@ -456,47 +444,56 @@ FullMenuView.prototype.focusNextColumn = function() { }; FullMenuView.prototype.focusPreviousPageItem = function() { - // - // Jump to current - up to page size or top - // If already at the top, jump to bottom - // - if (0 === this.focusedItemIndex) { - return this.focusPrevious(); // will jump to bottom + + // handle first page + if (this.currentPage == 0) { + // Do nothing, page up shouldn't go down on last page + return; } - const index = Math.max(this.focusedItemIndex - this.dimens.height, 0); + this.currentPage--; + this.focusedItemIndex = this.pages[this.currentPage].start; + this.clearPage(); - this.setFocusItemIndex(index); + this.redraw(); return FullMenuView.super_.prototype.focusPreviousPageItem.call(this); }; FullMenuView.prototype.focusNextPageItem = function() { - // - // Jump to current + up to page size or bottom - // If already at the bottom, jump to top - // - if (this.items.length - 1 === this.focusedItemIndex) { - return this.focusNext(); // will jump to top + + // handle last page + if (this.currentPage == this.pages.length - 1) { + // Do nothing, page up shouldn't go down on last page + return; } - const index = Math.min(this.focusedItemIndex + this.maxVisibleItems, this.items.length - 1); + this.currentPage++; + this.focusedItemIndex = this.pages[this.currentPage].start; + this.clearPage(); - this.setFocusItemIndex(index); + this.redraw(); return FullMenuView.super_.prototype.focusNextPageItem.call(this); }; FullMenuView.prototype.focusFirst = function() { - this.setFocusItemIndex(0); + + this.currentPage = 0; + this.focusedItemIndex = 0; + this.clearPage(); + + this.redraw(); return FullMenuView.super_.prototype.focusFirst.call(this); }; FullMenuView.prototype.focusLast = function() { - const index = this.items.length - 1; - this.setFocusItemIndex(index); + this.currentPage = this.pages.length - 1; + this.focusedItemIndex = this.pages[this.currentPage].end; + this.clearPage(); + this.redraw(); return FullMenuView.super_.prototype.focusLast.call(this); }; From 9ce599034353cf0f6a2e1dd90e9340f640de51c0 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 21 Jan 2022 16:09:41 -0600 Subject: [PATCH 08/14] Documentation WIP --- docs/_includes/nav.md | 2 + docs/art/mci.md | 3 +- docs/art/views/full_menu_view.md | 231 +++++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 docs/art/views/full_menu_view.md diff --git a/docs/_includes/nav.md b/docs/_includes/nav.md index 42cc7bff..cd635928 100644 --- a/docs/_includes/nav.md +++ b/docs/_includes/nav.md @@ -49,6 +49,8 @@ - [General]({{ site.baseurl }}{% link art/general.md %}) - [Themes]({{ site.baseurl }}{% link art/themes.md %}) - [MCI Codes]({{ site.baseurl }}{% link art/mci.md %}) + - Views + - [Full Menu]({{ site.baseurl }}{% link art/views/full_menu_view.md %}) - Servers - Login Servers diff --git a/docs/art/mci.md b/docs/art/mci.md index 174643a4..79135554 100644 --- a/docs/art/mci.md +++ b/docs/art/mci.md @@ -123,6 +123,7 @@ a Vertical Menu (`%VM`): Old-school BBSers may recognize this as a lightbar menu | `BT` | Button | A button | ...it's a button | | `VM` | Vertical Menu | A vertical menu | AKA a vertical lightbar; Useful for lists | | `HM` | Horizontal Menu | A horizontal menu | AKA a horizontal lightbar | +| `FM` | Full Menu | A menu that can go both vertical and horizontal. See [Full Menu](views/full_menu_view.md) | | `SM` | Spinner Menu | A spinner input control | Select *one* from multiple options | | `TM` | Toggle Menu | A toggle menu | Commonly used for Yes/No style input | | `KE` | Key Entry | A *single* key input control | Think hotkeys | @@ -233,4 +234,4 @@ Suppose a format object contains the following elements: `userName` and `affils` ![Example](../assets/images/text-format-example1.png "Text Format") -:bulb: Remember that a Python [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language) style syntax is available for widths, alignment, number prevision, etc. as well. A number can be made to be more human readable for example: `{byteSize:,}` may yield "1,123,456". \ No newline at end of file +:bulb: Remember that a Python [string format mini language](https://docs.python.org/3/library/string.html#format-specification-mini-language) style syntax is available for widths, alignment, number prevision, etc. as well. A number can be made to be more human readable for example: `{byteSize:,}` may yield "1,123,456". diff --git a/docs/art/views/full_menu_view.md b/docs/art/views/full_menu_view.md new file mode 100644 index 00000000..27f782fd --- /dev/null +++ b/docs/art/views/full_menu_view.md @@ -0,0 +1,231 @@ +--- +layout: page +title: Full Menu View +--- +## Full Menu View +A full menu view supports displaying a list of times on a screen in a very configurable manner. A full menu view supports either a single row or column of values, similar to Horizontal Menu (HM) and Vertical Menu (VM), or in multiple columns. + +## General Information + +Items can be selected on a menu via the cursor keys, Page Up, Page Down, Home, and End, or by selecting them via a `hotKey` - see ***Hot Keys*** below. + +:information_source: A full menu view is defined with a percent (%) and the characters FM, followed by the view number. For example: `%FM1` + +:information_source: See [Art](../art.md) for general information on how to use views and common configuration properties available for them. + +### Properties + +| Property | Description | +|-------------|--------------| +| `textStyle` | Sets the standard (non-focus) text style. See **Text Styles** in [Art](../art.md) | +| `focusTextStyle` | Sets focus text style. See **Text Styles** in [Art](../art.md)| +| `itemSpacing` | Used to separate items vertically in the menu | +| `itemHorizSpacing` | Used to separate items horizontally in the menu | +| `height` | Sets the height of views to display multiple items vertically (default 1) | +| `width` | Sets the width of a view to display one or more columns horizontally (default 15)| +| `focus` | If set to `true`, establishes initial focus | +| `submit` | If set to `true` any `accept` action upon this view will submit the encompassing **form** | +| `hotKeys` | Sets hot keys to activate specific items. See **Hot Keys** below | +| `hotKeySubmit` | Set to submit a form on hotkey selection | +| `argName` | Sets the argument name for this selection in the form | +| `justify` | Sets the justification of each item in the list. Options: left (default), right, center | +| `itemFormat` | Sets the format for a list entry. See **Entry Formatting** in [Art](../art.md) | +| `fillChar` | Specifies a character to fill extra space in the menu with. Defaults to an empty space | +| `textOverflow` | If a single column cannot be displayed due to `width`, set overflow characters. See **Text Overflow** below | +| `items` | List of items to show in the menu. See **Items** below. +| `focusItemFormat` | Sets the format for a focused list entry. See **Entry Formatting** in [Art](../art.md) | + + +### Hot Keys + +A set of `hotKeys` are used to allow the user to press a character on the keyboard to select that item, and optionally submit the form. + +Example: + +``` +hotKeys: { A: 0, B: 1, C: 2, D: 3 } +hotKeySubmit: true +``` +This would select and submit the first item if `A` is typed, second if `B`, etc. + +### Items + +A full menu, similar to other menus, take a list of items to display in the menu. For example: + + +``` +items: [ + { + text: First Item + data: first + } + { + text: Second Item + data: second + } +] +``` + +### Text Overflow + +The `textOverflow` option is used to specify what happens when a text string is too long to fit in the `width` defined. Note, because columns are automatically calculated, this can only occur when the text is too long to fit the `width` using a single column. + +:information_source: If `textOverflow` is not specified at all, a menu can become wider than the `width` if needed to display a single column. + +:information_source: Setting `textOverflow` to an empty string `textOverflow: ""` will cause the item to be truncated if necessary without any characters displayed + +:information_source: Otherwise, setting `textOverflow` to one or more characters will truncate the value if necessary and display those characters at the end. i.e. `textOverflow: ...` + +## Examples + +### A simple vertical menu - similar to VM + +#### Configuration fragment + +``` +FM1: { + submit: true + argName: navSelect + width: 1 + items: [ + { + text: login + data: login + } + { + text: apply + data: new user + } + { + text: about + data: about + } + { + text: log off + data: logoff + } + ] +} + +``` + +#### Example + +![Example](../assets/images/text-format-example1.png "Text Format") + +### A simple horizontal menu - similar to HM + +#### Configuration fragment + +``` +FM2: { + focus: true + height: 1 + width: 60 // set as desired + submit: true + argName: navSelect + items: [ + "prev", "next", "details", "toggle queue", "rate", "help", "quit" + ] +} +``` + +#### Example + +![Example](../assets/images/text-format-example1.png "Text Format") + +### A multi-column navigation menu with hotkeys + + +#### Configuration fragment + +``` +FM1: { + focus: true + height: 6 + width: 60 + submit: true + argName: navSelect + hotKeys: { M: 0, E: 1, D: 2 ,F: 3,!: 4, A: 5, C: 6, Y: 7, S: 8, R: 9, O: 10, L:11, U:12, W: 13, B:14, G:15, T: 16, Q:17 } + hotKeySubmit: true + items: [ + { + text: M) message area + data: message + } + { + text: E) private email + data: email + } + { + text: D) doors + data: doors + } + { + text: F) file base + data: files + } + { + text: !) global newscan + data: newscan + } + { + text: A) achievements + data: achievements + } + { + text: C) configuration + data: config + } + { + text: Y) user stats + data: userstats + } + { + text: S) system stats + data: systemstats + } + { + text: R) rumorz + data: rumorz + } + { + text: O) onelinerz + data: onelinerz + } + { + text: L) last callers + data: callers + } + { + text: U) user list + data: userlist + } + { + text: W) whos online + data: who + } + { + text: B) bbs list + data: bbslist + } + { + text: G) node-to-node messages + data: nodemessages + } + { + text: T) multi relay chat + data: mrc + } + { + text: Q) quit + data: quit + } + ] +} +``` + +#### Example + +![Example](../assets/images/text-format-example1.png "Text Format") + From e359891d046b22751c06c2f77a4e5e38e906984b Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Fri, 21 Jan 2022 16:56:50 -0600 Subject: [PATCH 09/14] Example gif --- docs/art/views/full_menu_view.md | 2 +- docs/assets/images/full_menu_view_example3.gif | Bin 0 -> 28011 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/assets/images/full_menu_view_example3.gif diff --git a/docs/art/views/full_menu_view.md b/docs/art/views/full_menu_view.md index 27f782fd..bfac75f7 100644 --- a/docs/art/views/full_menu_view.md +++ b/docs/art/views/full_menu_view.md @@ -227,5 +227,5 @@ FM1: { #### Example -![Example](../assets/images/text-format-example1.png "Text Format") +![Example](../../assets/images/full_menu_view_example3.gif "Multi column menu") diff --git a/docs/assets/images/full_menu_view_example3.gif b/docs/assets/images/full_menu_view_example3.gif new file mode 100644 index 0000000000000000000000000000000000000000..b57375fade714995f74fea72d9902dd31dccf7d0 GIT binary patch literal 28011 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 BMRNcE literal 0 HcmV?d00001 From 2c2951866b62afdab73155f44ef82f8bfe145b17 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Sun, 23 Jan 2022 10:17:23 -0600 Subject: [PATCH 10/14] Additional examples --- docs/art/views/full_menu_view.md | 30 +++++++++--------- .../assets/images/full_menu_view_example1.gif | Bin 0 -> 7198 bytes .../assets/images/full_menu_view_example2.gif | Bin 0 -> 7832 bytes 3 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 docs/assets/images/full_menu_view_example1.gif create mode 100644 docs/assets/images/full_menu_view_example2.gif diff --git a/docs/art/views/full_menu_view.md b/docs/art/views/full_menu_view.md index bfac75f7..03db2cbb 100644 --- a/docs/art/views/full_menu_view.md +++ b/docs/art/views/full_menu_view.md @@ -80,7 +80,10 @@ The `textOverflow` option is used to specify what happens when a text string is ### A simple vertical menu - similar to VM -#### Configuration fragment +![Example](../../assets/images/full_menu_view_example1.gif "Vertical menu") + +
+Configuration fragment (expand to view) ``` FM1: { @@ -108,14 +111,14 @@ FM1: { } ``` - -#### Example - -![Example](../assets/images/text-format-example1.png "Text Format") +
### A simple horizontal menu - similar to HM -#### Configuration fragment +![Example](../../assets/images/full_menu_view_example2.gif "Horizontal menu") + +
+Configuration fragment (expand to view) ``` FM2: { @@ -129,15 +132,15 @@ FM2: { ] } ``` - -#### Example - -![Example](../assets/images/text-format-example1.png "Text Format") +
### A multi-column navigation menu with hotkeys -#### Configuration fragment +![Example](../../assets/images/full_menu_view_example3.gif "Multi column menu") + +
+Configuration fragment (expand to view) ``` FM1: { @@ -224,8 +227,5 @@ FM1: { ] } ``` - -#### Example - -![Example](../../assets/images/full_menu_view_example3.gif "Multi column menu") +
diff --git a/docs/assets/images/full_menu_view_example1.gif b/docs/assets/images/full_menu_view_example1.gif new file mode 100644 index 0000000000000000000000000000000000000000..7366d8130dbada70680437908d58f1ad98b2c0aa GIT binary patch literal 7198 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 qQnD>jAWeF4 z0fGcj45&*$Q9!z72@4S{FNbyZocFxveA}~T&+I+(-t&BW<|#AxbzS%M`&(IC>Kk~P zK()YES1qu=AOJuCfB^sp0096O07w8(0B{4q0{{j9UI2suzyW{jBLGYRFay9604o5j0k8$Y z4uB&7I0E1Vz%c-v0dN7p6##btyaDh9zz+a_00IFB1|SrGZ~!g+%a66cE%G6fzbP{#96*EQ~Y7;mmM& zbG!&uM3g2fc34czPE6chT*660(pgf9Aw~2i5`Bm={xUKFGO|Ik@?r7{5ef?D6_l?i zD_>PsiBVBus;I`PswSwar>JYBYiML>Xy<9`6zS;R)YZMMt5>3@SE{F9rLSMDZ%|`k zP-|d#&(NsV$hh6uxWkzI8=2fkrVLOhgA~(YQ?pSs^C#vO;}#Yi3(E;h%SlVC=T=s~ zTUpOo(`IS3H#D0$8=D0i+jq8yR}R~)+u6OhJM!_!kxxhH8}y@_M~`kDwf|yozin^- z)!yNogX4Ec#~+SPdrto&F5(`KorR-=8HH}4t*!~%w*nyU!V&0w`R%J1fWK_Auc?2V z`htfoJ&b($fl?`7bn59Zs zHVFWcv7BXUZleH_lm)HzU~IMWY5eZwc3xR80f92wOks| z3~Dq8Pkmzr-8-q?({8z>`Y|LWr=ED zi52-!i)m84K(oGFia2VqB=OQ3m2}NGh!W3JxS_aw9on=~l5Ky{aVeP*eM3f2j&D)M z`YQwm6u(ZF%v~NVhhXBqu92{1Z2c~RHm?c$XgZ_A3BjVH%3-IeCd_G=W1uGj0(-GH z%nvOb0hLKcnY7|Ms3&U(cUJ71H)WMOO%7B^?Pg!>K$q9%Rv3ne*W-VP1r|SybgsH@ zAQoTS(C4MH8u|qKxvs#)uKQl+>}j|%4sB$U)pFR-K(eiZlvZOUbcV8;y)>HkF@u#l z^U31v36-F(FE|@)gY;}w3OU0h^|3h6XRlpmVjMRrZ->tCS3`RvD;lX{Eae0~qd+O@ zWMyAtQU489`elBJk}%wSSHZ`8cx$7DAk4*s2^o7}t8+oV?9%(Q_FJlfky3o#I;KuA zY{X5w+A&?$JN2_)`teLO1n9kwHW!$E;Hx_NBdGtOTlMLT$jm>pqtEw>`0Vw58SsF* zVF#tGZ9^(?JhD{TY!E!hEHbHXvby-oN12?dppbz%+OT9>Thg1?yIh4oKgSB&AGqb( zuH~_93L|MTbf$FT)H!cW96rjth40RWyL~ zNPKc&NdA!-b3-D2@an0uxBuvg?0#-weZI$r{S^$886hJ+pBnfbaP;R4=0St(s<3PdDqM_LRC-WN_R#leew@mw}#L z&Db~ZNjv%?m681QGKmaxjD=;^_jFUjMe|!^dXuziwD%Re1t!_Pv0-dqZPft-;6zjc z5>Q+uI@3u3bFI1%f^PSWsazcUky_v!H3uHW5v)6>)J&*n>A~?d~>7}O%OP> zx17k-EO3Uul|Q6}WpoLETktZCd1roJbU@*k0b5>6P<>omAosaL*_W7I>z<#s=h|d* zmEuy={PeQV^@z&p$!zP>cUH&&gk)){B+PjMIF5~lQ?Vm^Onm2%nJgA16xsquQ%=j6 z_1WU$8#QFI{Lr})REm}vKFKQ)1#`2@P&9|d{!r2XDLX+i(4L@k50(nWDgx7@(%c+H zXYnChE4LbAo-(|m&|lC7Kf_6~3$4O9;q2Nyo@IDEjU&&#B<**otB(%fG`{C=eyS_s z1{65jeqqxiXvAW#&9Z2fHR?t(=1ARq9aQO|%hQ>J*t&>&To5y(p93*#1XextZmoM& z80bMS)=R0zxKjjTy3`3an8HP{JsLB>@oR*-1iHb1GgIhT1Co#o-KJq5UF68beo+Ck zwa14^`4D~-pMMphb?oRPx&?eQy&o8I4_>BiAisTUC*Xd9)5%;h0v^j;s(l!Y`6lzI@VxV$g*Ci> z7vh{z-p`0?Ji<4ahO*KJvZ;zdlSLGYX{?aa8#(&Sxmu`uXGk-%Dc#rho!G0LNBZMU zndcwBlls0hOy+COO0r!fiSM$hcFj2jj~C?+?T*kho3B^dE-5p1N1ex;^V=UUsbAV1 z^WeK*IBdJDeRKD*pWXeUmyehAx_3pM1ZUpAwPL$s_-c1NYW#lj*T*X+?XrY_DT~HX z&3%yC?~f=r=!C=nZ;Slzd4mt2_o)iI|GRsj^Zz;9i33OY{`c9w&}Y}9x_`E7k(9he z52Z>dZJwes8EvnpvP09j)ivzv{qCBB0&X?NgGDc51zFt0Iy5C7<5E`2K7CwV( zZkG0jzoSMGUqwY$)(u!#(Rhp;ypGa`d})f%-XO@Uu(78s(~m-kAw=Pe(k^(c@|h%? z`qH3iq9E=xYa~|bEX!3qk_WC%o~``B$_shb9+VK8oo36b>}ErG7snDFpDaI%l9Dfs z(t5=rxcAKE=&ZbdoPF?I+vrl5ZyohJ+PtWGZhbV>pOMJ8(cIs5?SiKC$h3~%1S&-w znG$8Q*^t3&S8GC@8^NjC+S_s5dcQ`*S7&{ixf0s6AT+0Oeuaqr^%RrRA{!gj9kKl8 z96O!FA(cniJIeGEPL1Ici8|@xRfK29&oWklgB=Dt2uYF>2p_}|u+FTA+q)16H*?~t zWOCPMd8`_BqQaFAv!%#^7;lS^DHJY$4j|U-42cDx!ntyxT1q~-uGb+IxyRAEiPzb8 zSC5Y{g7=Um>VJt zD6&rbZH{SB9xY)&zoy{LHi}$0C{R%xo99&`YyGFkH8oPe6<3}6`>Ig=b#ozac&?(@ zFyhVQ8!e=#*G-sul2Jlu9^x5ob?h#S6re8H5qhv6@jjmin&Q1g%75<|?|!<({6xej zRSnnWbxA*~bBeKE%MVnY^X4Ra75TYavYb5gT3t#|&$C}Z3lvC;{beSH<@_=_nn zYj^voDF~?9H=SyV(QsCX?CPZAWDgt44NsWl!#`rcXJUGm9`=p8Vmy}DV+gApu5(1j z;E*o<`LTmd`83V(+CLt7x9!Xj|GcDDFyH?wv{g0heM;=pl4s*?y$X?o z$6771(uy|(QZyGn{vtw2;5X-{Wj9i=!2|rh5~kbpNx4_vZTLz?@^I^8lU+aO%#jzh zqF#|3Sc@ZMp$EQ1Q%bOKLZWR*^35RWdTLg3)fexDKtvJNq9%sVb+_7rngDhLx+N>(EI z3kUgay7*q|9}$s2ZYp`>xS^E!VzNzeMOJ1F%h6W3H)I0s)f+>>s)aF0oJz%er8Y;H zPZGE(tsmsFq(Q3L=n%kY60p!#wB!5ww$KoAeLGZz4(VRk=ZFPVADtCD8N7jL=9C`g zh;aC}RGZyyF+VhMiYRn3PCbR*@hX%b`*-p!cVCH|e`e1Xr;OFN&lTwBC5^;^SRsK0 z#i@rS`&=P;Wt2TZ60s+&h3|94q6M!(TcGF-Gn#dtn7}Bxbh7+#((S0(4?7lu+TIr< z(ULP+l*5_EZ5^^o7BsE|-AsXl*617Kckx%bhDKjhPUKPZyfV;j0mC)NpU6M2X}f0u zp*hmzRF3b9H1toYmaOy00chwcdD*mJQ};ai=l^j1@#mb}qrbw8$`u-detz3JGJDa| zoE}LKIxOYwdHsoGcs$L(#}T}yYLJhOJM&@z=yGYURxc3b%@ z0ejdYC}apm3G`&fiW>!6+=2c~ITNNVeDhw5G~c^NO6)1(8!70}z^r0$eoFnlYxPk< zUB2?V4flR+y2*WQHMAtC;M=B+MzmS=^#tr>i07cE115$e5UN$)=xR<55%LI~h`|b8 z(cZl={T9-vrNXsOeF-6Qohs}|Zyv?>_Bda@qI%3$`9?^|pGTA$ zKIm|k(3kI%F`A8iOpLw}`I)0a79HX!tsC(SLf~vs9Cd<$DLOubK6Ubx!6UIi=`F;o z;Ia_x>yVypy;r^ZGh(k#cewKY@+$OH_*bzD!q8Q&3Op7P&a z67es!mi)+9dZA3K8=;SHCw>gf1!U% zA#z$Q&A2VvRVlmNx@aH$%ab3=+1@xLID?_J_>C89DCD@(LJ7Q;jO(a5lIPt#VOh1{ z3$#f@b|oyyFhSXg^is|JX3mek1jy7q@2dK<-?vpqnhj;X1U?s>R@H!h-(HlVH+XoL z!^aKMF%x}-I*11Ze^U?PRt76L1RA-C6U4U!u7%K5@lW$QUEc`sALN=S?|d9F_D$Dk ztZ;fVKbrC7)vd^j0yeO^m43z}UXoL$Z2_5X{1Fu&$oaD`yUzDKmx@pLS`O?U&)(ph zqVDt4jPP>}PZD~zXBJ-VJrb3e7FRFI`E5o+Ww&o>k0?lunGB)0MV;#`kaGKc1J7`t z+u(W;jZAsz7Wjv?KJnuEL4D$-J~@SbYz!vSj$}J8ob)h3u$`GmT9jK5W|TdY@OIW) zJXz2s%02NAx^*syA|^+_jQXNGC#Dh#A*8|Ng-&H}`uwtZ5jyXe<-c)sQ6};DMMdu< zJ+YKU3N5r>R@<))Rbi^-wgIsFpP%M}5}Be04t`3m%Ezup`4$Tuvzol~Yf!&0@B8_Y zgyLiBGm0ffli~7VWx?6jxyC;B$sxu*qeG+#-j+doM2IKc(E-zkK#HS>Ta+CR;QEAk zVNbdoYX#P5_JNH#`)+c%J6coQAEXu7^dP7|z&%!O|7f`^oJ+e4&7N`4@Cc@S^kj)R z0@xy-F6dm9*Ti=5_e7|7I^0sgc3~tu)O#`2Diht9!S94Fo|d7Q+8;UlvjPmXw6OfO(H49%)^dSQHTLnKdZEE!#AxIPGDExSU7`&ELc!s6%oyu4)+mb$QN7G+=?a67{Z_EwDRh>wj`z9w&`YG-KqXrZXv#le&?d7GPp zCp%o{L(>Ex(6p}C1bX$Pnlaois%r>-BvIx#MJ$CnSCQ@Edx3~@^eE5H7|3j$6G)3y zR_8HHv7!6Us?p)=!T$5>l*ZvR87*Nrm*eVvmN|#5Ipb7%vhe*`C^L<>%Sv0Jla4wqGEa+&jIt!ol5dB8{S;3iXBYDyGC|IYsH047Tmd;DR0bCD0uFXJFGq) zMF^bExxB~sgxs~VHYhG;^&r}F`ekb8%f-f8}N;20ImJI`AJOQ+|FWd+e{0EloaYys1cJ@as~@Un5g~chkfVo(k@$ zLsx94o4iMF>J#l25Q0P{Z)4uk9v7Xei8B7HTxwI<$!VGL=*d;8Q%X#n1WE;IDEF9IkzRdQLIfyX)ed>sFHcnc9y{w*&SCZXy4t*6`4ghaF6` zb|l?L3UB#AZwMfSS>n80d6q8{ABsLtbwzD&9Mihn=xs#_>dnKdonsClVEQCFHdBHx z=G`@Ji7gozGJc9bOHOQSNX&LSmUY6m2C{@lRP@W{a%Y72S6ut$4 mcu5JdHQ0?g$BL+H5wf>tN1VO>UQqn!1@rGG{?EYV(EkEIMc7{e literal 0 HcmV?d00001 From 0f4cecc97d41c7122b06584d00431b8a22c88867 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Sun, 23 Jan 2022 10:46:09 -0600 Subject: [PATCH 11/14] Fixed formatting on menu_view.js --- core/menu_view.js | 388 +++++++++++++++++++++++----------------------- 1 file changed, 194 insertions(+), 194 deletions(-) diff --git a/core/menu_view.js b/core/menu_view.js index f2bca3cf..9c750aba 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -2,68 +2,68 @@ 'use strict'; // ENiGMA½ -const View = require('./view.js').View; -const miscUtil = require('./misc_util.js'); -const pipeToAnsi = require('./color_codes.js').pipeToAnsi; +const View = require('./view.js').View; +const miscUtil = require('./misc_util.js'); +const pipeToAnsi = require('./color_codes.js').pipeToAnsi; // deps -const util = require('util'); -const assert = require('assert'); -const _ = require('lodash'); +const util = require('util'); +const assert = require('assert'); +const _ = require('lodash'); -exports.MenuView = MenuView; +exports.MenuView = MenuView; function MenuView(options) { - options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); - options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); + options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true); + options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true); - View.call(this, options); + View.call(this, options); - this.disablePipe = options.disablePipe || false; + this.disablePipe = options.disablePipe || false; - const self = this; + const self = this; - if (options.items) { - this.setItems(options.items); - } else { - this.items = []; - } - - this.renderCache = {}; - - this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); - - this.setHotKeys(options.hotKeys); - - this.focusedItemIndex = options.focusedItemIndex || 0; - this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; - - this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; - this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; - - // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization - this.focusPrefix = options.focusPrefix || ''; - this.focusSuffix = options.focusSuffix || ''; - - this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); - - this.hasFocusItems = function() { - return !_.isUndefined(self.focusItems); - }; - - this.getHotKeyItemIndex = function(ch) { - if (ch && self.hotKeys) { - const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; - if (_.isNumber(keyIndex)) { - return keyIndex; - } + if(options.items) { + this.setItems(options.items); + } else { + this.items = []; } - return -1; - }; - this.emitIndexUpdate = function() { - self.emit('index update', self.focusedItemIndex); - }; + this.renderCache = {}; + + this.caseInsensitiveHotKeys = miscUtil.valueWithDefault(options.caseInsensitiveHotKeys, true); + + this.setHotKeys(options.hotKeys); + + this.focusedItemIndex = options.focusedItemIndex || 0; + this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0; + + this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0; + this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0; + + // :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization + this.focusPrefix = options.focusPrefix || ''; + this.focusSuffix = options.focusSuffix || ''; + + this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1); + + this.hasFocusItems = function() { + return !_.isUndefined(self.focusItems); + }; + + this.getHotKeyItemIndex = function(ch) { + if(ch && self.hotKeys) { + const keyIndex = self.hotKeys[self.caseInsensitiveHotKeys ? ch.toLowerCase() : ch]; + if(_.isNumber(keyIndex)) { + return keyIndex; + } + } + return -1; + }; + + this.emitIndexUpdate = function() { + self.emit('index update', self.focusedItemIndex); + }; } util.inherits(MenuView, View); @@ -74,226 +74,226 @@ MenuView.prototype.setTextOverflow = function(overflow) { } MenuView.prototype.hasTextOverflow = function() { - return this.textOverflow !== undefined; + return this.textOverflow != undefined; } MenuView.prototype.setItems = function(items) { - if (Array.isArray(items)) { - this.sorted = false; - this.renderCache = {}; + if(Array.isArray(items)) { + this.sorted = false; + this.renderCache = {}; - // - // Items can be an array of strings or an array of objects. - // - // In the case of objects, items are considered complex and - // may have one or more members that can later be formatted - // against. The default member is 'text'. The member 'data' - // may be overridden to provide a form value other than the - // item's index. - // - // Items can be formatted with 'itemFormat' and 'focusItemFormat' - // - let text; - let stringItem; - this.items = items.map(item => { - stringItem = _.isString(item); - if (stringItem) { - text = item; - } else { - text = item.text || ''; - this.complexItems = true; - } + // + // Items can be an array of strings or an array of objects. + // + // In the case of objects, items are considered complex and + // may have one or more members that can later be formatted + // against. The default member is 'text'. The member 'data' + // may be overridden to provide a form value other than the + // item's index. + // + // Items can be formatted with 'itemFormat' and 'focusItemFormat' + // + let text; + let stringItem; + this.items = items.map(item => { + stringItem = _.isString(item); + if(stringItem) { + text = item; + } else { + text = item.text || ''; + this.complexItems = true; + } - text = this.disablePipe ? text : pipeToAnsi(text, this.client); - return Object.assign({}, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others - }); + text = this.disablePipe ? text : pipeToAnsi(text, this.client); + return Object.assign({ }, { text }, stringItem ? {} : item); // ensure we have a text member, plus any others + }); - if (this.complexItems) { - this.itemFormat = this.itemFormat || '{text}'; + if(this.complexItems) { + this.itemFormat = this.itemFormat || '{text}'; + } + + this.invalidateRenderCache(); } - - this.invalidateRenderCache(); - } }; MenuView.prototype.getRenderCacheItem = function(index, focusItem = false) { - const item = this.renderCache[index]; - return item && item[focusItem ? 'focus' : 'standard']; + const item = this.renderCache[index]; + return item && item[focusItem ? 'focus' : 'standard']; }; MenuView.prototype.removeRenderCacheItem = function(index) { - delete this.renderCache[index]; + delete this.renderCache[index]; }; MenuView.prototype.setRenderCacheItem = function(index, rendered, focusItem = false) { - this.renderCache[index] = this.renderCache[index] || {}; - this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; + this.renderCache[index] = this.renderCache[index] || {}; + this.renderCache[index][focusItem ? 'focus' : 'standard'] = rendered; }; MenuView.prototype.invalidateRenderCache = function() { - this.renderCache = {}; + this.renderCache = {}; }; MenuView.prototype.setSort = function(sort) { - if (this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { - return; - } - - const key = true === sort ? 'text' : sort; - if ('text' !== sort && !this.complexItems) { - return; // need a valid sort key - } - - this.items.sort((a, b) => { - const a1 = a[key]; - const b1 = b[key]; - if (!a1) { - return -1; + if(this.sorted || !Array.isArray(this.items) || 0 === this.items.length) { + return; } - if (!b1) { - return 1; - } - return a1.localeCompare(b1, { sensitivity: false, numeric: true }); - }); - this.sorted = true; + const key = true === sort ? 'text' : sort; + if('text' !== sort && !this.complexItems) { + return; // need a valid sort key + } + + this.items.sort( (a, b) => { + const a1 = a[key]; + const b1 = b[key]; + if(!a1) { + return -1; + } + if(!b1) { + return 1; + } + return a1.localeCompare( b1, { sensitivity : false, numeric : true } ); + }); + + this.sorted = true; }; MenuView.prototype.removeItem = function(index) { - this.sorted = false; - this.items.splice(index, 1); + this.sorted = false; + this.items.splice(index, 1); - if (this.focusItems) { - this.focusItems.splice(index, 1); - } + if(this.focusItems) { + this.focusItems.splice(index, 1); + } - if (this.focusedItemIndex >= index) { - this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); - } + if(this.focusedItemIndex >= index) { + this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0); + } - this.removeRenderCacheItem(index); + this.removeRenderCacheItem(index); - this.positionCacheExpired = true; + this.positionCacheExpired = true; }; MenuView.prototype.getCount = function() { - return this.items.length; + return this.items.length; }; MenuView.prototype.getItems = function() { - if (this.complexItems) { - return this.items; - } + if(this.complexItems) { + return this.items; + } - return this.items.map(item => { - return item.text; - }); + return this.items.map( item => { + return item.text; + }); }; MenuView.prototype.getItem = function(index) { - if (this.complexItems) { - return this.items[index]; - } + if(this.complexItems) { + return this.items[index]; + } - return this.items[index].text; + return this.items[index].text; }; MenuView.prototype.focusNext = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPrevious = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusNextPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusPreviousPageItem = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusFirst = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.focusLast = function() { - this.emitIndexUpdate(); + this.emitIndexUpdate(); }; MenuView.prototype.setFocusItemIndex = function(index) { - this.focusedItemIndex = index; + this.focusedItemIndex = index; }; MenuView.prototype.onKeyPress = function(ch, key) { - const itemIndex = this.getHotKeyItemIndex(ch); - if (itemIndex >= 0) { - this.setFocusItemIndex(itemIndex); + const itemIndex = this.getHotKeyItemIndex(ch); + if(itemIndex >= 0) { + this.setFocusItemIndex(itemIndex); - if (true === this.hotKeySubmit) { - this.emit('action', 'accept'); + if(true === this.hotKeySubmit) { + this.emit('action', 'accept'); + } } - } - MenuView.super_.prototype.onKeyPress.call(this, ch, key); + MenuView.super_.prototype.onKeyPress.call(this, ch, key); }; MenuView.prototype.setFocusItems = function(items) { - const self = this; + const self = this; - if (items) { - this.focusItems = []; - items.forEach(itemText => { - this.focusItems.push( - { - text: self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) - } - ); - }); - } + if(items) { + this.focusItems = []; + items.forEach( itemText => { + this.focusItems.push( + { + text : self.disablePipe ? itemText : pipeToAnsi(itemText, self.client) + } + ); + }); + } }; MenuView.prototype.setItemSpacing = function(itemSpacing) { - itemSpacing = parseInt(itemSpacing); - assert(_.isNumber(itemSpacing)); + itemSpacing = parseInt(itemSpacing); + assert(_.isNumber(itemSpacing)); - this.itemSpacing = itemSpacing; - this.positionCacheExpired = true; + this.itemSpacing = itemSpacing; + this.positionCacheExpired = true; }; MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) { - itemHorizSpacing = parseInt(itemHorizSpacing); - assert(_.isNumber(itemHorizSpacing)); + itemHorizSpacing = parseInt(itemHorizSpacing); + assert(_.isNumber(itemHorizSpacing)); - this.itemHorizSpacing = itemHorizSpacing; - this.positionCacheExpired = true; + this.itemHorizSpacing = itemHorizSpacing; + this.positionCacheExpired = true; }; MenuView.prototype.setPropertyValue = function(propName, value) { - switch (propName) { - case 'itemSpacing': this.setItemSpacing(value); break; - case 'itemHorizSpacing': this.setItemHorizSpacing(value); break; - case 'items': this.setItems(value); break; - case 'focusItems': this.setFocusItems(value); break; - case 'hotKeys': this.setHotKeys(value); break; - case 'textOverflow': this.setTextOverflow(value); break; - case 'hotKeySubmit': this.hotKeySubmit = value; break; - case 'justify': this.setJustify(value); break; - case 'fillChar': this.setFillChar(value); break; - case 'focusItemIndex': this.focusedItemIndex = value; break; + switch(propName) { + case 'itemSpacing' : this.setItemSpacing(value); break; + case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break; + case 'items' : this.setItems(value); break; + case 'focusItems' : this.setFocusItems(value); break; + case 'hotKeys' : this.setHotKeys(value); break; + case 'textOverflow' : this.setTextOverflow(value); break; + case 'hotKeySubmit' : this.hotKeySubmit = value; break; + case 'justify' : this.setJustify(value); break; + case 'fillChar' : this.setFillChar(value); break; + case 'focusItemIndex' : this.focusedItemIndex = value; break; - case 'itemFormat': - case 'focusItemFormat': - this[propName] = value; - // if there is a cache currently, invalidate it - this.invalidateRenderCache(); - break; + case 'itemFormat' : + case 'focusItemFormat' : + this[propName] = value; + // if there is a cache currently, invalidate it + this.invalidateRenderCache(); + 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.setFillChar = function(fillChar) { @@ -305,18 +305,18 @@ MenuView.prototype.setJustify = function(justify) { this.justify = justify; this.invalidateRenderCache(); this.positionCacheExpired = true; -}; +} MenuView.prototype.setHotKeys = function(hotKeys) { - if (_.isObject(hotKeys)) { - if (this.caseInsensitiveHotKeys) { - this.hotKeys = {}; - for (var key in hotKeys) { - this.hotKeys[key.toLowerCase()] = hotKeys[key]; - } - } else { - this.hotKeys = hotKeys; + if(_.isObject(hotKeys)) { + if(this.caseInsensitiveHotKeys) { + this.hotKeys = {}; + for(var key in hotKeys) { + this.hotKeys[key.toLowerCase()] = hotKeys[key]; + } + } else { + this.hotKeys = hotKeys; + } } - } }; From 7735017791927dde6d3e08d6e74547903d4c654e Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Mon, 24 Jan 2022 12:35:28 -0600 Subject: [PATCH 12/14] Changed var to let/const --- core/full_menu_view.js | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index bbdda4b6..8cc61dee 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -59,7 +59,7 @@ function FullMenuView(options) { this.clearPage = function() { - var width = this.dimens.width; + let width = this.dimens.width; if (this.oldDimens) { if (this.oldDimens.width > width) { width = this.oldDimens.width; @@ -67,8 +67,8 @@ function FullMenuView(options) { delete this.oldDimens; } - for (var i = 0; i < this.dimens.height; i++) { - let text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; + for (let i = 0; i < this.dimens.height; i++) { + const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`); } } @@ -95,16 +95,16 @@ function FullMenuView(options) { this.itemsPerRow = this.items.length; } - var col = this.position.col; - var row = this.position.row; - var spacer = this.getSpacer(); + let col = this.position.col; + let row = this.position.row; + const spacer = this.getSpacer(); - var itemInRow = 0; - var itemInCol = 0; + let itemInRow = 0; + let itemInCol = 0; - var pageStart = 0; + let pageStart = 0; - for (var i = 0; i < this.items.length; ++i) { + for (let i = 0; i < this.items.length; ++i) { itemInRow++; this.items[i].row = row; this.items[i].col = col; @@ -114,19 +114,19 @@ function FullMenuView(options) { // have to calculate the max length on the last entry if (i == this.items.length - 1) { - var maxLength = 0; - for (var j = 0; j < this.itemsPerRow; j++) { + let maxLength = 0; + for (let 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; + const 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++) { + for (let j = 0; j < this.itemsPerRow; j++) { if (this.items[i - j].col != this.items[i].col) { break; } @@ -141,7 +141,7 @@ function FullMenuView(options) { this.pages.push({ start: pageStart, end: i - itemInRow }); // fix the last column processed - for (var j = 0; j < this.itemsPerRow; j++) { + for (let j = 0; j < this.itemsPerRow; j++) { if (this.items[i - j].col != col) { break; } @@ -161,17 +161,17 @@ function FullMenuView(options) { // restart row for next column row = this.position.row; - var maxLength = 0; - for (var j = 0; j < this.itemsPerRow; j++) { + let maxLength = 0; + for (let j = 0; j < this.itemsPerRow; j++) { // TODO: handle complex items - var itemLength = this.items[i - j].text.length; + let 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++) { + for (let j = 0; j < this.itemsPerRow; j++) { this.items[i - j].fixedLength = maxLength; } @@ -189,7 +189,7 @@ function FullMenuView(options) { itemInRow = 0; // fix the last column processed - for (var j = 0; j < this.itemsPerRow; j++) { + for (let j = 0; j < this.itemsPerRow; j++) { this.items[i - j].col = col; } @@ -395,11 +395,11 @@ FullMenuView.prototype.focusPrevious = function() { FullMenuView.prototype.focusPreviousColumn = function() { - var currentRow = this.items[this.focusedItemIndex].itemInRow; + const currentRow = this.items[this.focusedItemIndex].itemInRow; this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow; if (this.focusedItemIndex < 0) { this.clearPage(); - var lastItemRow = this.items[this.items.length - 1].itemInRow; + const lastItemRow = this.items[this.items.length - 1].itemInRow; if (lastItemRow > currentRow) { this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1; } @@ -425,7 +425,7 @@ FullMenuView.prototype.focusPreviousColumn = function() { FullMenuView.prototype.focusNextColumn = function() { - var currentRow = this.items[this.focusedItemIndex].itemInRow; + const currentRow = this.items[this.focusedItemIndex].itemInRow; this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow; if (this.focusedItemIndex > this.items.length - 1) { this.focusedItemIndex = currentRow - 1; From 626329d180970f081e186ad3112c2e6816475a93 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Mon, 24 Jan 2022 12:56:58 -0600 Subject: [PATCH 13/14] Removed self --- core/full_menu_view.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 8cc61dee..4ac48504 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -28,10 +28,6 @@ function FullMenuView(options) { this.initDefaultWidth(); - - - const self = this; - // we want page up/page down by default if (!_.isObject(options.specialKeyMap)) { Object.assign(this.specialKeyMap, { @@ -51,14 +47,7 @@ function FullMenuView(options) { this.autoAdjustHeightIfEnabled(); - - - this.getSpacer = function() { - return new Array(self.itemHorizSpacing + 1).join(this.fillChar); - } - - - this.clearPage = function() { + this.clearPage = () => { let width = this.dimens.width; if (this.oldDimens) { if (this.oldDimens.width > width) { @@ -69,11 +58,11 @@ function FullMenuView(options) { for (let i = 0; i < this.dimens.height; i++) { const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`; - self.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`); + this.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`); } } - this.cachePositions = function() { + this.cachePositions = () => { if (this.positionCacheExpired) { // first, clear the page this.clearPage(); @@ -97,7 +86,7 @@ function FullMenuView(options) { let col = this.position.col; let row = this.position.row; - const spacer = this.getSpacer(); + const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar); let itemInRow = 0; let itemInCol = 0; From 113db826cf45a25f1d52493d6b7bed86e5e3b517 Mon Sep 17 00:00:00 2001 From: Nathan Byrd Date: Mon, 24 Jan 2022 13:08:54 -0600 Subject: [PATCH 14/14] Changed to arrow functions --- core/full_menu_view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/full_menu_view.js b/core/full_menu_view.js index 4ac48504..212b4d15 100644 --- a/core/full_menu_view.js +++ b/core/full_menu_view.js @@ -36,7 +36,7 @@ function FullMenuView(options) { }); } - this.autoAdjustHeightIfEnabled = function() { + this.autoAdjustHeightIfEnabled = () => { if (this.autoAdjustHeight) { this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing); this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row); @@ -200,7 +200,7 @@ function FullMenuView(options) { this.positionCacheExpired = false; }; - this.drawItem = function(index) { + this.drawItem = (index) => { const item = this.items[index]; if (!item) { return;