blob: 72a59a500a5c13877bd2ee57ddfced6fc1e50a3d [file] [log] [blame]
Matteo Scandolofb46ae62017-08-08 09:10:50 -07001
2/*
3 * Copyright 2017-present Open Networking Foundation
4
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8
9 * http://www.apache.org/licenses/LICENSE-2.0
10
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080019import * as $ from 'jquery';
20import * as _ from 'lodash';
21import {IXosSidePanelService} from '../side-panel/side-panel.service';
22
23export interface IXosKeyboardShortcutService {
24 keyMapping: IXosKeyboardShortcutMap;
25 registerKeyBinding(binding: IXosKeyboardShortcutBinding, target?: string);
26 setup(): void;
27}
28
29export interface IXosKeyboardShortcutMap {
30 global: IXosKeyboardShortcutBinding[];
31 view: IXosKeyboardShortcutBinding[];
32}
33
34export interface IXosKeyboardShortcutBinding {
35 key: string;
36 cb: any;
37 modifiers?: string[];
Matteo Scandoloc8178492017-04-11 17:55:13 -070038 label?: string;
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080039 description?: string;
40 onInput?: boolean;
41}
42
43export class XosKeyboardShortcut implements IXosKeyboardShortcutService {
44 static $inject = ['$log', '$transitions', 'XosSidePanel'];
45 public keyMapping: IXosKeyboardShortcutMap = {
46 global: [],
47 view: []
48 };
Matteo Scandoloc8178492017-04-11 17:55:13 -070049 public allowedModifiers: string[] = ['meta', 'alt', 'shift', 'control'];
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080050 public activeModifiers: string[] = [];
51
52 private toggleKeyBindingPanel = (): void => {
53 if (!this.isPanelOpen) {
54 this.XosSidePanel.injectComponent('xosKeyBindingPanel');
55 this.isPanelOpen = true;
56 }
57 else {
58 this.XosSidePanel.removeInjectedComponents();
59 this.isPanelOpen = false;
60 }
61 };
62
63 /* tslint:disable */
64 public baseBindings: IXosKeyboardShortcutBinding[] = [
65 {
Matteo Scandoloc8178492017-04-11 17:55:13 -070066 key: 'slash',
67 label: '/',
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080068 description: 'Toggle Shortcut Panel',
69 cb: this.toggleKeyBindingPanel,
70 },
71 {
Matteo Scandoloc8178492017-04-11 17:55:13 -070072 key: 'esc',
73 label: 'Esc',
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080074 cb: (event) => {
75 // NOTE removing focus from input elements on Esc
76 event.target.blur();
77 },
78 onInput: true
79 }
80 ];
81 /* tslint:enable */
82
83 private isPanelOpen: boolean;
84
85 constructor(
86 private $log: ng.ILogService,
87 $transitions: any,
88 private XosSidePanel: IXosSidePanelService
89 ) {
90 this.keyMapping.global = this.keyMapping.global.concat(this.baseBindings);
91
92 $transitions.onStart({ to: '**' }, (transtion) => {
93 // delete view keys before that a new view is loaded
Matteo Scandoloa62adbc2017-03-02 15:37:34 -080094 this.$log.debug(`[XosKeyboardShortcut] Deleting view keys`);
Matteo Scandolo5053cbe2017-01-31 17:37:56 -080095 this.keyMapping.view = [];
96 });
97 }
98
99
100 public setup(): void {
101 this.$log.info(`[XosKeyboardShortcut] Setup`);
102 $('body').on('keydown', (e) => {
103
Matteo Scandoloc8178492017-04-11 17:55:13 -0700104 const pressedKey = this.whatKey(e.which);
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800105
Matteo Scandolo0f3692e2017-07-10 14:06:41 -0700106 if (!pressedKey) {
107 return;
108 }
109
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800110 if (this.allowedModifiers.indexOf(e.key) > -1) {
111 this.addActiveModifierKey(e.key);
112 return;
113 }
114
115 // NOTE e.key change if we are using some modifiers (eg: Alt) while getting the value from the keyCode works
116 const binding = this.findBindedShortcut(pressedKey);
117 if (angular.isDefined(binding) && angular.isFunction(binding.cb)) {
118 // NOTE disable binding if they come from an input or textarea
119 // if not different specified
120 const t = e.target.tagName.toLowerCase();
121 if ((t === 'input' || t === 'textarea') && !binding.onInput) {
122 return;
123 }
124 binding.cb(e);
125 e.preventDefault();
126 }
127 });
128
129 $('body').on('keyup', (e) => {
130 if (this.allowedModifiers.indexOf(e.key) > -1) {
131 this.removeActiveModifierKey(e.key);
132 return;
133 }
134 });
135 }
136
137 public registerKeyBinding(binding: IXosKeyboardShortcutBinding, target: string = 'view'): void {
Matteo Scandoloc8178492017-04-11 17:55:13 -0700138
139 if (target !== 'global' && target !== 'view') {
140 throw new Error('[XosKeyboardShortcut] A shortcut can be registered with scope "global" or "view" only');
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800141 }
Matteo Scandoloc8178492017-04-11 17:55:13 -0700142
143 binding.key = binding.key.toLowerCase();
144 if (_.find(this.keyMapping.global, {key: binding.key}) || _.find(this.keyMapping.view, {key: binding.key})) {
Matteo Scandolo9b460042017-04-14 16:24:45 -0700145 this.$log.warn(`[XosKeyboardShortcut] A shortcut for key "${binding.key}" has already been registered`);
146 return;
Matteo Scandoloc8178492017-04-11 17:55:13 -0700147 }
148
149 this.$log.debug(`[XosKeyboardShortcut] Registering binding for key: ${binding.key}`);
150 this.keyMapping[target].push(binding);
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800151 }
152
153 private addActiveModifierKey(key: string) {
154 if (this.activeModifiers.indexOf(key) === -1) {
155 this.activeModifiers.push(key);
156 }
157 }
158
159 private removeActiveModifierKey(key: string) {
160 _.remove(this.activeModifiers, k => k === key);
161 }
162
163 private findBindedShortcut(key: string): IXosKeyboardShortcutBinding {
Matteo Scandoloc8178492017-04-11 17:55:13 -0700164 const globalTargets = _.filter(this.keyMapping.global, {key: key.toLowerCase()});
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800165
Matteo Scandoloc8178492017-04-11 17:55:13 -0700166 const localTargets = _.filter(this.keyMapping.view, {key: key.toLowerCase()});
167
168 let targets = globalTargets.concat(localTargets);
169
170 if (targets.length === 0) {
171 return;
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800172 }
173
Matteo Scandoloc8178492017-04-11 17:55:13 -0700174 // NOTE remove targets that does not match modifiers
175 targets = _.filter(targets, (t: IXosKeyboardShortcutBinding) => {
176 if (this.activeModifiers.length === 0) {
177 return true;
178 }
179 else if (t.modifiers && _.difference(t.modifiers, this.activeModifiers).length === 0) {
180 return true;
181 }
182 return false;
183 });
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800184
Matteo Scandoloc8178492017-04-11 17:55:13 -0700185 return targets[0];
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800186 }
187
Matteo Scandoloc8178492017-04-11 17:55:13 -0700188 private whatKey(code: number) {
189 switch (code) {
190 case 8: return 'delete';
191 case 9: return 'tab';
192 case 13: return 'enter';
193 case 16: return 'shift';
194 case 17: return 'control';
195 case 18: return 'alt';
196 case 27: return 'esc';
197 case 32: return 'space';
198 case 37: return 'leftArrow';
199 case 38: return 'upArrow';
200 case 39: return 'rightArrow';
201 case 40: return 'downArrow';
202 case 91: return 'meta';
203 case 186: return 'semicolon';
204 case 187: return 'equals';
205 case 188: return 'comma';
206 case 189: return 'dash';
207 case 190: return 'dot';
208 case 191: return 'slash';
209 case 192: return 'backQuote';
210 case 219: return 'openBracket';
211 case 220: return 'backSlash';
212 case 221: return 'closeBracket';
213 case 222: return 'quote';
214 default:
215 if ((code >= 48 && code <= 57) ||
216 (code >= 65 && code <= 90)) {
217 return String.fromCharCode(code);
218 } else if (code >= 112 && code <= 123) {
219 return 'F' + (code - 111);
220 }
221 return null;
222 }
223}
224
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800225}