blob: 1907b0dce998270b0813284ad872807e3e166cee [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
Matteo Scandolo0f3692e2017-07-10 14:06:41 -070088 if (!pressedKey) {
89 return;
90 }
91
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080092 if (this.allowedModifiers.indexOf(e.key) > -1) {
93 this.addActiveModifierKey(e.key);
94 return;
95 }
96
97 // NOTE e.key change if we are using some modifiers (eg: Alt) while getting the value from the keyCode works
98 const binding = this.findBindedShortcut(pressedKey);
99 if (angular.isDefined(binding) && angular.isFunction(binding.cb)) {
100 // NOTE disable binding if they come from an input or textarea
101 // if not different specified
102 const t = e.target.tagName.toLowerCase();
103 if ((t === 'input' || t === 'textarea') && !binding.onInput) {
104 return;
105 }
106 binding.cb(e);
107 e.preventDefault();
108 }
109 });
110
111 $('body').on('keyup', (e) => {
112 if (this.allowedModifiers.indexOf(e.key) > -1) {
113 this.removeActiveModifierKey(e.key);
114 return;
115 }
116 });
117 }
118
119 public registerKeyBinding(binding: IXosKeyboardShortcutBinding, target: string = 'view'): void {
Matteo Scandoloc8178492017-04-11 17:55:13 -0700120
121 if (target !== 'global' && target !== 'view') {
122 throw new Error('[XosKeyboardShortcut] A shortcut can be registered with scope "global" or "view" only');
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800123 }
Matteo Scandoloc8178492017-04-11 17:55:13 -0700124
125 binding.key = binding.key.toLowerCase();
126 if (_.find(this.keyMapping.global, {key: binding.key}) || _.find(this.keyMapping.view, {key: binding.key})) {
Matteo Scandolo9b460042017-04-14 16:24:45 -0700127 this.$log.warn(`[XosKeyboardShortcut] A shortcut for key "${binding.key}" has already been registered`);
128 return;
Matteo Scandoloc8178492017-04-11 17:55:13 -0700129 }
130
131 this.$log.debug(`[XosKeyboardShortcut] Registering binding for key: ${binding.key}`);
132 this.keyMapping[target].push(binding);
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800133 }
134
135 private addActiveModifierKey(key: string) {
136 if (this.activeModifiers.indexOf(key) === -1) {
137 this.activeModifiers.push(key);
138 }
139 }
140
141 private removeActiveModifierKey(key: string) {
142 _.remove(this.activeModifiers, k => k === key);
143 }
144
145 private findBindedShortcut(key: string): IXosKeyboardShortcutBinding {
Matteo Scandoloc8178492017-04-11 17:55:13 -0700146 const globalTargets = _.filter(this.keyMapping.global, {key: key.toLowerCase()});
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800147
Matteo Scandoloc8178492017-04-11 17:55:13 -0700148 const localTargets = _.filter(this.keyMapping.view, {key: key.toLowerCase()});
149
150 let targets = globalTargets.concat(localTargets);
151
152 if (targets.length === 0) {
153 return;
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800154 }
155
Matteo Scandoloc8178492017-04-11 17:55:13 -0700156 // NOTE remove targets that does not match modifiers
157 targets = _.filter(targets, (t: IXosKeyboardShortcutBinding) => {
158 if (this.activeModifiers.length === 0) {
159 return true;
160 }
161 else if (t.modifiers && _.difference(t.modifiers, this.activeModifiers).length === 0) {
162 return true;
163 }
164 return false;
165 });
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800166
Matteo Scandoloc8178492017-04-11 17:55:13 -0700167 return targets[0];
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800168 }
169
Matteo Scandoloc8178492017-04-11 17:55:13 -0700170 private whatKey(code: number) {
171 switch (code) {
172 case 8: return 'delete';
173 case 9: return 'tab';
174 case 13: return 'enter';
175 case 16: return 'shift';
176 case 17: return 'control';
177 case 18: return 'alt';
178 case 27: return 'esc';
179 case 32: return 'space';
180 case 37: return 'leftArrow';
181 case 38: return 'upArrow';
182 case 39: return 'rightArrow';
183 case 40: return 'downArrow';
184 case 91: return 'meta';
185 case 186: return 'semicolon';
186 case 187: return 'equals';
187 case 188: return 'comma';
188 case 189: return 'dash';
189 case 190: return 'dot';
190 case 191: return 'slash';
191 case 192: return 'backQuote';
192 case 219: return 'openBracket';
193 case 220: return 'backSlash';
194 case 221: return 'closeBracket';
195 case 222: return 'quote';
196 default:
197 if ((code >= 48 && code <= 57) ||
198 (code >= 65 && code <= 90)) {
199 return String.fromCharCode(code);
200 } else if (code >= 112 && code <= 123) {
201 return 'F' + (code - 111);
202 }
203 return null;
204 }
205}
206
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800207}