From e4a6de4eee866aabccd4b258c1f333799c9c8e68 Mon Sep 17 00:00:00 2001 From: Florrie Date: Tue, 2 Apr 2019 07:48:40 -0300 Subject: Focus context menu items by typing out their name --- README.md | 1 + ui.js | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5c95677..d66a65b 100644 --- a/README.md +++ b/README.md @@ -61,3 +61,4 @@ You're also welcome to share any ideas, suggestions, and questions through there * **In a context menu:** * All the usual controls (up, down, tab, space, enter) work * Backspace or Escape - close the menu without taking any action + * Type the beginning of the name of an option, like its first letter, to quickly jump to it diff --git a/ui.js b/ui.js index d9b652d..de2226a 100644 --- a/ui.js +++ b/ui.js @@ -273,7 +273,7 @@ class AppElement extends FocusElement { `(${item.name ? `"${item.name}"` : 'Unnamed'} -` + ` ${item.items.length} item${item.items.length === 1 ? '' : 's'}` + `, ${countTotalItems(item)} total` + - ')'}, + ')', keyboardIdentifier: item.name}, // The actual controls! {divider: true}, @@ -1451,6 +1451,7 @@ class InlineListPickerElement extends FocusElement { this.labelText = labelText this.options = options this.curIndex = 0 + this.keyboardIdentifier = this.labelText } fixLayout() { @@ -2210,6 +2211,8 @@ class ContextMenu extends FocusElement { this.form = new ListScrollForm() this.pane.addChild(this.form) + this.keyboardSelector = new KeyboardSelector(this.form) + this.visible = false } @@ -2238,6 +2241,7 @@ class ContextMenu extends FocusElement { this.form.addInput(element) } else { const button = new Button(item.label) + button.keyboardIdentifier = item.keyboardIdentifier || item.label if (item.action) { button.on('pressed', () => { this.close() @@ -2257,9 +2261,20 @@ class ContextMenu extends FocusElement { if (telc.isEscape(keyBuf) || telc.isBackspace(keyBuf)) { this.close() return false - } else { - super.keyPressed(keyBuf) + } else if (this.keyboardSelector.keyPressed(keyBuf)) { + return false + + const key = String.fromCharCode(keyBuf[0]).toLowerCase() + for (const input of this.form.inputs) { + if (input.selectable && input.menuKeyShortcut) { + if (input.menuKeyShortcut.toLowerCase() === key) { + this.form.selectInput(input) + return false + } + } + } } + super.keyPressed(keyBuf) } unselected() { @@ -2349,4 +2364,74 @@ class HorizontalRule extends FocusElement { } } +class KeyboardSelector { + // Class used to select things when you type out their name. Specify strings + // used to access each element of a form in the keyboardIdentifier property. + // (Elements without a keyboardIdentifier, or which are !selectable, will be + // skipped.) + + constructor(form) { + this.value = '' + this.form = form + } + + keyPressed(keyBuf) { + // Don't do anything if the input isn't a single keyboard character. + if (keyBuf.length !== 1 || keyBuf[0] <= 31 || keyBuf[0] >= 127) { + return + } + + // First see if a result is found when we append the typed character to our + // recorded input. + const char = keyBuf.toString() + this.value += char + if (!KeyboardSelector.find(this.value, this.form)) { + // If we don't find a result, replace our recorded input with the single + // character entered, then do a search. Start from the input after the + // current-selected one, so that we don't just end up re-selecting the + // element that was selected before, if there's another option that would + // match this key ahead. (This is so that you can easily type a string or + // character over and over to navigate through options that all start + // with the same string/character.) + this.value = char + return KeyboardSelector.find(this.value, this.form, this.form.curIndex + 1) + } + return true + } + + static find(text, form, fromIndex = form.curIndex) { + // Most of this code is just stolen from AppElement's code for handling + // input from JumpElement! + + const lower = text.toLowerCase() + const getName = inp => inp.keyboardIdentifier ? inp.keyboardIdentifier.toLowerCase().trim() : '' + + const testStartsWith = inp => getName(inp).startsWith(lower) + + const searchPastCurrentIndex = test => { + const start = fromIndex + const match = form.inputs.slice(start).findIndex(test) + if (match === -1) { + return -1 + } else { + return start + match + } + } + + const allIndexes = [ + searchPastCurrentIndex(testStartsWith), + form.inputs.findIndex(testStartsWith), + ] + + const matchedIndex = allIndexes.find(value => value >= 0) + + if (typeof matchedIndex !== 'undefined') { + form.selectInput(form.inputs[matchedIndex]) + return true + } else { + return false + } + } +} + module.exports.AppElement = AppElement -- cgit 1.3.0-6-gf8a5