[CORD-965] Fixed Safari bug on Keyboard Shortcut and added tests
Change-Id: Ibaf99ea4eccda47105f6dba149950b55ad7f383c
diff --git a/src/app/core/header/header.ts b/src/app/core/header/header.ts
index 62e3ce9..268fdcd 100644
--- a/src/app/core/header/header.ts
+++ b/src/app/core/header/header.ts
@@ -56,7 +56,7 @@
// listen for keypress
this.XosKeyboardShortcut.registerKeyBinding({
- key: 'f',
+ key: 'F',
description: 'Select search box',
cb: () => {
$('.navbar-form input').focus();
diff --git a/src/app/core/key-binding/key-binding-panel.html b/src/app/core/key-binding/key-binding-panel.html
index 7ce982a..4769faa 100644
--- a/src/app/core/key-binding/key-binding-panel.html
+++ b/src/app/core/key-binding/key-binding-panel.html
@@ -14,7 +14,7 @@
<span ng-repeat="m in binding.modifiers">
{{m}} +
</span>
- {{binding.key}}
+ {{binding.label.toLowerCase() || binding.key.toLowerCase()}}
</code>
</div>
<div class="col-xs-7">
diff --git a/src/app/core/services/keyboard-shortcut.spec.ts b/src/app/core/services/keyboard-shortcut.spec.ts
new file mode 100644
index 0000000..b573656
--- /dev/null
+++ b/src/app/core/services/keyboard-shortcut.spec.ts
@@ -0,0 +1,179 @@
+import * as angular from 'angular';
+import 'angular-mocks';
+import {IXosKeyboardShortcutService, XosKeyboardShortcut, IXosKeyboardShortcutBinding} from './keyboard-shortcut';
+import {IXosSidePanelService} from '../side-panel/side-panel.service';
+
+let service: IXosKeyboardShortcutService;
+let $log: ng.ILogService;
+let $transitions: any;
+let XosSidePanel: IXosSidePanelService;
+
+const baseGlobalModifiers: IXosKeyboardShortcutBinding[] = [
+ {
+ key: 'a',
+ cb: 'cb'
+ },
+ {
+ key: 'a',
+ cb: 'modified',
+ modifiers: ['alt']
+ },
+ {
+ key: 'a',
+ cb: 'modified',
+ modifiers: ['meta']
+ }
+];
+
+const baseLocalModifiers: IXosKeyboardShortcutBinding[] = [
+ {
+ key: 'b',
+ cb: 'cb'
+ },
+ {
+ key: 'b',
+ cb: 'modified',
+ modifiers: ['meta', 'alt']
+ }
+];
+
+describe('The XosKeyboardShortcut service', () => {
+
+ beforeEach(() => {
+ angular.module('leyBinding', ['ui.router'])
+ .service('XosKeyboardShortcut', XosKeyboardShortcut)
+ .value('XosSidePanel', {
+
+ });
+ angular.mock.module('leyBinding');
+
+ angular.mock.inject((
+ _$log_: ng.ILogService,
+ _$transitions_: any,
+ _XosSidePanel_: IXosSidePanelService
+ ) => {
+ $log = _$log_;
+ $transitions = _$transitions_;
+ XosSidePanel = _XosSidePanel_;
+ });
+
+ service = new XosKeyboardShortcut($log, $transitions, XosSidePanel);
+ });
+
+ it('should have a setup method', () => {
+ expect(service.setup).toBeDefined();
+ });
+
+ describe('the addActiveModifierKey method', () => {
+ beforeEach(() => {
+ service['activeModifiers'] = [];
+ });
+ it('should add an active modifier', () => {
+ service['addActiveModifierKey']('shift');
+ expect(service['activeModifiers']).toEqual(['shift']);
+ });
+
+ it('should not add a modifier twice', () => {
+ service['addActiveModifierKey']('shift');
+ service['addActiveModifierKey']('shift');
+ expect(service['activeModifiers']).toEqual(['shift']);
+ });
+ });
+
+ describe('the removeActiveModifierKey method', () => {
+ beforeEach(() => {
+ service['activeModifiers'] = ['shift', 'meta'];
+ });
+ it('should remove an active modifier', () => {
+ service['removeActiveModifierKey']('shift');
+ expect(service['activeModifiers']).toEqual(['meta']);
+ });
+ });
+
+ describe('the findBindedShortcut method', () => {
+ beforeEach(() => {
+ service['activeModifiers'] = [];
+ service['keyMapping']['global'] = baseGlobalModifiers;
+ service['keyMapping']['view'] = baseLocalModifiers;
+ });
+
+ it('should find a global keybinding', () => {
+ const binding = service['findBindedShortcut']('a');
+ expect(binding).toEqual({key: 'a', cb: 'cb'});
+ });
+
+ it('should find a global keybinding with modifiers', () => {
+ service['activeModifiers'] = ['meta'];
+ const binding = service['findBindedShortcut']('a');
+ expect(binding).toEqual({key: 'a', cb: 'modified', modifiers: ['meta']});
+ });
+
+ it('should find a view keybinding', () => {
+ const binding = service['findBindedShortcut']('b');
+ expect(binding).toEqual({key: 'b', cb: 'cb'});
+ });
+
+ it('should find a view keybinding with modifiers', () => {
+ service['activeModifiers'] = ['meta', 'alt'];
+ const binding = service['findBindedShortcut']('b');
+ expect(binding).toEqual({key: 'b', cb: 'modified', modifiers: ['meta', 'alt']});
+ });
+
+ it('should not care about binding key case', () => {
+ const binding = service['findBindedShortcut']('A');
+ expect(binding).toEqual({key: 'a', cb: 'cb'});
+ });
+ });
+
+ describe('the registerKeyBinding method', () => {
+
+ const binding = {
+ key: 'B',
+ cb: 'callback'
+ };
+
+ beforeEach(() => {
+ service['keyMapping'] = {
+ global: [
+ {
+ key: 'a',
+ cb: 'cb'
+ }
+ ],
+ view: []
+ };
+ });
+
+ it('should add a new global keybinding', () => {
+ service['registerKeyBinding'](binding, 'global');
+ expect(service['keyMapping']['global'].length).toBe(2);
+ expect(service['keyMapping']['global'][1].key).toBe('b');
+ });
+
+ it('should add a new view keybinding', () => {
+ service['registerKeyBinding'](binding);
+ expect(service['keyMapping']['view'].length).toBe(1);
+ expect(service['keyMapping']['view'][0].key).toBe('b');
+ });
+
+ it('should not add binding that is not registered as "global" or "view"', () => {
+ function errorFunctionWrapper() {
+ service['registerKeyBinding']({
+ key: 'z',
+ cb: 'cb'
+ }, 'something');
+ }
+ expect(errorFunctionWrapper).toThrow(new Error('[XosKeyboardShortcut] A shortcut can be registered with scope "global" or "view" only'));
+ });
+
+ it('should not add binding that has an already registered key', () => {
+ function errorFunctionWrapper() {
+ service['registerKeyBinding']({
+ key: 'A',
+ cb: 'cb'
+ }, 'global');
+ }
+ expect(errorFunctionWrapper).toThrow(new Error('[XosKeyboardShortcut] A shortcut for key "a" has already been registered'));
+ });
+ });
+});
diff --git a/src/app/core/services/keyboard-shortcut.ts b/src/app/core/services/keyboard-shortcut.ts
index c27c9c3..e814943 100644
--- a/src/app/core/services/keyboard-shortcut.ts
+++ b/src/app/core/services/keyboard-shortcut.ts
@@ -17,6 +17,7 @@
key: string;
cb: any;
modifiers?: string[];
+ label?: string;
description?: string;
onInput?: boolean;
}
@@ -27,7 +28,7 @@
global: [],
view: []
};
- public allowedModifiers: string[] = ['Meta', 'Alt', 'Shift', 'Control'];
+ public allowedModifiers: string[] = ['meta', 'alt', 'shift', 'control'];
public activeModifiers: string[] = [];
private toggleKeyBindingPanel = (): void => {
@@ -44,17 +45,14 @@
/* tslint:disable */
public baseBindings: IXosKeyboardShortcutBinding[] = [
{
- key: '?',
+ key: 'slash',
+ label: '/',
description: 'Toggle Shortcut Panel',
cb: this.toggleKeyBindingPanel,
},
{
- key: '/',
- description: 'Toggle Shortcut Panel',
- cb: this.toggleKeyBindingPanel,
- },
- {
- key: 'Escape',
+ key: 'esc',
+ label: 'Esc',
cb: (event) => {
// NOTE removing focus from input elements on Esc
event.target.blur();
@@ -85,15 +83,7 @@
this.$log.info(`[XosKeyboardShortcut] Setup`);
$('body').on('keydown', (e) => {
- let pressedKey = null;
-
- if (e.key.length === 1 && e.key.match(/[a-z]/i) || String.fromCharCode(e.keyCode).toLowerCase().match(/[a-z]/i)) {
- // alphabet letters found
- pressedKey = String.fromCharCode(e.keyCode).toLowerCase();
- }
- else {
- pressedKey = e.key;
- }
+ const pressedKey = this.whatKey(e.which);
if (this.allowedModifiers.indexOf(e.key) > -1) {
this.addActiveModifierKey(e.key);
@@ -123,12 +113,18 @@
}
public registerKeyBinding(binding: IXosKeyboardShortcutBinding, target: string = 'view'): void {
- // NOTE check for already taken binding (by key)
- // NOTE check target is either 'view' or 'global'
- this.$log.debug(`[XosKeyboardShortcut] Registering binding for key: ${binding.key}`);
- if (!_.find(this.keyMapping[target], {key: binding.key})) {
- this.keyMapping[target].push(binding);
+
+ if (target !== 'global' && target !== 'view') {
+ throw new Error('[XosKeyboardShortcut] A shortcut can be registered with scope "global" or "view" only');
}
+
+ binding.key = binding.key.toLowerCase();
+ if (_.find(this.keyMapping.global, {key: binding.key}) || _.find(this.keyMapping.view, {key: binding.key})) {
+ throw new Error(`[XosKeyboardShortcut] A shortcut for key "${binding.key}" has already been registered`);
+ }
+
+ this.$log.debug(`[XosKeyboardShortcut] Registering binding for key: ${binding.key}`);
+ this.keyMapping[target].push(binding);
}
private addActiveModifierKey(key: string) {
@@ -142,23 +138,65 @@
}
private findBindedShortcut(key: string): IXosKeyboardShortcutBinding {
- // NOTE search for binding in the global map
- let target = _.find(this.keyMapping.global, {key: key});
+ const globalTargets = _.filter(this.keyMapping.global, {key: key.toLowerCase()});
- // NOTE if it is not there look in the view map
- if (!angular.isDefined(target)) {
- target = _.find(this.keyMapping.view, {key: key});
+ const localTargets = _.filter(this.keyMapping.view, {key: key.toLowerCase()});
+
+ let targets = globalTargets.concat(localTargets);
+
+ if (targets.length === 0) {
+ return;
}
+ // NOTE remove targets that does not match modifiers
+ targets = _.filter(targets, (t: IXosKeyboardShortcutBinding) => {
+ if (this.activeModifiers.length === 0) {
+ return true;
+ }
+ else if (t.modifiers && _.difference(t.modifiers, this.activeModifiers).length === 0) {
+ return true;
+ }
+ return false;
+ });
- if (target && target.modifiers) {
- // if not all the modifier keys for that binding are pressed
- if (_.difference(target.modifiers, this.activeModifiers).length > 0) {
- // do not match
- return;
- };
- }
- return target;
+ return targets[0];
}
+ private whatKey(code: number) {
+ switch (code) {
+ case 8: return 'delete';
+ case 9: return 'tab';
+ case 13: return 'enter';
+ case 16: return 'shift';
+ case 17: return 'control';
+ case 18: return 'alt';
+ case 27: return 'esc';
+ case 32: return 'space';
+ case 37: return 'leftArrow';
+ case 38: return 'upArrow';
+ case 39: return 'rightArrow';
+ case 40: return 'downArrow';
+ case 91: return 'meta';
+ case 186: return 'semicolon';
+ case 187: return 'equals';
+ case 188: return 'comma';
+ case 189: return 'dash';
+ case 190: return 'dot';
+ case 191: return 'slash';
+ case 192: return 'backQuote';
+ case 219: return 'openBracket';
+ case 220: return 'backSlash';
+ case 221: return 'closeBracket';
+ case 222: return 'quote';
+ default:
+ if ((code >= 48 && code <= 57) ||
+ (code >= 65 && code <= 90)) {
+ return String.fromCharCode(code);
+ } else if (code >= 112 && code <= 123) {
+ return 'F' + (code - 111);
+ }
+ return null;
+ }
+}
+
}