blob: 9b98b458da3df650a90ca71a9eb384116ca5156e [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 Scandolo0f3692e2017-07-10 14:06:41 -0700105 if (!pressedKey) {
106 return;
107 }
108
Max Chu3e2f13b2017-09-07 11:05:01 -0700109 if (this.allowedModifiers.indexOf(e.key.toLowerCase()) > -1) {
110 this.addActiveModifierKey(e.key.toLowerCase());
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800111 return;
112 }
113
114 // NOTE e.key change if we are using some modifiers (eg: Alt) while getting the value from the keyCode works
115 const binding = this.findBindedShortcut(pressedKey);
116 if (angular.isDefined(binding) && angular.isFunction(binding.cb)) {
117 // NOTE disable binding if they come from an input or textarea
118 // if not different specified
119 const t = e.target.tagName.toLowerCase();
120 if ((t === 'input' || t === 'textarea') && !binding.onInput) {
121 return;
122 }
123 binding.cb(e);
124 e.preventDefault();
125 }
126 });
127
128 $('body').on('keyup', (e) => {
Max Chu3e2f13b2017-09-07 11:05:01 -0700129 if (this.allowedModifiers.indexOf(e.key.toLowerCase()) > -1) {
130 this.removeActiveModifierKey(e.key.toLowerCase());
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800131 return;
132 }
133 });
134 }
135
136 public registerKeyBinding(binding: IXosKeyboardShortcutBinding, target: string = 'view'): void {
Matteo Scandoloc8178492017-04-11 17:55:13 -0700137
138 if (target !== 'global' && target !== 'view') {
139 throw new Error('[XosKeyboardShortcut] A shortcut can be registered with scope "global" or "view" only');
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800140 }
Matteo Scandoloc8178492017-04-11 17:55:13 -0700141
142 binding.key = binding.key.toLowerCase();
Matteo Scandoloc8a58c82017-08-17 17:14:38 -0700143 if (_.find(this.keyMapping.global, {key: binding.key, modifiers: binding.modifiers}) || _.find(this.keyMapping.view, {key: binding.key, modifiers: binding.modifiers})) {
Matteo Scandolo9b460042017-04-14 16:24:45 -0700144 this.$log.warn(`[XosKeyboardShortcut] A shortcut for key "${binding.key}" has already been registered`);
145 return;
Matteo Scandoloc8178492017-04-11 17:55:13 -0700146 }
147
148 this.$log.debug(`[XosKeyboardShortcut] Registering binding for key: ${binding.key}`);
149 this.keyMapping[target].push(binding);
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800150 }
151
152 private addActiveModifierKey(key: string) {
153 if (this.activeModifiers.indexOf(key) === -1) {
154 this.activeModifiers.push(key);
155 }
156 }
157
158 private removeActiveModifierKey(key: string) {
159 _.remove(this.activeModifiers, k => k === key);
160 }
161
162 private findBindedShortcut(key: string): IXosKeyboardShortcutBinding {
Matteo Scandoloc8178492017-04-11 17:55:13 -0700163 const globalTargets = _.filter(this.keyMapping.global, {key: key.toLowerCase()});
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800164
Matteo Scandoloc8178492017-04-11 17:55:13 -0700165 const localTargets = _.filter(this.keyMapping.view, {key: key.toLowerCase()});
166
167 let targets = globalTargets.concat(localTargets);
168
169 if (targets.length === 0) {
170 return;
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800171 }
172
Matteo Scandoloc8178492017-04-11 17:55:13 -0700173 // NOTE remove targets that does not match modifiers
174 targets = _.filter(targets, (t: IXosKeyboardShortcutBinding) => {
175 if (this.activeModifiers.length === 0) {
176 return true;
177 }
178 else if (t.modifiers && _.difference(t.modifiers, this.activeModifiers).length === 0) {
179 return true;
180 }
181 return false;
182 });
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800183
Matteo Scandoloc8178492017-04-11 17:55:13 -0700184 return targets[0];
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800185 }
186
Matteo Scandoloc8178492017-04-11 17:55:13 -0700187 private whatKey(code: number) {
188 switch (code) {
189 case 8: return 'delete';
190 case 9: return 'tab';
191 case 13: return 'enter';
192 case 16: return 'shift';
193 case 17: return 'control';
194 case 18: return 'alt';
195 case 27: return 'esc';
196 case 32: return 'space';
197 case 37: return 'leftArrow';
198 case 38: return 'upArrow';
199 case 39: return 'rightArrow';
200 case 40: return 'downArrow';
201 case 91: return 'meta';
202 case 186: return 'semicolon';
203 case 187: return 'equals';
204 case 188: return 'comma';
205 case 189: return 'dash';
206 case 190: return 'dot';
207 case 191: return 'slash';
208 case 192: return 'backQuote';
209 case 219: return 'openBracket';
210 case 220: return 'backSlash';
211 case 221: return 'closeBracket';
212 case 222: return 'quote';
213 default:
214 if ((code >= 48 && code <= 57) ||
215 (code >= 65 && code <= 90)) {
216 return String.fromCharCode(code);
217 } else if (code >= 112 && code <= 123) {
218 return 'F' + (code - 111);
219 }
220 return null;
221 }
222}
223
Matteo Scandolo5053cbe2017-01-31 17:37:56 -0800224}