Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 1 | import * as $ from 'jquery'; |
| 2 | import * as _ from 'lodash'; |
| 3 | import {IXosSidePanelService} from '../side-panel/side-panel.service'; |
| 4 | |
| 5 | export interface IXosKeyboardShortcutService { |
| 6 | keyMapping: IXosKeyboardShortcutMap; |
| 7 | registerKeyBinding(binding: IXosKeyboardShortcutBinding, target?: string); |
| 8 | setup(): void; |
| 9 | } |
| 10 | |
| 11 | export interface IXosKeyboardShortcutMap { |
| 12 | global: IXosKeyboardShortcutBinding[]; |
| 13 | view: IXosKeyboardShortcutBinding[]; |
| 14 | } |
| 15 | |
| 16 | export interface IXosKeyboardShortcutBinding { |
| 17 | key: string; |
| 18 | cb: any; |
| 19 | modifiers?: string[]; |
| 20 | description?: string; |
| 21 | onInput?: boolean; |
| 22 | } |
| 23 | |
| 24 | export class XosKeyboardShortcut implements IXosKeyboardShortcutService { |
| 25 | static $inject = ['$log', '$transitions', 'XosSidePanel']; |
| 26 | public keyMapping: IXosKeyboardShortcutMap = { |
| 27 | global: [], |
| 28 | view: [] |
| 29 | }; |
| 30 | public allowedModifiers: string[] = ['Meta', 'Alt', 'Shift', 'Control']; |
| 31 | public activeModifiers: string[] = []; |
| 32 | |
| 33 | private toggleKeyBindingPanel = (): void => { |
| 34 | if (!this.isPanelOpen) { |
| 35 | this.XosSidePanel.injectComponent('xosKeyBindingPanel'); |
| 36 | this.isPanelOpen = true; |
| 37 | } |
| 38 | else { |
| 39 | this.XosSidePanel.removeInjectedComponents(); |
| 40 | this.isPanelOpen = false; |
| 41 | } |
| 42 | }; |
| 43 | |
| 44 | /* tslint:disable */ |
| 45 | public baseBindings: IXosKeyboardShortcutBinding[] = [ |
| 46 | { |
| 47 | key: '?', |
| 48 | description: 'Toggle Shortcut Panel', |
| 49 | cb: this.toggleKeyBindingPanel, |
| 50 | }, |
| 51 | { |
| 52 | key: '/', |
| 53 | description: 'Toggle Shortcut Panel', |
| 54 | cb: this.toggleKeyBindingPanel, |
| 55 | }, |
| 56 | { |
| 57 | key: 'Escape', |
| 58 | cb: (event) => { |
| 59 | // NOTE removing focus from input elements on Esc |
| 60 | event.target.blur(); |
| 61 | }, |
| 62 | onInput: true |
| 63 | } |
| 64 | ]; |
| 65 | /* tslint:enable */ |
| 66 | |
| 67 | private isPanelOpen: boolean; |
| 68 | |
| 69 | constructor( |
| 70 | private $log: ng.ILogService, |
| 71 | $transitions: any, |
| 72 | private XosSidePanel: IXosSidePanelService |
| 73 | ) { |
| 74 | this.keyMapping.global = this.keyMapping.global.concat(this.baseBindings); |
| 75 | |
| 76 | $transitions.onStart({ to: '**' }, (transtion) => { |
| 77 | // delete view keys before that a new view is loaded |
Matteo Scandolo | a62adbc | 2017-03-02 15:37:34 -0800 | [diff] [blame] | 78 | this.$log.debug(`[XosKeyboardShortcut] Deleting view keys`); |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 79 | this.keyMapping.view = []; |
| 80 | }); |
| 81 | } |
| 82 | |
| 83 | |
| 84 | public setup(): void { |
| 85 | this.$log.info(`[XosKeyboardShortcut] Setup`); |
| 86 | $('body').on('keydown', (e) => { |
| 87 | |
| 88 | let pressedKey = null; |
| 89 | |
| 90 | if (e.key.length === 1 && e.key.match(/[a-z]/i) || String.fromCharCode(e.keyCode).toLowerCase().match(/[a-z]/i)) { |
| 91 | // alphabet letters found |
| 92 | pressedKey = String.fromCharCode(e.keyCode).toLowerCase(); |
| 93 | } |
| 94 | else { |
| 95 | pressedKey = e.key; |
| 96 | } |
| 97 | |
| 98 | if (this.allowedModifiers.indexOf(e.key) > -1) { |
| 99 | this.addActiveModifierKey(e.key); |
| 100 | return; |
| 101 | } |
| 102 | |
| 103 | // NOTE e.key change if we are using some modifiers (eg: Alt) while getting the value from the keyCode works |
| 104 | const binding = this.findBindedShortcut(pressedKey); |
| 105 | if (angular.isDefined(binding) && angular.isFunction(binding.cb)) { |
| 106 | // NOTE disable binding if they come from an input or textarea |
| 107 | // if not different specified |
| 108 | const t = e.target.tagName.toLowerCase(); |
| 109 | if ((t === 'input' || t === 'textarea') && !binding.onInput) { |
| 110 | return; |
| 111 | } |
| 112 | binding.cb(e); |
| 113 | e.preventDefault(); |
| 114 | } |
| 115 | }); |
| 116 | |
| 117 | $('body').on('keyup', (e) => { |
| 118 | if (this.allowedModifiers.indexOf(e.key) > -1) { |
| 119 | this.removeActiveModifierKey(e.key); |
| 120 | return; |
| 121 | } |
| 122 | }); |
| 123 | } |
| 124 | |
| 125 | public registerKeyBinding(binding: IXosKeyboardShortcutBinding, target: string = 'view'): void { |
| 126 | // NOTE check for already taken binding (by key) |
| 127 | // NOTE check target is either 'view' or 'global' |
Matteo Scandolo | a62adbc | 2017-03-02 15:37:34 -0800 | [diff] [blame] | 128 | this.$log.debug(`[XosKeyboardShortcut] Registering binding for key: ${binding.key}`); |
Matteo Scandolo | 5053cbe | 2017-01-31 17:37:56 -0800 | [diff] [blame] | 129 | if (!_.find(this.keyMapping[target], {key: binding.key})) { |
| 130 | this.keyMapping[target].push(binding); |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | private addActiveModifierKey(key: string) { |
| 135 | if (this.activeModifiers.indexOf(key) === -1) { |
| 136 | this.activeModifiers.push(key); |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | private removeActiveModifierKey(key: string) { |
| 141 | _.remove(this.activeModifiers, k => k === key); |
| 142 | } |
| 143 | |
| 144 | private findBindedShortcut(key: string): IXosKeyboardShortcutBinding { |
| 145 | // NOTE search for binding in the global map |
| 146 | let target = _.find(this.keyMapping.global, {key: key}); |
| 147 | |
| 148 | // NOTE if it is not there look in the view map |
| 149 | if (!angular.isDefined(target)) { |
| 150 | target = _.find(this.keyMapping.view, {key: key}); |
| 151 | } |
| 152 | |
| 153 | |
| 154 | if (target && target.modifiers) { |
| 155 | // if not all the modifier keys for that binding are pressed |
| 156 | if (_.difference(target.modifiers, this.activeModifiers).length > 0) { |
| 157 | // do not match |
| 158 | return; |
| 159 | }; |
| 160 | } |
| 161 | return target; |
| 162 | } |
| 163 | |
| 164 | } |