Matteo Scandolo | fb46ae6 | 2017-08-08 09:10:50 -0700 | [diff] [blame] | 1 | |
| 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 Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 19 | import * as $ from 'jquery'; |
| 20 | import * as _ from 'lodash'; |
| 21 | import {IXosSidePanelService} from '../side-panel/side-panel.service'; |
| 22 | |
| 23 | export interface IXosKeyboardShortcutService { |
| 24 | keyMapping: IXosKeyboardShortcutMap; |
| 25 | registerKeyBinding(binding: IXosKeyboardShortcutBinding, target?: string); |
| 26 | setup(): void; |
| 27 | } |
| 28 | |
| 29 | export interface IXosKeyboardShortcutMap { |
| 30 | global: IXosKeyboardShortcutBinding[]; |
| 31 | view: IXosKeyboardShortcutBinding[]; |
| 32 | } |
| 33 | |
| 34 | export interface IXosKeyboardShortcutBinding { |
| 35 | key: string; |
| 36 | cb: any; |
| 37 | modifiers?: string[]; |
Matteo Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 38 | label?: string; |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 39 | description?: string; |
| 40 | onInput?: boolean; |
| 41 | } |
| 42 | |
| 43 | export class XosKeyboardShortcut implements IXosKeyboardShortcutService { |
| 44 | static $inject = ['$log', '$transitions', 'XosSidePanel']; |
| 45 | public keyMapping: IXosKeyboardShortcutMap = { |
| 46 | global: [], |
| 47 | view: [] |
| 48 | }; |
Matteo Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 49 | public allowedModifiers: string[] = ['meta', 'alt', 'shift', 'control']; |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 50 | 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 Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 66 | key: 'slash', |
| 67 | label: '/', |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 68 | description: 'Toggle Shortcut Panel', |
| 69 | cb: this.toggleKeyBindingPanel, |
| 70 | }, |
| 71 | { |
Matteo Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 72 | key: 'esc', |
| 73 | label: 'Esc', |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 74 | 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 Scandolo | a62adbc | 2017-03-02 15:37:34 -0800 | [diff] [blame] | 94 | this.$log.debug(`[XosKeyboardShortcut] Deleting view keys`); |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 95 | this.keyMapping.view = []; |
| 96 | }); |
| 97 | } |
| 98 | |
| 99 | |
| 100 | public setup(): void { |
| 101 | this.$log.info(`[XosKeyboardShortcut] Setup`); |
| 102 | $('body').on('keydown', (e) => { |
| 103 | |
Matteo Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 104 | const pressedKey = this.whatKey(e.which); |
Matteo Scandolo | 0f3692e | 2017-07-10 14:06:41 -0700 | [diff] [blame] | 105 | if (!pressedKey) { |
| 106 | return; |
| 107 | } |
| 108 | |
Max Chu | 17b4115 | 2017-09-07 11:05:01 -0700 | [diff] [blame] | 109 | if (this.allowedModifiers.indexOf(e.key.toLowerCase()) > -1) { |
| 110 | this.addActiveModifierKey(e.key.toLowerCase()); |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 111 | 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 Chu | 17b4115 | 2017-09-07 11:05:01 -0700 | [diff] [blame] | 129 | if (this.allowedModifiers.indexOf(e.key.toLowerCase()) > -1) { |
| 130 | this.removeActiveModifierKey(e.key.toLowerCase()); |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 131 | return; |
| 132 | } |
| 133 | }); |
| 134 | } |
| 135 | |
| 136 | public registerKeyBinding(binding: IXosKeyboardShortcutBinding, target: string = 'view'): void { |
Matteo Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 137 | |
| 138 | if (target !== 'global' && target !== 'view') { |
| 139 | throw new Error('[XosKeyboardShortcut] A shortcut can be registered with scope "global" or "view" only'); |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 140 | } |
Matteo Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 141 | |
| 142 | binding.key = binding.key.toLowerCase(); |
Matteo Scandolo | c8a58c8 | 2017-08-17 17:14:38 -0700 | [diff] [blame] | 143 | if (_.find(this.keyMapping.global, {key: binding.key, modifiers: binding.modifiers}) || _.find(this.keyMapping.view, {key: binding.key, modifiers: binding.modifiers})) { |
Matteo Scandolo | 9b46004 | 2017-04-14 16:24:45 -0700 | [diff] [blame] | 144 | this.$log.warn(`[XosKeyboardShortcut] A shortcut for key "${binding.key}" has already been registered`); |
| 145 | return; |
Matteo Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 146 | } |
| 147 | |
| 148 | this.$log.debug(`[XosKeyboardShortcut] Registering binding for key: ${binding.key}`); |
| 149 | this.keyMapping[target].push(binding); |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 150 | } |
| 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 Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 163 | const globalTargets = _.filter(this.keyMapping.global, {key: key.toLowerCase()}); |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 164 | |
Matteo Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 165 | 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 Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 171 | } |
| 172 | |
Matteo Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 173 | // 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 Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 183 | |
Matteo Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 184 | return targets[0]; |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 185 | } |
| 186 | |
Matteo Scandolo | c817849 | 2017-04-11 17:55:13 -0700 | [diff] [blame] | 187 | 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 Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 224 | } |