From 55774fbf3086a0768cea91ba8ce5cf495e5cc274 Mon Sep 17 00:00:00 2001 From: Ebbe Brandstrup Date: Mon, 22 Feb 2016 13:40:45 +0400 Subject: [PATCH] Added optional 'category' for hotkeys. When displaying the cheatsheet, the hotkeys are shown per category in a 2 column layout. If no category is specified for a hotkey, the hotkey defaults to a configurable default category. If no hotkeys have a category, the cheatsheet looks just like it used to. --- README.md | 1 + src/hotkeys.css | 28 ++++++++++++++++- src/hotkeys.js | 76 +++++++++++++++++++++++++++++++++++---------- test/hotkeys.coffee | 59 ++++++++++++++++++++++++++++++++++- 4 files changed, 146 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4160ebd..b380fdb 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,7 @@ angular.module('myApp', ['cfp.hotkeys']) - `callback`: The function to execute when the key(s) are pressed. Passes along two arguments, `event` and `hotkey` - `action`: [OPTIONAL] The type of event to listen for, such as `keypress`, `keydown` or `keyup`. Usage of this parameter is discouraged as the underlying library will pick the most suitable option automatically. This should only be necessary in advanced situations. - `allowIn`: [OPTIONAL] an array of tag names to allow this combo in ('INPUT', 'SELECT', and/or 'TEXTAREA') +- `category`: [OPTIONAL] A category name that the shortcut will be grouped under. ```js hotkeys.add({ diff --git a/src/hotkeys.css b/src/hotkeys.css index 24e504c..1f3900b 100644 --- a/src/hotkeys.css +++ b/src/hotkeys.css @@ -32,6 +32,12 @@ font-size: 1.2em; } +.cfp-hotkeys-category-title { + font-weight: bold; + font-size: 1.0em; + padding: 14px 0 10px 70px; +} + .cfp-hotkeys { width: 100%; height: 100%; @@ -39,9 +45,28 @@ vertical-align: middle; } +.cfp-hotkeys-category-container { + margin: 0 auto; + display: table; +} + +.cfp-hotkeys-category-container:after { + content: ""; + display: table; + clear: both; +} + .cfp-hotkeys table { - margin: auto; + margin: 0 25px 0 0; color: #333; + float: left; + width: 300px; +} + +.cfp-hotkeys-category-container table:nth-child(2n+3) { + content: ""; + display: table; + clear: both; } .cfp-content { @@ -52,6 +77,7 @@ .cfp-hotkeys-keys { padding: 5px; text-align: right; + width: 45%; } .cfp-hotkeys-key { diff --git a/src/hotkeys.js b/src/hotkeys.js index 5b27ec1..31ec7e9 100644 --- a/src/hotkeys.js +++ b/src/hotkeys.js @@ -19,6 +19,13 @@ */ this.includeCheatSheet = true; + /** + * Category name displayed on the cheatsheet for shortcuts that have not been added to a category. + * No category name is displayed if categories aren't used on any hotkeys. + * @type {String} + */ + this.defaultCategoryName = 'Generic'; + /** * Configurable setting to disable ngRoute hooks * @type {Boolean} @@ -47,14 +54,19 @@ this.template = ''; @@ -142,8 +154,9 @@ * @param {string} action the type of event to listen for (for mousetrap) * @param {array} allowIn an array of tag names to allow this combo in ('INPUT', 'SELECT', and/or 'TEXTAREA') * @param {Boolean} persistent Whether the hotkey persists navigation events + * @param {string} category Optional name for a category that the cheatsheet will show this hotkey under */ - function Hotkey (combo, description, callback, action, allowIn, persistent) { + function Hotkey (combo, description, callback, action, allowIn, persistent, category) { // TODO: Check that the values are sane because we could // be trying to instantiate a new Hotkey with outside dev's // supplied values @@ -154,6 +167,7 @@ this.action = action; this.allowIn = allowIn; this.persistent = persistent; + this.category = category; this._formated = null; } @@ -223,6 +237,11 @@ */ scope.toggleCheatSheet = toggleCheatSheet; + /** + * Expose defaultCategoryName to hotkeys scope so we can access it from within the Hotkey object + * @type {String} + */ + scope.defaultCategoryName = this.defaultCategoryName; /** * Holds references to the different scopes that have bound hotkeys @@ -291,7 +310,26 @@ } /** - * Toggles the help menu element's visiblity + * Rebuilds the shortcut categories so the cheatsheet will reflect the currently active shortcuts in their respective categories + */ + function _updateCategories () { + scope.categories = {}; + scope.numCategories = 0; + + for (var i = 0; i < scope.hotkeys.length; ++i) { + var hotkey = scope.hotkeys[i]; + var category = hotkey.category; + + if (!scope.categories[category]) { + scope.categories[category] = []; + ++scope.numCategories; + } + scope.categories[category].push(hotkey); + } + } + + /** + * Toggles the help menu element's visibility */ var previousEsc = false; @@ -302,6 +340,8 @@ // as a directive in the template, but that would create a nasty // circular dependency issue that I don't feel like sorting out. if (scope.helpVisible) { + _updateCategories(); + previousEsc = _get('esc'); _del('esc'); @@ -328,9 +368,9 @@ * @param {string} action the type of event to listen for (for mousetrap) * @param {array} allowIn an array of tag names to allow this combo in ('INPUT', 'SELECT', and/or 'TEXTAREA') * @param {boolean} persistent if true, the binding is preserved upon route changes + * @param {string} category optional name for a category that the cheatsheet will show this hotkey under */ - function _add (combo, description, callback, action, allowIn, persistent) { - + function _add (combo, description, callback, action, allowIn, persistent, category) { // used to save original callback for "allowIn" wrapping: var _callback; @@ -346,6 +386,7 @@ action = combo.action; persistent = combo.persistent; allowIn = combo.allowIn; + category = combo.category; combo = combo.combo; } @@ -361,6 +402,11 @@ description = '$$undefined$$'; } + // category is optional, but hotkeys that are not categorized still live in an implicit default category + if (!category) { + category = scope.defaultCategoryName; + } + // any items added through the public API are for controllers // that persist through navigation, and thus undefined should mean // true in this case. @@ -427,7 +473,7 @@ Mousetrap.bind(combo, wrapApply(callback)); } - var hotkey = new Hotkey(combo, description, callback, action, allowIn, persistent); + var hotkey = new Hotkey(combo, description, callback, action, allowIn, persistent, category); scope.hotkeys.push(hotkey); return hotkey; } @@ -579,6 +625,7 @@ template : this.template, toggleCheatSheet : toggleCheatSheet, includeCheatSheet : this.includeCheatSheet, + defaultCategoryName : this.defaultCategoryName, cheatSheetHotkey : this.cheatSheetHotkey, cheatSheetDescription : this.cheatSheetDescription, useNgRoute : this.useNgRoute, @@ -591,8 +638,6 @@ return publicApi; }; - - }) .directive('hotkey', function (hotkeys) { @@ -629,5 +674,4 @@ // force hotkeys to run by injecting it. Without this, hotkeys only runs // when a controller or something else asks for it via DI. }); - })(); diff --git a/test/hotkeys.coffee b/test/hotkeys.coffee index 02cb9e9..ee948c5 100644 --- a/test/hotkeys.coffee +++ b/test/hotkeys.coffee @@ -6,6 +6,7 @@ describe 'Angular Hotkeys', -> beforeEach -> module 'cfp.hotkeys', (hotkeysProvider) -> hotkeysProvider.useNgRoute = true + hotkeysProvider.defaultCategoryName = 'Generic' return result = null @@ -77,6 +78,54 @@ describe 'Angular Hotkeys', -> KeyEvent.simulate('?'.charCodeAt(0), 90) expect(hotkeys.get('esc')).toBe false + it 'should show a separate group of hotkeys for each unique hotkey category', -> + hotkeys.add + combo: 'w' + description: 'w' + category: 'group1' + + KeyEvent.simulate('?'.charCodeAt(0), 90) + + categoryList = Object.keys(scope.$$prevSibling.categories) + expect(categoryList.length).toBe 2 + categories = scope.$$prevSibling.categories + expect(categories['Generic']).toBeDefined() + expect(categories['group1']).toBeDefined() + + it 'should show a separate group of hotkeys for each unique hotkey category', -> + hotkeys.add + combo: 'w' + description: 'w' + category: 'group1' + + hotkeys.add + combo: 'm' + description: 'm' + category: 'group2' + + hotkeys.add + combo: 'n' + description: 'n' + category: 'Generic' # Explicitly setting the default category is the same as not setting a category + + hotkeys.add + combo: 'l' + description: 'l' + + KeyEvent.simulate('?'.charCodeAt(0), 90) + + categories = scope.$$prevSibling.categories + expect(categories['Generic'].length).toBe 3 + expect(categories['Generic']).toContain(jasmine.objectContaining({combo: ['?'] })) + expect(categories['Generic']).toContain(jasmine.objectContaining({combo: ['n'] })) + expect(categories['Generic']).toContain(jasmine.objectContaining({combo: ['l'] })) + + expect(categories['group1'].length).toBe 1 + expect(categories['group1']).toContain(jasmine.objectContaining({combo: ['w'] })) + + expect(categories['group2'].length).toBe 1 + expect(categories['group2']).toContain(jasmine.objectContaining({combo: ['m'] })) + it 'should remember previously bound ESC when cheatsheet is shown', -> expect(hotkeys.get('esc')).toBe false @@ -558,6 +607,14 @@ describe 'Configuration options', -> children = angular.element($rootElement).children() expect(children.hasClass('little-teapot')).toBe true + it 'should set the configured default category name for hotkeys without a category', -> + module 'cfp.hotkeys', (hotkeysProvider) -> + hotkeysProvider.cheatSheetHotkey = 'h' + hotkeysProvider.defaultCategoryName = 'default' + return + inject ($rootElement, hotkeys) -> + expect(hotkeys.get('h').category).toBe 'default' + it 'should run and inject itself so it is always available', -> module 'cfp.hotkeys' @@ -572,7 +629,7 @@ describe 'Configuration options', -> injector = angular.bootstrap(document, ['cfp.hotkeys']) injected = angular.element(document.body).find('div') - expect(injected.length).toBe 3 + expect(injected.length).toBe 4 expect(injected.hasClass('cfp-hotkeys-container')).toBe true it 'should have a configurable hotkey and description', ->