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/header/header.spec.ts b/src/app/core/header/header.spec.ts
index 0bd6877..c1d6062 100644
--- a/src/app/core/header/header.spec.ts
+++ b/src/app/core/header/header.spec.ts
@@ -42,6 +42,10 @@
}
};
+const MockXosKeyboardShortcut = {
+ registerKeyBinding: jasmine.createSpy('registerKeyBinding')
+};
+
describe('header component', () => {
beforeEach(() => {
angular
@@ -52,6 +56,7 @@
.value('toastrConfig', MockToastrConfig)
.value('AuthService', MockAuth)
.value('NavigationService', {})
+ .value('XosKeyboardShortcut', MockXosKeyboardShortcut)
.value('StyleConfig', {
logo: 'cord-logo.png',
})
@@ -77,6 +82,17 @@
expect(header.trim()).not.toBeNull();
});
+ it('should register a keyboard shortcut', () => {
+ expect(MockXosKeyboardShortcut.registerKeyBinding).toHaveBeenCalled();
+ // expect(MockXosKeyboardShortcut.registerKeyBinding).toHaveBeenCalledWith({
+ // key: 'f',
+ // description: 'Select search box',
+ // cb: () => {
+ // $('.navbar-form input').focus();
+ // },
+ // }, 'global');
+ });
+
it('should print user email', () => {
expect($('.profile-address', element).text()).toBe('test@xos.us');
});
diff --git a/src/app/core/header/header.ts b/src/app/core/header/header.ts
index 1f01bf8..8926080 100644
--- a/src/app/core/header/header.ts
+++ b/src/app/core/header/header.ts
@@ -7,13 +7,14 @@
import * as $ from 'jquery';
import {IXosStyleConfig} from '../../../index';
import {IXosSearchService, IXosSearchResult} from '../../datasources/helpers/search.service';
+import {IXosKeyboardShortcutService} from '../services/keyboard-shortcut';
export interface INotification extends IWSEvent {
viewed?: boolean;
}
class HeaderController {
- static $inject = ['$scope', '$rootScope', '$state', 'AuthService', 'SynchronizerStore', 'toastr', 'toastrConfig', 'NavigationService', 'StyleConfig', 'SearchService'];
+ static $inject = ['$scope', '$rootScope', '$state', 'AuthService', 'SynchronizerStore', 'toastr', 'toastrConfig', 'NavigationService', 'StyleConfig', 'SearchService', 'XosKeyboardShortcut'];
public notifications: INotification[] = [];
public newNotifications: INotification[] = [];
public version: string;
@@ -33,7 +34,8 @@
private toastrConfig: ng.toastr.IToastrConfig,
private NavigationService: IXosNavigationService,
private StyleConfig: IXosStyleConfig,
- private SearchService: IXosSearchService
+ private SearchService: IXosSearchService,
+ private XosKeyboardShortcut: IXosKeyboardShortcutService
) {
this.version = require('../../../../package.json').version;
angular.extend(this.toastrConfig, {
@@ -53,11 +55,13 @@
};
// listen for keypress
- $(document).on('keyup', (e) => {
- if (e.key === 'f') {
+ this.XosKeyboardShortcut.registerKeyBinding({
+ key: 'f',
+ description: 'Select search box',
+ cb: () => {
$('.navbar-form input').focus();
- }
- });
+ },
+ }, 'global');
// redirect to selected page
this.routeSelected = (item: IXosSearchResult) => {
diff --git a/src/app/core/index.ts b/src/app/core/index.ts
index 4b89ac8..a31aa96 100644
--- a/src/app/core/index.ts
+++ b/src/app/core/index.ts
@@ -19,6 +19,8 @@
import {xosSidePanel} from './side-panel/side-panel';
import {XosSidePanel} from './side-panel/side-panel.service';
import {XosComponentInjector} from './services/helpers/component-injector.helpers';
+import {XosKeyboardShortcut} from './services/keyboard-shortcut';
+import {xosKeyBindingPanel} from './key-binding/key-binding-panel';
export const xosCore = 'xosCore';
@@ -36,6 +38,7 @@
.service('ConfigHelpers', ConfigHelpers)
.service('ModelSetup', ModelSetup)
.service('XosSidePanel', XosSidePanel)
+ .service('XosKeyboardShortcut', XosKeyboardShortcut)
.service('XosComponentInjector', XosComponentInjector)
.directive('xosLinkWrapper', xosLinkWrapper)
.component('xosHeader', xosHeader)
@@ -47,4 +50,5 @@
.component('xosField', xosField)
.component('xosAlert', xosAlert)
.component('xosValidation', xosValidation)
- .component('xosSidePanel', xosSidePanel);
+ .component('xosSidePanel', xosSidePanel)
+ .component('xosKeyBindingPanel', xosKeyBindingPanel);
diff --git a/src/app/core/key-binding/key-binding-panel.html b/src/app/core/key-binding/key-binding-panel.html
new file mode 100644
index 0000000..7ce982a
--- /dev/null
+++ b/src/app/core/key-binding/key-binding-panel.html
@@ -0,0 +1,25 @@
+<div class="row">
+ <div class="col-xs-12">
+ <h3>Active Key Bindings</h3>
+ </div>
+</div>
+<div class="row" ng-repeat="(k, v) in vm.bindings">
+ <div class="col-xs-12" ng-if="v.length > 0">
+ <h5>{{k | capitalize}}</h5>
+ </div>
+ <div class="col-xs-12" ng-repeat="binding in v" ng-if="binding.description">
+ <div class="row">
+ <div class="col-xs-5">
+ <code class="text-center">
+ <span ng-repeat="m in binding.modifiers">
+ {{m}} +
+ </span>
+ {{binding.key}}
+ </code>
+ </div>
+ <div class="col-xs-7">
+ <p>{{binding.description}}</p>
+ </div>
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/src/app/core/key-binding/key-binding-panel.scss b/src/app/core/key-binding/key-binding-panel.scss
new file mode 100644
index 0000000..0fde571
--- /dev/null
+++ b/src/app/core/key-binding/key-binding-panel.scss
@@ -0,0 +1,7 @@
+@import './../../style/vars.scss';
+
+xos-key-binding-panel {
+ code {
+ background: $background-light-color;
+ }
+}
\ No newline at end of file
diff --git a/src/app/core/key-binding/key-binding-panel.ts b/src/app/core/key-binding/key-binding-panel.ts
new file mode 100644
index 0000000..4ee31eb
--- /dev/null
+++ b/src/app/core/key-binding/key-binding-panel.ts
@@ -0,0 +1,19 @@
+import {IXosKeyboardShortcutService, IXosKeyboardShortcutMap} from '../services/keyboard-shortcut';
+import './key-binding-panel.scss';
+
+class XosKeyBindingPanelController {
+ static $inject = ['$scope', 'XosKeyboardShortcut'];
+ public bindings: IXosKeyboardShortcutMap;
+ constructor (
+ private $scope: ng.IScope,
+ private XosKeyboardShortcut: IXosKeyboardShortcutService
+ ) {
+ this.bindings = this.XosKeyboardShortcut.keyMapping;
+ }
+}
+
+export const xosKeyBindingPanel: angular.IComponentOptions = {
+ template: require('./key-binding-panel.html'),
+ controllerAs: 'vm',
+ controller: XosKeyBindingPanelController
+};
diff --git a/src/app/core/services/helpers/component-injector.helpers.ts b/src/app/core/services/helpers/component-injector.helpers.ts
index e70e257..908e4c7 100644
--- a/src/app/core/services/helpers/component-injector.helpers.ts
+++ b/src/app/core/services/helpers/component-injector.helpers.ts
@@ -16,7 +16,7 @@
}
export class XosComponentInjector implements IXosComponentInjectorService {
- static $inject = ['$rootScope', '$compile', '$transitions', '$log'];
+ static $inject = ['$rootScope', '$compile', '$transitions', '$log', '$timeout'];
public injectedComponents: IXosInjectedComponent[] = [];
@@ -24,7 +24,8 @@
private $rootScope: ng.IRootScopeService,
private $compile: ng.ICompileService,
private $transitions: any,
- private $log: ng.ILogService
+ private $log: ng.ILogService,
+ private $timeout: ng.ITimeoutService
) {
$transitions.onFinish({ to: '**' }, (transtion) => {
// wait for route transition to complete
@@ -79,6 +80,10 @@
const componentTag = `<${componentTagName} ${attr}>${transclude || ''}</${componentTagName}>`;
const element = this.$compile(componentTag)(scope);
+ this.$timeout(function() {
+ scope.$apply();
+ });
+
targetEl.append(element);
// store a reference for the element
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;
+ }
+
+}
diff --git a/src/app/core/side-panel/side-panel.service.ts b/src/app/core/side-panel/side-panel.service.ts
index a7471f5..96e4162 100644
--- a/src/app/core/side-panel/side-panel.service.ts
+++ b/src/app/core/side-panel/side-panel.service.ts
@@ -5,10 +5,11 @@
open(): void;
close(): void;
injectComponent(componentName: string, attributes?: any, transclude?: string): void;
+ removeInjectedComponents(): void;
}
export class XosSidePanel implements IXosSidePanelService {
- static $inject = ['$rootScope', '$compile', 'XosComponentInjector'];
+ static $inject = ['$rootScope', '$compile', '$timeout', 'XosComponentInjector'];
public sidePanelElName = 'xos-side-panel';
public sidePanelElClass = '.xos-side-panel';
public sidePanelEl: JQuery;
@@ -16,6 +17,7 @@
constructor (
private $rootScope: ng.IRootScopeService,
private $compile: ng.ICompileService,
+ private $timeout: ng.ITimeoutService,
private XosComponentInjector: IXosComponentInjectorService
) {
this.sidePanelEl = $(`${this.sidePanelElName} > ${this.sidePanelElClass}`);
@@ -33,4 +35,11 @@
this.XosComponentInjector.injectComponent('#side-panel-container', componentName, attributes, transclude, true);
this.open();
}
+
+ public removeInjectedComponents() {
+ this.close();
+ this.$timeout(() => {
+ this.XosComponentInjector.removeInjectedComponents('#side-panel-container');
+ }, 500);
+ }
}