[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;
+  }
+}
+
 }
diff --git a/src/app/template/directives/directives.ts b/src/app/template/directives/directives.ts
index 625c101..3e4840d 100644
--- a/src/app/template/directives/directives.ts
+++ b/src/app/template/directives/directives.ts
@@ -7,23 +7,41 @@
 import * as $ from 'jquery';
 import {IXosKeyboardShortcutService} from '../../core/services/keyboard-shortcut';
 
+export interface IXosNavHandlerService {
+  minimalize: () => void;
+}
+
+export class XosNavHandlerService implements IXosNavHandlerService {
+
+  static $inject = ['XosKeyboardShortcut'];
+
+  constructor(
+    private XosKeyboardShortcut: IXosKeyboardShortcutService
+  ) {
+    this.XosKeyboardShortcut.registerKeyBinding({
+      key: 'n',
+      description: 'Toggle Navigation',
+      cb: this.minimalize,
+    }, 'global');
+  }
+
+  public minimalize() {
+    $("body").toggleClass("nav-toggle");
+  }
+
+}
+
 /**
  * minimalizaSidebar - Directive for minimalize sidebar
  */
-export function minimalizaMenu($rootScope, XosKeyboardShortcut: IXosKeyboardShortcutService) {
+export function minimalizaMenu() {
   return {
     restrict: 'EA',
     template: '<div class="left-nav-toggle"><a href ng-click="minimalize()"><i class="stroke-hamburgermenu"></i> </a>',
-    controller: function ($scope, $element) {
+    controller: function ($scope, XosNavHandler: IXosNavHandlerService) {
       $scope.minimalize = function () {
-        $("body").toggleClass("nav-toggle");
+        XosNavHandler.minimalize();
       };
-
-      XosKeyboardShortcut.registerKeyBinding({
-        key: 'n',
-        description: 'Toggle Navigation',
-        cb: $scope.minimalize,
-      }, 'global');
     }
   };
 }
diff --git a/src/app/template/index.ts b/src/app/template/index.ts
index 11557f9..0e68771 100644
--- a/src/app/template/index.ts
+++ b/src/app/template/index.ts
@@ -1,6 +1,6 @@
 // TODO check used deps
 
-import {minimalizaMenu, panelTools} from './directives/directives';
+import {minimalizaMenu, panelTools, XosNavHandlerService} from './directives/directives';
 export const xosTemplate = 'xosTemplate';
 
 import 'angular-ui-bootstrap';
@@ -26,6 +26,7 @@
     // 'datatables.buttons',       // Datatables Buttons
     // 'ui.tree'                   // Angular ui Tree
   ])
+    .service('XosNavHandler', XosNavHandlerService)
     .directive('minimalizaMenu', minimalizaMenu)
     // .directive('sparkline', sparkline)
     .directive('panelTools', panelTools)