blob: 35af07edec0a4eea54a93ff393a2927412e99216 [file] [log] [blame]
Matteo Scandolo5053cbe2017-01-31 17:37:56 -08001import * as $ from 'jquery';
2import * as _ from 'lodash';
3import {IXosSidePanelService} from '../side-panel/side-panel.service';
4
5export interface IXosKeyboardShortcutService {
6 keyMapping: IXosKeyboardShortcutMap;
7 registerKeyBinding(binding: IXosKeyboardShortcutBinding, target?: string);
8 setup(): void;
9}
10
11export interface IXosKeyboardShortcutMap {
12 global: IXosKeyboardShortcutBinding[];
13 view: IXosKeyboardShortcutBinding[];
14}
15
16export interface IXosKeyboardShortcutBinding {
17 key: string;
18 cb: any;
19 modifiers?: string[];
Matteo Scandoloc8178492017-04-11 17:55:13 -070020 label?: string;
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080021 description?: string;
22 onInput?: boolean;
23}
24
25export class XosKeyboardShortcut implements IXosKeyboardShortcutService {
26 static $inject = ['$log', '$transitions', 'XosSidePanel'];
27 public keyMapping: IXosKeyboardShortcutMap = {
28 global: [],
29 view: []
30 };
Matteo Scandoloc8178492017-04-11 17:55:13 -070031 public allowedModifiers: string[] = ['meta', 'alt', 'shift', 'control'];
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080032 public activeModifiers: string[] = [];
33
34 private toggleKeyBindingPanel = (): void => {
35 if (!this.isPanelOpen) {
36 this.XosSidePanel.injectComponent('xosKeyBindingPanel');
37 this.isPanelOpen = true;
38 }
39 else {
40 this.XosSidePanel.removeInjectedComponents();
41 this.isPanelOpen = false;
42 }
43 };
44
45 /* tslint:disable */
46 public baseBindings: IXosKeyboardShortcutBinding[] = [
47 {
Matteo Scandoloc8178492017-04-11 17:55:13 -070048 key: 'slash',
49 label: '/',
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080050 description: 'Toggle Shortcut Panel',
51 cb: this.toggleKeyBindingPanel,
52 },
53 {
Matteo Scandoloc8178492017-04-11 17:55:13 -070054 key: 'esc',
55 label: 'Esc',
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080056 cb: (event) => {
57 // NOTE removing focus from input elements on Esc
58 event.target.blur();
59 },
60 onInput: true
61 }
62 ];
63 /* tslint:enable */
64
65 private isPanelOpen: boolean;
66
67 constructor(
68 private $log: ng.ILogService,
69 $transitions: any,
70 private XosSidePanel: IXosSidePanelService
71 ) {
72 this.keyMapping.global = this.keyMapping.global.concat(this.baseBindings);
73
74 $transitions.onStart({ to: '**' }, (transtion) => {
75 // delete view keys before that a new view is loaded
Matteo Scandoloa62adbc2017-03-02 15:37:34 -080076 this.$log.debug(`[XosKeyboardShortcut] Deleting view keys`);
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080077 this.keyMapping.view = [];
78 });
79 }
80
81
82 public setup(): void {
83 this.$log.info(`[XosKeyboardShortcut] Setup`);
84 $('body').on('keydown', (e) => {
85
Matteo Scandoloc8178492017-04-11 17:55:13 -070086 const pressedKey = this.whatKey(e.which);
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080087
88 if (this.allowedModifiers.indexOf(e.key) > -1) {
89 this.addActiveModifierKey(e.key);
90 return;
91 }
92
93 // NOTE e.key change if we are using some modifiers (eg: Alt) while getting the value from the keyCode works
94 const binding = this.findBindedShortcut(pressedKey);
95 if (angular.isDefined(binding) && angular.isFunction(binding.cb)) {
96 // NOTE disable binding if they come from an input or textarea
97 // if not different specified
98 const t = e.target.tagName.toLowerCase();
99 if ((t === 'input' || t === 'textarea') && !binding.onInput) {
100 return;
101 }
102 binding.cb(e);
103 e.preventDefault();
104 }
105 });
106
107 $('body').on('keyup', (e) => {
108 if (this.allowedModifiers.indexOf(e.key) > -1) {
109 this.removeActiveModifierKey(e.key);
110 return;
111 }
112 });
113 }
114
115 public registerKeyBinding(binding: IXosKeyboardShortcutBinding, target: string = 'view'): void {
Matteo Scandoloc8178492017-04-11 17:55:13 -0700116
117 if (target !== 'global' && target !== 'view') {
118 throw new Error('[XosKeyboardShortcut] A shortcut can be registered with scope "global" or "view" only');
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800119 }
Matteo Scandoloc8178492017-04-11 17:55:13 -0700120
121 binding.key = binding.key.toLowerCase();
122 if (_.find(this.keyMapping.global, {key: binding.key}) || _.find(this.keyMapping.view, {key: binding.key})) {
Matteo Scandolo9b460042017-04-14 16:24:45 -0700123 this.$log.warn(`[XosKeyboardShortcut] A shortcut for key "${binding.key}" has already been registered`);
124 return;
Matteo Scandoloc8178492017-04-11 17:55:13 -0700125 }
126
127 this.$log.debug(`[XosKeyboardShortcut] Registering binding for key: ${binding.key}`);
128 this.keyMapping[target].push(binding);
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800129 }
130
131 private addActiveModifierKey(key: string) {
132 if (this.activeModifiers.indexOf(key) === -1) {
133 this.activeModifiers.push(key);
134 }
135 }
136
137 private removeActiveModifierKey(key: string) {
138 _.remove(this.activeModifiers, k => k === key);
139 }
140
141 private findBindedShortcut(key: string): IXosKeyboardShortcutBinding {
Matteo Scandoloc8178492017-04-11 17:55:13 -0700142 const globalTargets = _.filter(this.keyMapping.global, {key: key.toLowerCase()});
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800143
Matteo Scandoloc8178492017-04-11 17:55:13 -0700144 const localTargets = _.filter(this.keyMapping.view, {key: key.toLowerCase()});
145
146 let targets = globalTargets.concat(localTargets);
147
148 if (targets.length === 0) {
149 return;
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800150 }
151
Matteo Scandoloc8178492017-04-11 17:55:13 -0700152 // NOTE remove targets that does not match modifiers
153 targets = _.filter(targets, (t: IXosKeyboardShortcutBinding) => {
154 if (this.activeModifiers.length === 0) {
155 return true;
156 }
157 else if (t.modifiers && _.difference(t.modifiers, this.activeModifiers).length === 0) {
158 return true;
159 }
160 return false;
161 });
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800162
Matteo Scandoloc8178492017-04-11 17:55:13 -0700163 return targets[0];
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800164 }
165
Matteo Scandoloc8178492017-04-11 17:55:13 -0700166 private whatKey(code: number) {
167 switch (code) {
168 case 8: return 'delete';
169 case 9: return 'tab';
170 case 13: return 'enter';
171 case 16: return 'shift';
172 case 17: return 'control';
173 case 18: return 'alt';
174 case 27: return 'esc';
175 case 32: return 'space';
176 case 37: return 'leftArrow';
177 case 38: return 'upArrow';
178 case 39: return 'rightArrow';
179 case 40: return 'downArrow';
180 case 91: return 'meta';
181 case 186: return 'semicolon';
182 case 187: return 'equals';
183 case 188: return 'comma';
184 case 189: return 'dash';
185 case 190: return 'dot';
186 case 191: return 'slash';
187 case 192: return 'backQuote';
188 case 219: return 'openBracket';
189 case 220: return 'backSlash';
190 case 221: return 'closeBracket';
191 case 222: return 'quote';
192 default:
193 if ((code >= 48 && code <= 57) ||
194 (code >= 65 && code <= 90)) {
195 return String.fromCharCode(code);
196 } else if (code >= 112 && code <= 123) {
197 return 'F' + (code - 111);
198 }
199 return null;
200 }
201}
202
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800203}