CORD-582, CORD-734 Registering events listeners for keyboard shortcuts
and displaying them in the side panel

Change-Id: Ifbb227b3a425be5c33d1fe211abd473209414896
diff --git a/src/app/core/services/keyboard-shortcut.ts b/src/app/core/services/keyboard-shortcut.ts
new file mode 100644
index 0000000..e649abf
--- /dev/null
+++ b/src/app/core/services/keyboard-shortcut.ts
@@ -0,0 +1,164 @@
+import * as $ from 'jquery';
+import * as _ from 'lodash';
+import {IXosSidePanelService} from '../side-panel/side-panel.service';
+
+export interface IXosKeyboardShortcutService {
+  keyMapping: IXosKeyboardShortcutMap;
+  registerKeyBinding(binding: IXosKeyboardShortcutBinding, target?: string);
+  setup(): void;
+}
+
+export interface IXosKeyboardShortcutMap {
+  global: IXosKeyboardShortcutBinding[];
+  view: IXosKeyboardShortcutBinding[];
+}
+
+export interface IXosKeyboardShortcutBinding {
+  key: string;
+  cb: any;
+  modifiers?: string[];
+  description?: string;
+  onInput?: boolean;
+}
+
+export class XosKeyboardShortcut implements IXosKeyboardShortcutService {
+  static $inject = ['$log', '$transitions', 'XosSidePanel'];
+  public keyMapping: IXosKeyboardShortcutMap = {
+    global: [],
+    view: []
+  };
+  public allowedModifiers: string[] = ['Meta', 'Alt', 'Shift', 'Control'];
+  public activeModifiers: string[] = [];
+
+  private toggleKeyBindingPanel = (): void => {
+    if (!this.isPanelOpen) {
+      this.XosSidePanel.injectComponent('xosKeyBindingPanel');
+      this.isPanelOpen = true;
+    }
+    else {
+      this.XosSidePanel.removeInjectedComponents();
+      this.isPanelOpen = false;
+    }
+  };
+
+  /* tslint:disable */
+  public baseBindings: IXosKeyboardShortcutBinding[] = [
+    {
+      key: '?',
+      description: 'Toggle Shortcut Panel',
+      cb: this.toggleKeyBindingPanel,
+    },
+    {
+      key: '/',
+      description: 'Toggle Shortcut Panel',
+      cb: this.toggleKeyBindingPanel,
+    },
+    {
+      key: 'Escape',
+      cb: (event) => {
+        // NOTE removing focus from input elements on Esc
+        event.target.blur();
+      },
+      onInput: true
+    }
+  ];
+  /* tslint:enable */
+
+  private isPanelOpen: boolean;
+
+  constructor(
+    private $log: ng.ILogService,
+    $transitions: any,
+    private XosSidePanel: IXosSidePanelService
+  ) {
+    this.keyMapping.global = this.keyMapping.global.concat(this.baseBindings);
+
+    $transitions.onStart({ to: '**' }, (transtion) => {
+      // delete view keys before that a new view is loaded
+        this.$log.info(`[XosKeyboardShortcut] Deleting view keys`);
+        this.keyMapping.view = [];
+    });
+  }
+
+
+  public setup(): void {
+    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;
+      }
+
+      if (this.allowedModifiers.indexOf(e.key) > -1) {
+        this.addActiveModifierKey(e.key);
+        return;
+      }
+
+      // NOTE e.key change if we are using some modifiers (eg: Alt) while getting the value from the keyCode works
+      const binding = this.findBindedShortcut(pressedKey);
+      if (angular.isDefined(binding) && angular.isFunction(binding.cb)) {
+        // NOTE disable binding if they come from an input or textarea
+        // if not different specified
+        const t = e.target.tagName.toLowerCase();
+        if ((t === 'input' || t === 'textarea') && !binding.onInput) {
+          return;
+        }
+        binding.cb(e);
+        e.preventDefault();
+      }
+    });
+
+    $('body').on('keyup', (e) => {
+      if (this.allowedModifiers.indexOf(e.key) > -1) {
+        this.removeActiveModifierKey(e.key);
+        return;
+      }
+    });
+  }
+
+  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.info(`[XosKeyboardShortcut] Registering binding for key: ${binding.key}`);
+    if (!_.find(this.keyMapping[target], {key: binding.key})) {
+      this.keyMapping[target].push(binding);
+    }
+  }
+
+  private addActiveModifierKey(key: string) {
+    if (this.activeModifiers.indexOf(key) === -1) {
+      this.activeModifiers.push(key);
+    }
+  }
+
+  private removeActiveModifierKey(key: string) {
+    _.remove(this.activeModifiers, k => k === key);
+  }
+
+  private findBindedShortcut(key: string): IXosKeyboardShortcutBinding {
+    // NOTE search for binding in the global map
+    let target =  _.find(this.keyMapping.global, {key: key});
+
+    // NOTE if it is not there look in the view map
+    if (!angular.isDefined(target)) {
+      target = _.find(this.keyMapping.view, {key: key});
+    }
+
+
+    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;
+  }
+
+}