Skip to content

Added optional 'category' for hotkeys. #205

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
28 changes: 27 additions & 1 deletion src/hotkeys.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,41 @@
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%;
display: table-cell;
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 {
Expand All @@ -52,6 +77,7 @@
.cfp-hotkeys-keys {
padding: 5px;
text-align: right;
width: 45%;
}

.cfp-hotkeys-key {
Expand Down
76 changes: 60 additions & 16 deletions src/hotkeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -47,14 +54,19 @@
this.template = '<div class="cfp-hotkeys-container fade" ng-class="{in: helpVisible}" style="display: none;"><div class="cfp-hotkeys">' +
'<h4 class="cfp-hotkeys-title" ng-if="!header">{{ title }}</h4>' +
'<div ng-bind-html="header" ng-if="header"></div>' +
'<table><tbody>' +
'<tr ng-repeat="hotkey in hotkeys | filter:{ description: \'!$$undefined$$\' }">' +
'<td class="cfp-hotkeys-keys">' +
'<span ng-repeat="key in hotkey.format() track by $index" class="cfp-hotkeys-key">{{ key }}</span>' +
'</td>' +
'<td class="cfp-hotkeys-text">{{ hotkey.description }}</td>' +
'</tr>' +
'</tbody></table>' +
'<div class="cfp-hotkeys-category-container">' +
'<table ng-repeat="(categoryName,hotkeysInCategory) in categories"><tbody>' +
'<tr ng-if="numCategories > 1">' +
'<td class="cfp-hotkeys-category-title" colspan="2">{{categoryName}}</td>' +
'</tr>' +
'<tr ng-repeat="hotkey in hotkeysInCategory | filter:{ description: \'!$$undefined$$\' }">' +
'<td class="cfp-hotkeys-keys">' +
'<span class="cfp-hotkeys-key" ng-repeat="key in hotkey.format() track by $index">{{ key }}</span>' +
'</td>' +
'<td class="cfp-hotkeys-text">{{ hotkey.description }}</td>' +
'</tr>' +
'</tbody></table>' +
'</div>' +
'<div ng-bind-html="footer" ng-if="footer"></div>' +
'<div class="cfp-hotkeys-close" ng-click="toggleCheatSheet()">&#215;</div>' +
'</div></div>';
Expand Down Expand Up @@ -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
Expand All @@ -154,6 +167,7 @@
this.action = action;
this.allowIn = allowIn;
this.persistent = persistent;
this.category = category;
this._formated = null;
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand All @@ -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');

Expand All @@ -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;

Expand All @@ -346,6 +386,7 @@
action = combo.action;
persistent = combo.persistent;
allowIn = combo.allowIn;
category = combo.category;
combo = combo.combo;
}

Expand All @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -579,6 +625,7 @@
template : this.template,
toggleCheatSheet : toggleCheatSheet,
includeCheatSheet : this.includeCheatSheet,
defaultCategoryName : this.defaultCategoryName,
cheatSheetHotkey : this.cheatSheetHotkey,
cheatSheetDescription : this.cheatSheetDescription,
useNgRoute : this.useNgRoute,
Expand All @@ -591,8 +638,6 @@
return publicApi;

};


})

.directive('hotkey', function (hotkeys) {
Expand Down Expand Up @@ -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.
});

})();
59 changes: 58 additions & 1 deletion test/hotkeys.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ describe 'Angular Hotkeys', ->
beforeEach ->
module 'cfp.hotkeys', (hotkeysProvider) ->
hotkeysProvider.useNgRoute = true
hotkeysProvider.defaultCategoryName = 'Generic'
return

result = null
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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'

Expand All @@ -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', ->
Expand Down