sogo/UI/WebServerResources/js/Common/sgHotkeys.service.js

476 lines
14 KiB
JavaScript

/* -*- Mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */
(function() {
/* jshint validthis: true */
'use strict';
/**
* $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'
};
// Char-code values for characters that require a key combinations
var CHAR_CODES = {
42: '*',
63: '?'
};
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;
this.description = params.description || null;
this.context = params.context || null;
this.callback = params.callback;
this.preventInClass = params.preventInClass;
this.args = params.args;
this.onKeyUp = false;
if (this.key.length > 1)
// Automatically translate common hotkeys
this.lkey = l('key_' + this.key);
};
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.
*/
this._preventIn = ['INPUT', 'SELECT', 'TEXTAREA', 'MD-OPTION'];
/**
* Keybindings are ignored by default when coming from special elements
*/
this._preventInClass = ['md-chip-content'];
this._onKeydown = this._onKeydown.bind(this);
this._onKeyup = this._onKeyup.bind(this);
this._onKeypress = this._onKeypress.bind(this);
this.initialize();
};
/**
* Binds Keydown, Keyup with the window object
*/
Hotkeys.prototype.initialize = function() {
this.registerHotkey(
this.createHotkey({
key: '?',
description: l('Show or hide this help'),
callback: this._toggleCheatSheet.bind(this)
})
);
$window.addEventListener('keydown', this._onKeydown, true);
$window.addEventListener('keyup', this._onKeyup, true);
$window.addEventListener('keypress', this._onKeypress, true);
};
/**
* 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) &&
_.intersection(target.classList, this._preventInClass).length === 0 &&
_.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);
}
};
/**
* 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]);
}
};
/**
* 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);
}
}
if (callbackArray.length === 0)
delete this._hotkeys[hotkey.key];
}
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));
};
/**
* 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>',
' <md-toolbar class="md-hue-2">',
' <div class="md-toolbar-tools">',
' <div ng-bind="::\'Keyboard Shortcuts\' | loc"></div>',
' </div>',
' </md-toolbar>',
' <md-dialog-content>',
' <md-list>',
' <md-list-item ng-repeat="(hotkey, keys) in hotkeys">',
' {{keys[0].description}}',
' <div class="md-secondary sg-hotkey-container">',
' <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
}
})
.finally(function() {
_this._cheatSheet = null;
});
}
CheatSheetController.$inject = ['$scope', 'hotkeys'];
function CheatSheetController($scope, hotkeys) {
$scope.hotkeys = hotkeys;
$scope.closeDialog = function() {
Hotkeys.$modal.hide();
};
}
};
return Hotkeys;
}
}
sgHotkeys.$inject = ['$mdDialog', '$sgHotkeys'];
function sgHotkeys($mdDialog, $sgHotkeys) {
angular.extend($sgHotkeys, { $modal: $mdDialog });
return new $sgHotkeys();
}
angular
.module('SOGo.Common')
.service('sgHotkeys', sgHotkeys)
.provider('$sgHotkeys', $sgHotkeys);
})();