2016-09-23 23:17:25 +02:00
|
|
|
/* -*- Mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
|
|
|
|
|
|
(function() {
|
|
|
|
/* jshint validthis: true */
|
|
|
|
'use strict';
|
2016-09-27 21:19:24 +02:00
|
|
|
|
2016-09-23 23:17:25 +02:00
|
|
|
/**
|
|
|
|
* $sgHotkeys - A service to associate keyboard shortcuts to actions.
|
|
|
|
* @memberof SOGo.Common
|
|
|
|
*
|
|
|
|
* @description
|
|
|
|
* This service is a modified version of angular-hotkeys-light by Eugene Brodsky:
|
|
|
|
* https://github.com/fupslot/angular-hotkeys-light
|
|
|
|
*/
|
|
|
|
function $sgHotkeys() {
|
|
|
|
|
|
|
|
// Key-code values for various meta-keys.
|
|
|
|
// Source : http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
|
|
|
|
// http://unixpapa.com/js/key.html
|
|
|
|
// Date: Oct 02, 2015.
|
|
|
|
var KEY_CODES = {
|
|
|
|
8: 'backspace',
|
|
|
|
9: 'tab',
|
|
|
|
13: 'enter',
|
|
|
|
16: 'shift',
|
|
|
|
17: 'ctrl',
|
|
|
|
18: 'alt',
|
|
|
|
19: 'pause',
|
|
|
|
20: 'caps',
|
|
|
|
27: 'escape',
|
|
|
|
32: 'space',
|
|
|
|
33: 'pageup',
|
|
|
|
34: 'pagedown',
|
|
|
|
35: 'end',
|
|
|
|
36: 'home',
|
|
|
|
37: 'left',
|
|
|
|
38: 'up',
|
|
|
|
39: 'right',
|
|
|
|
40: 'down',
|
|
|
|
45: 'insert',
|
|
|
|
46: 'delete',
|
|
|
|
// Numpad
|
|
|
|
96: '0',
|
|
|
|
97: '1',
|
|
|
|
98: '2',
|
|
|
|
99: '3',
|
|
|
|
100: '4',
|
|
|
|
101: '5',
|
|
|
|
102: '6',
|
|
|
|
103: '7',
|
|
|
|
104: '8',
|
|
|
|
105: '9',
|
|
|
|
106: '*',
|
|
|
|
107: '+',
|
|
|
|
109: '-',
|
|
|
|
110: '.',
|
|
|
|
111: '/',
|
|
|
|
// Function keys
|
|
|
|
112: 'f1',
|
|
|
|
113: 'f2',
|
|
|
|
114: 'f3',
|
|
|
|
115: 'f4',
|
|
|
|
116: 'f5',
|
|
|
|
117: 'f6',
|
|
|
|
118: 'f7',
|
|
|
|
119: 'f8',
|
|
|
|
120: 'f9',
|
|
|
|
121: 'f10',
|
|
|
|
122: 'f11',
|
|
|
|
123: 'f12'
|
|
|
|
};
|
2016-09-27 21:19:24 +02:00
|
|
|
// Char-code values for characters that require a key combinations
|
|
|
|
var CHAR_CODES = {
|
2016-10-17 21:38:13 +02:00
|
|
|
42: '*',
|
2016-09-27 21:19:24 +02:00
|
|
|
63: '?'
|
|
|
|
};
|
2016-09-23 23:17:25 +02:00
|
|
|
|
|
|
|
this.$get = getService;
|
|
|
|
|
|
|
|
getService.$inject = ['$rootScope', '$window'];
|
|
|
|
function getService($rootScope, $window) {
|
|
|
|
|
|
|
|
var wrapWithApply = function (fn) {
|
|
|
|
return function(event, args) {
|
|
|
|
$rootScope.$apply(function() {
|
|
|
|
fn.call(this, event, args);
|
|
|
|
}.bind(this));
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
var HotKey = function(params) {
|
|
|
|
this.id = params.id || guid();
|
|
|
|
this.key = params.key;
|
2016-09-27 21:19:24 +02:00
|
|
|
this.description = params.description || null;
|
2016-09-23 23:17:25 +02:00
|
|
|
this.context = params.context || null;
|
|
|
|
this.callback = params.callback;
|
|
|
|
this.preventInClass = params.preventInClass;
|
|
|
|
this.args = params.args;
|
|
|
|
this.onKeyUp = false;
|
2016-09-27 21:19:24 +02:00
|
|
|
|
|
|
|
if (this.key.length > 1)
|
|
|
|
// Automatically translate common hotkeys
|
|
|
|
this.lkey = l('key_' + this.key);
|
2016-09-23 23:17:25 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
HotKey.prototype.clone = function() {
|
|
|
|
return new HotKey(this);
|
|
|
|
};
|
|
|
|
|
|
|
|
var Hotkeys = function() {
|
|
|
|
/**
|
|
|
|
* Sometimes a UI wants keybindings which are global, so called hotkeys.
|
|
|
|
* Keys are keystrings (identify key combinations) and values are objects
|
|
|
|
* with keys callback, context.
|
|
|
|
*/
|
|
|
|
this._hotkeys = {};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sometimes a UI wants keybindings for keyup behaviour.
|
|
|
|
*/
|
|
|
|
this._hotkeysUp = {};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Keybindings are ignored by default when coming from a form input field.
|
|
|
|
*/
|
2016-11-07 18:14:00 +01:00
|
|
|
this._preventIn = ['INPUT', 'SELECT', 'TEXTAREA', 'MD-OPTION'];
|
2016-09-27 21:19:24 +02:00
|
|
|
|
2016-09-28 20:38:59 +02:00
|
|
|
/**
|
|
|
|
* Keybindings are ignored by default when coming from special elements
|
|
|
|
*/
|
2016-11-07 18:14:00 +01:00
|
|
|
this._preventInClass = ['md-chip-content'];
|
2016-09-28 20:38:59 +02:00
|
|
|
|
2016-09-23 23:17:25 +02:00
|
|
|
this._onKeydown = this._onKeydown.bind(this);
|
|
|
|
this._onKeyup = this._onKeyup.bind(this);
|
2016-09-27 21:19:24 +02:00
|
|
|
this._onKeypress = this._onKeypress.bind(this);
|
2016-09-23 23:17:25 +02:00
|
|
|
|
|
|
|
this.initialize();
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Binds Keydown, Keyup with the window object
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype.initialize = function() {
|
2016-09-27 21:19:24 +02:00
|
|
|
this.registerHotkey(
|
|
|
|
this.createHotkey({
|
|
|
|
key: '?',
|
|
|
|
description: l('Show or hide this help'),
|
|
|
|
callback: this._toggleCheatSheet.bind(this)
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
2016-09-23 23:17:25 +02:00
|
|
|
$window.addEventListener('keydown', this._onKeydown, true);
|
|
|
|
$window.addEventListener('keyup', this._onKeyup, true);
|
2016-09-27 21:19:24 +02:00
|
|
|
$window.addEventListener('keypress', this._onKeypress, true);
|
2016-09-23 23:17:25 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invokes callback functions assosiated with the given hotkey
|
|
|
|
* @param {Event} event
|
|
|
|
* @param {String} keyString hotkey
|
|
|
|
* @param {Array.<HotKey>} hotkeys List of registered callbacks for
|
|
|
|
* the given hotkey
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype._invokeHotkeyHandlers = function(event, keyString, hotkeys) {
|
|
|
|
for (var i = 0, l = hotkeys.length; i < l; i++) {
|
|
|
|
var hotkey = hotkeys[i],
|
|
|
|
target = event.target || event.srcElement,
|
|
|
|
nodeName = target.nodeName.toUpperCase();
|
|
|
|
if (!_.includes(this._preventIn, nodeName) &&
|
2016-09-28 20:38:59 +02:00
|
|
|
_.intersection(target.classList, this._preventInClass).length === 0 &&
|
2016-09-23 23:17:25 +02:00
|
|
|
_.intersection(target.classList, hotkey.preventInClass).length === 0) {
|
|
|
|
try {
|
|
|
|
hotkey.callback.call(hotkey.context, event, hotkey.args);
|
|
|
|
} catch(e) {
|
|
|
|
console.error('HotKeys: ', hotkey.key, e.message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Keydown Event Handler
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype._onKeydown = function(event) {
|
|
|
|
var keyString = this.keyStringFromEvent(event);
|
|
|
|
if (this._hotkeys[keyString]) {
|
|
|
|
this._invokeHotkeyHandlers(event, keyString, this._hotkeys[keyString]);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Keyup Event Handler
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype._onKeyup = function(event) {
|
|
|
|
var keyString = this.keyStringFromEvent(event);
|
|
|
|
if (this._hotkeysUp[keyString]) {
|
|
|
|
this._invokeHotkeyHandlers(this._hotkeysUp[keyString], keyString);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-09-27 21:19:24 +02:00
|
|
|
/**
|
|
|
|
* Keypress Event Handler
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype._onKeypress = function(event) {
|
|
|
|
var charCode, keyString;
|
|
|
|
|
|
|
|
charCode = event.keyCode || event.which;
|
|
|
|
keyString = CHAR_CODES[charCode];
|
|
|
|
if (keyString && this._hotkeys[keyString]) {
|
|
|
|
this._invokeHotkeyHandlers(event, keyString, this._hotkeys[keyString]);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-09-23 23:17:25 +02:00
|
|
|
/**
|
|
|
|
* Cross-browser method which can extract a key string from an event.
|
|
|
|
* Key strings are of the form
|
|
|
|
*
|
|
|
|
* ctrl+alt+shift+meta+character
|
|
|
|
*
|
|
|
|
* where each of the 4 modifiers may or may not appear, but always appear
|
|
|
|
* in that order if they do appear.
|
|
|
|
*
|
|
|
|
* TODO : this is not yet implemented fully. The trouble is, the keyCode,
|
|
|
|
* charCode, and which properties of the DOM standard KeyboardEvent are
|
|
|
|
* discouraged in favour of the use of key and char, but key and char are
|
|
|
|
* not yet implemented in Gecko nor in Blink/Webkit. We need to leverage
|
|
|
|
* keyCode/charCode so that current browser versions are supported, but also
|
|
|
|
* look to key and char because they're apparently more useful and are the
|
|
|
|
* future.
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype.keyStringFromEvent = function(event) {
|
|
|
|
var result = [];
|
|
|
|
var key = event.which;
|
|
|
|
|
|
|
|
if (KEY_CODES[key]) {
|
|
|
|
key = KEY_CODES[key];
|
|
|
|
} else {
|
|
|
|
key = String.fromCharCode(key).toLowerCase();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.ctrlKey) { result.push('ctrl'); }
|
|
|
|
if (event.altKey) { result.push('alt'); }
|
|
|
|
if (event.shiftKey) { result.push('shift'); }
|
|
|
|
if (event.metaKey) { result.push('meta'); }
|
|
|
|
result.push(key);
|
|
|
|
return _.uniq(result).join('+');
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unregister a hotkey (shortcut) helper for (keyUp/keyDown).
|
|
|
|
*
|
|
|
|
* @param {String} params.key - valid key string.
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype._deregisterHotkey = function(hotkey) {
|
|
|
|
var ret;
|
|
|
|
var table = this._hotkeys;
|
|
|
|
|
|
|
|
if (hotkey.onKeyUp) {
|
|
|
|
table = this._hotkeysUp;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (table[hotkey.key]) {
|
|
|
|
var callbackArray = table[hotkey.key];
|
|
|
|
for (var i = 0; i < callbackArray.length; ++i) {
|
|
|
|
var callbackData = callbackArray[i];
|
|
|
|
if ((hotkey.callback === callbackData.callback &&
|
|
|
|
callbackData.context === hotkey.context) ||
|
|
|
|
(hotkey.id === callbackData.id)) {
|
|
|
|
ret = callbackArray.splice(i, 1);
|
|
|
|
}
|
|
|
|
}
|
2016-10-17 21:38:13 +02:00
|
|
|
if (callbackArray.length === 0)
|
|
|
|
delete this._hotkeys[hotkey.key];
|
2016-09-23 23:17:25 +02:00
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unregister hotkeys
|
|
|
|
* @param {Hotkey} hotkey A hotkey object
|
|
|
|
* @return {Array}
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype.deregisterHotkey = function(hotkey) {
|
|
|
|
var result = [];
|
|
|
|
|
|
|
|
this._validateHotkey(hotkey);
|
|
|
|
|
|
|
|
if (angular.isArray(hotkey.key)) {
|
|
|
|
for (var i = hotkey.key.length - 1; i >= 0; i--) {
|
|
|
|
var clone = hotkey.clone();
|
|
|
|
clone.key = hotkey.key[i];
|
|
|
|
var ret = this._deregisterHotkey(clone);
|
|
|
|
if (ret !== void 0) {
|
|
|
|
result.push(ret[0]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
result.push(this._deregisterHotkey(hotkey));
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate HotKey type
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype._validateHotkey = function(hotkey) {
|
|
|
|
if (!(hotkey instanceof HotKey)) {
|
|
|
|
throw new TypeError('Hotkeys: Expected a hotkey object be instance of HotKey');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register a hotkey (shortcut) helper for (keyUp/keyDown).
|
|
|
|
* @param {Object} params Parameters object
|
|
|
|
* @param {String} params.key - valid key string.
|
|
|
|
* @param {Function} params.callback - routine to run when key is pressed.
|
|
|
|
* @param {Object} params.context - @this value in the callback.
|
|
|
|
* @param [Boolean] params.onKeyUp - if this is intended for a keyup.
|
|
|
|
* @param [String] params.id - the identifier for this registration.
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype._registerKey = function(hotkey) {
|
|
|
|
var table = this._hotkeys;
|
|
|
|
|
|
|
|
if (hotkey.onKeyUp) {
|
|
|
|
table = this._hotkeysUp;
|
|
|
|
}
|
|
|
|
|
|
|
|
table[hotkey.key] = table[hotkey.key] || [];
|
|
|
|
table[hotkey.key].push(hotkey);
|
|
|
|
return hotkey;
|
|
|
|
};
|
|
|
|
|
|
|
|
Hotkeys.prototype._registerKeys = function(hotkey) {
|
|
|
|
var result = [];
|
|
|
|
|
|
|
|
if (angular.isArray(hotkey.key)) {
|
|
|
|
for (var i = hotkey.key.length - 1; i >= 0; i--) {
|
|
|
|
var clone = hotkey.clone();
|
|
|
|
clone.id = guid();
|
|
|
|
clone.key = hotkey.key[i];
|
|
|
|
result.push(this._registerKey(clone));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
result.push(this._registerKey(hotkey));
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register a hotkey (shortcut). see _registerHotKey
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype.registerHotkey = function(hotkey) {
|
|
|
|
this._validateHotkey(hotkey);
|
|
|
|
return this._registerKeys(hotkey);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Register a hotkey (shortcut) keyup behavior.
|
|
|
|
* see _registerHotKey
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype.registerHotkeyUp = function(hotkey) {
|
|
|
|
this._validateHotkey(hotkey);
|
|
|
|
hotkey.onKeyUp = true;
|
|
|
|
this._registerKeys(hotkey);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates new hotkey object.
|
|
|
|
* @param {Object} args
|
|
|
|
* @return {HotKey}
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype.createHotkey = function(args) {
|
|
|
|
if (args.key === null || args.key === void 0) {
|
|
|
|
throw new TypeError('HotKeys: Argument "key" is required');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (args.callback === null || args.callback === void 0) {
|
|
|
|
throw new TypeError('HotKeys: Argument "callback" is required');
|
|
|
|
}
|
|
|
|
|
|
|
|
args.callback = wrapWithApply(args.callback);
|
|
|
|
return new HotKey(args);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if given shortcut match the event
|
|
|
|
* @param {Event} event An event
|
|
|
|
* @param {String|Array} key A shortcut
|
|
|
|
* @return {Boolean}
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype.match = function(event, key) {
|
|
|
|
if (!angular.isArray(key)) {
|
|
|
|
key = [key];
|
|
|
|
}
|
|
|
|
|
|
|
|
var eventHotkey = this.keyStringFromEvent(event);
|
|
|
|
return Boolean(~key.indexOf(eventHotkey));
|
|
|
|
};
|
|
|
|
|
2016-09-27 21:19:24 +02:00
|
|
|
/**
|
|
|
|
* Build and display (or hide) the hotkeys cheat sheet
|
|
|
|
*
|
|
|
|
* If a hotkey is registered multiple times, only the description of the first registered
|
|
|
|
* hotkey is displayed.
|
|
|
|
*/
|
|
|
|
Hotkeys.prototype._toggleCheatSheet = function() {
|
|
|
|
var _this = this;
|
|
|
|
|
|
|
|
if (this._cheatSheet) {
|
|
|
|
Hotkeys.$modal.hide();
|
|
|
|
this._cheatSheet = null;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
this._cheatSheet = Hotkeys.$modal
|
|
|
|
.show({
|
|
|
|
clickOutsideToClose: true,
|
|
|
|
escapeToClose: true,
|
|
|
|
template: [
|
|
|
|
'<md-dialog>',
|
2018-11-06 16:23:37 +01:00
|
|
|
' <md-toolbar class="md-hue-2">',
|
|
|
|
' <div class="md-toolbar-tools">',
|
|
|
|
' <div ng-bind="::\'Keyboard Shortcuts\' | loc"></div>',
|
|
|
|
' </div>',
|
|
|
|
' </md-toolbar>',
|
2016-09-27 21:19:24 +02:00
|
|
|
' <md-dialog-content>',
|
|
|
|
' <md-list>',
|
|
|
|
' <md-list-item ng-repeat="(hotkey, keys) in hotkeys">',
|
2018-11-06 16:23:37 +01:00
|
|
|
' {{keys[0].description}}',
|
|
|
|
' <div class="md-secondary sg-hotkey-container">',
|
2016-09-27 21:19:24 +02:00
|
|
|
' <sg-hotkey>{{keys[0].lkey || hotkey}}</sg-hotkey>',
|
|
|
|
' </div>',
|
|
|
|
' </md-list-item>',
|
|
|
|
' </md-list>',
|
|
|
|
' </md-dialog-content>',
|
|
|
|
'</md-dialog>'
|
|
|
|
].join(''),
|
|
|
|
controller: CheatSheetController,
|
|
|
|
locals: {
|
|
|
|
hotkeys: _this._hotkeys
|
|
|
|
}
|
2016-09-28 22:04:28 +02:00
|
|
|
})
|
|
|
|
.finally(function() {
|
|
|
|
_this._cheatSheet = null;
|
2016-09-27 21:19:24 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
CheatSheetController.$inject = ['$scope', 'hotkeys'];
|
|
|
|
function CheatSheetController($scope, hotkeys) {
|
|
|
|
$scope.hotkeys = hotkeys;
|
|
|
|
$scope.closeDialog = function() {
|
|
|
|
Hotkeys.$modal.hide();
|
|
|
|
};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-09-23 23:17:25 +02:00
|
|
|
return Hotkeys;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-27 21:19:24 +02:00
|
|
|
sgHotkeys.$inject = ['$mdDialog', '$sgHotkeys'];
|
|
|
|
function sgHotkeys($mdDialog, $sgHotkeys) {
|
|
|
|
angular.extend($sgHotkeys, { $modal: $mdDialog });
|
|
|
|
|
2016-09-23 23:17:25 +02:00
|
|
|
return new $sgHotkeys();
|
|
|
|
}
|
|
|
|
|
|
|
|
angular
|
|
|
|
.module('SOGo.Common')
|
|
|
|
.service('sgHotkeys', sgHotkeys)
|
|
|
|
.provider('$sgHotkeys', $sgHotkeys);
|
|
|
|
})();
|