diff --git a/glances/outputs/static/bower.json b/glances/outputs/static/bower.json
index 2424d373..940bc718 100644
--- a/glances/outputs/static/bower.json
+++ b/glances/outputs/static/bower.json
@@ -6,6 +6,13 @@
"angular-route": "^1.5.8",
"lodash": "^4.13.1",
"favico.js": "^0.3.10",
- "angular-hotkeys-light": "^1.1.1"
+ "angular-hotkeys": "chieffancypants/angular-hotkeys#^1.7.0"
+ },
+ "overrides": {
+ "angular-hotkeys": {
+ "main": [
+ "build/hotkeys.js"
+ ]
+ }
}
}
diff --git a/glances/outputs/static/js/app.js b/glances/outputs/static/js/app.js
index 1a41542b..caade0c0 100644
--- a/glances/outputs/static/js/app.js
+++ b/glances/outputs/static/js/app.js
@@ -1,8 +1,13 @@
-var glancesApp = angular.module('glancesApp', ['ngRoute', 'glances.config', 'fps.hotkeys'])
+var glancesApp = angular.module('glancesApp', ['ngRoute', 'glances.config', 'cfp.hotkeys'])
.value('CONFIG', {})
.value('ARGUMENTS', {})
+.config(function(hotkeysProvider) {
+ hotkeysProvider.useNgRoute = false;
+ hotkeysProvider.includeCheatSheet = false;
+})
+
.run(function($rootScope) {
$rootScope.title = "Glances";
});
diff --git a/glances/outputs/static/js/components/glances/controller.js b/glances/outputs/static/js/components/glances/controller.js
index a5a8b4e0..491f8cd6 100644
--- a/glances/outputs/static/js/components/glances/controller.js
+++ b/glances/outputs/static/js/components/glances/controller.js
@@ -1,6 +1,6 @@
'use strict';
-function GlancesController($scope, $rootScope, $timeout, GlancesStats, REFRESH_TIME, Hotkeys, ARGUMENTS) {
+function GlancesController($scope, $rootScope, $timeout, GlancesStats, REFRESH_TIME, hotkeys, ARGUMENTS) {
var vm = this;
vm.dataLoaded = false;
vm.arguments = ARGUMENTS;
@@ -30,10 +30,207 @@ function GlancesController($scope, $rootScope, $timeout, GlancesStats, REFRESH_T
vm.refreshData();
- Hotkeys.registerHotkey(Hotkeys.createHotkey({
- key: 'm',
+ // A => Enable/disable AMPs
+ hotkeys.add({
+ combo: 'A',
callback: function () {
- console.log('Sort processes by MEM%');
+ ARGUMENTS.disable_amps = !ARGUMENTS.disable_amps;
}
- }));
+ });
+
+ // d => Show/hide disk I/O stats
+ hotkeys.add({
+ combo: 'd',
+ callback: function () {
+ ARGUMENTS.disable_diskio = !ARGUMENTS.disable_diskio;
+ }
+ });
+
+ // Q => Show/hide IRQ
+ hotkeys.add({
+ combo: 'Q',
+ callback: function () {
+ ARGUMENTS.enable_irq = !ARGUMENTS.enable_irq;
+ }
+ });
+
+ // f => Show/hide filesystem stats
+ hotkeys.add({
+ combo: 'f',
+ callback: function () {
+ ARGUMENTS.disable_fs = !ARGUMENTS.disable_fs;
+ }
+ });
+
+ // n => Show/hide network stats
+ hotkeys.add({
+ combo: 'n',
+ callback: function () {
+ ARGUMENTS.disable_network = !ARGUMENTS.disable_network;
+ }
+ });
+
+ // s => Show/hide sensors stats
+ hotkeys.add({
+ combo: 's',
+ callback: function () {
+ ARGUMENTS.disable_sensors = !ARGUMENTS.disable_sensors;
+ }
+ });
+
+ // 2 => Show/hide left sidebar
+ hotkeys.add({
+ combo: '2',
+ callback: function () {
+ ARGUMENTS.disable_left_sidebar = !ARGUMENTS.disable_left_sidebar;
+ }
+ });
+
+ // z => Enable/disable processes stats
+ hotkeys.add({
+ combo: 'z',
+ callback: function () {
+ ARGUMENTS.disable_process = !ARGUMENTS.disable_process;
+ }
+ });
+
+ // SLASH => Enable/disable short processes name
+ hotkeys.add({
+ combo: '/',
+ callback: function () {
+ ARGUMENTS.process_short_name = !ARGUMENTS.process_short_name;
+ }
+ });
+
+ // D => Enable/disable Docker stats
+ hotkeys.add({
+ combo: 'D',
+ callback: function () {
+ ARGUMENTS.disable_docker = !ARGUMENTS.disable_docker;
+ }
+ });
+
+ // b => Bytes or bits for network I/O
+ hotkeys.add({
+ combo: 'b',
+ callback: function () {
+ ARGUMENTS.byte = !ARGUMENTS.byte;
+ }
+ });
+
+ // 'B' => Switch between bit/s and IO/s for Disk IO
+ hotkeys.add({
+ combo: 'B',
+ callback: function () {
+ ARGUMENTS.diskio_iops = !ARGUMENTS.diskio_iops;
+ }
+ });
+
+ // l => Show/hide alert logs
+ hotkeys.add({
+ combo: 'l',
+ callback: function () {
+ ARGUMENTS.disable_alert = !ARGUMENTS.disable_alert;
+ }
+ });
+
+ // 1 => Global CPU or per-CPU stats
+ hotkeys.add({
+ combo: '1',
+ callback: function () {
+ ARGUMENTS.percpu = !ARGUMENTS.percpu;
+ }
+ });
+
+ // h => Show/hide this help screen
+ hotkeys.add({
+ combo: 'h',
+ callback: function () {
+ ARGUMENTS.help_tag = !ARGUMENTS.help_tag;
+ }
+ });
+
+ // T => View network I/O as combination
+ hotkeys.add({
+ combo: 'T',
+ callback: function () {
+ ARGUMENTS.network_sum = !ARGUMENTS.network_sum;
+ }
+ });
+
+ // U => View cumulative network I/O
+ hotkeys.add({
+ combo: 'U',
+ callback: function () {
+ ARGUMENTS.network_cumul = !ARGUMENTS.network_cumul;
+ }
+ });
+
+ // F => Show filesystem free space
+ hotkeys.add({
+ combo: 'F',
+ callback: function () {
+ ARGUMENTS.fs_free_space = !ARGUMENTS.fs_free_space;
+ }
+ });
+
+ // 3 => Enable/disable quick look plugin
+ hotkeys.add({
+ combo: '3',
+ callback: function () {
+ ARGUMENTS.disable_quicklook = !ARGUMENTS.disable_quicklook;
+ }
+ });
+
+ // 6 => Enable/disable mean gpu
+ hotkeys.add({
+ combo: '6',
+ callback: function () {
+ ARGUMENTS.meangpu = !ARGUMENTS.meangpu;
+ }
+ });
+
+ // G => Enable/disable gpu
+ hotkeys.add({
+ combo: 'G',
+ callback: function () {
+ ARGUMENTS.disable_gpu = !ARGUMENTS.disable_gpu;
+ }
+ });
+
+ hotkeys.add({
+ combo: '5',
+ callback: function () {
+ ARGUMENTS.disable_quicklook = !ARGUMENTS.disable_quicklook;
+ ARGUMENTS.disable_cpu = !ARGUMENTS.disable_cpu;
+ ARGUMENTS.disable_mem = !ARGUMENTS.disable_mem;
+ ARGUMENTS.disable_memswap = !ARGUMENTS.disable_memswap;
+ ARGUMENTS.disable_load = !ARGUMENTS.disable_load;
+ ARGUMENTS.disable_gpu = !ARGUMENTS.disable_gpu;
+ }
+ });
+
+ // I => Show/hide IP module
+ hotkeys.add({
+ combo: 'I',
+ callback: function () {
+ ARGUMENTS.disable_ip = !ARGUMENTS.disable_ip;
+ }
+ });
+
+ // P => Enable/disable ports module
+ hotkeys.add({
+ combo: 'P',
+ callback: function () {
+ ARGUMENTS.disable_ports = !ARGUMENTS.disable_ports;
+ }
+ });
+
+ // 'W' > Enable/Disable Wifi plugin
+ hotkeys.add({
+ combo: 'W',
+ callback: function () {
+ ARGUMENTS.disable_wifi = !ARGUMENTS.disable_wifi;
+ }
+ });
}
diff --git a/glances/outputs/static/js/components/plugin-process/controller.js b/glances/outputs/static/js/components/plugin-process/controller.js
index b04b1d90..b05905e8 100644
--- a/glances/outputs/static/js/components/plugin-process/controller.js
+++ b/glances/outputs/static/js/components/plugin-process/controller.js
@@ -1,6 +1,6 @@
'use strict';
-function GlancesPluginProcessController(ARGUMENTS) {
+function GlancesPluginProcessController(ARGUMENTS, hotkeys) {
var vm = this;
vm.arguments = ARGUMENTS;
@@ -18,4 +18,67 @@ function GlancesPluginProcessController(ARGUMENTS) {
}
}
};
+
+ // a => Sort processes automatically
+ hotkeys.add({
+ combo: 'a',
+ callback: function () {
+ vm.sorter.column = "cpu_percent";
+ vm.sorter.auto = true;
+ }
+ });
+
+ // c => Sort processes by CPU%
+ hotkeys.add({
+ combo: 'c',
+ callback: function () {
+ vm.sorter.column = "cpu_percent";
+ vm.sorter.auto = false;
+ }
+ });
+
+ // m => Sort processes by MEM%
+ hotkeys.add({
+ combo: 'm',
+ callback: function () {
+ vm.sorter.column = "memory_percent";
+ vm.sorter.auto = false;
+ }
+ });
+
+ // u => Sort processes by user
+ hotkeys.add({
+ combo: 'u',
+ callback: function () {
+ vm.sorter.column = "username";
+ vm.sorter.auto = false;
+ }
+ });
+
+ // p => Sort processes by name
+ hotkeys.add({
+ combo: 'p',
+ callback: function () {
+ vm.sorter.column = "name";
+ vm.sorter.auto = false;
+ }
+ });
+
+ // i => Sort processes by I/O rate
+ hotkeys.add({
+ combo: 'i',
+ callback: function () {
+ vm.sorter.column = ['io_read', 'io_write'];
+ vm.sorter.auto = false;
+ }
+ });
+
+ // t => Sort processes by time
+ hotkeys.add({
+ combo: 't',
+ callback: function () {
+ vm.sorter.column = "timemillis";
+ vm.sorter.auto = false;
+ }
+ });
}
diff --git a/glances/outputs/static/public/js/main.min.js b/glances/outputs/static/public/js/main.min.js
index 2a2e552d..45d53d34 100644
--- a/glances/outputs/static/public/js/main.min.js
+++ b/glances/outputs/static/public/js/main.min.js
@@ -1,8 +1,13 @@
-var glancesApp = angular.module('glancesApp', ['ngRoute', 'glances.config', 'fps.hotkeys'])
+var glancesApp = angular.module('glancesApp', ['ngRoute', 'glances.config', 'cfp.hotkeys'])
.value('CONFIG', {})
.value('ARGUMENTS', {})
+.config(["hotkeysProvider", function(hotkeysProvider) {
+ hotkeysProvider.useNgRoute = false;
+ hotkeysProvider.includeCheatSheet = false;
+}])
+
.run(["$rootScope", function($rootScope) {
$rootScope.title = "Glances";
}]);
@@ -274,7 +279,7 @@ glancesApp.component('glances', {
'use strict';
-function GlancesController($scope, $rootScope, $timeout, GlancesStats, REFRESH_TIME, Hotkeys, ARGUMENTS) {
+function GlancesController($scope, $rootScope, $timeout, GlancesStats, REFRESH_TIME, hotkeys, ARGUMENTS) {
var vm = this;
vm.dataLoaded = false;
vm.arguments = ARGUMENTS;
@@ -304,12 +309,209 @@ function GlancesController($scope, $rootScope, $timeout, GlancesStats, REFRESH_T
vm.refreshData();
- Hotkeys.registerHotkey(Hotkeys.createHotkey({
- key: 'm',
+ // A => Enable/disable AMPs
+ hotkeys.add({
+ combo: 'A',
callback: function () {
- console.log('Sort processes by MEM%');
+ ARGUMENTS.disable_amps = !ARGUMENTS.disable_amps;
}
- }));
+ });
+
+ // d => Show/hide disk I/O stats
+ hotkeys.add({
+ combo: 'd',
+ callback: function () {
+ ARGUMENTS.disable_diskio = !ARGUMENTS.disable_diskio;
+ }
+ });
+
+ // Q => Show/hide IRQ
+ hotkeys.add({
+ combo: 'Q',
+ callback: function () {
+ ARGUMENTS.enable_irq = !ARGUMENTS.enable_irq;
+ }
+ });
+
+ // f => Show/hide filesystem stats
+ hotkeys.add({
+ combo: 'f',
+ callback: function () {
+ ARGUMENTS.disable_fs = !ARGUMENTS.disable_fs;
+ }
+ });
+
+ // n => Show/hide network stats
+ hotkeys.add({
+ combo: 'n',
+ callback: function () {
+ ARGUMENTS.disable_network = !ARGUMENTS.disable_network;
+ }
+ });
+
+ // s => Show/hide sensors stats
+ hotkeys.add({
+ combo: 's',
+ callback: function () {
+ ARGUMENTS.disable_sensors = !ARGUMENTS.disable_sensors;
+ }
+ });
+
+ // 2 => Show/hide left sidebar
+ hotkeys.add({
+ combo: '2',
+ callback: function () {
+ ARGUMENTS.disable_left_sidebar = !ARGUMENTS.disable_left_sidebar;
+ }
+ });
+
+ // z => Enable/disable processes stats
+ hotkeys.add({
+ combo: 'z',
+ callback: function () {
+ ARGUMENTS.disable_process = !ARGUMENTS.disable_process;
+ }
+ });
+
+ // SLASH => Enable/disable short processes name
+ hotkeys.add({
+ combo: '/',
+ callback: function () {
+ ARGUMENTS.process_short_name = !ARGUMENTS.process_short_name;
+ }
+ });
+
+ // D => Enable/disable Docker stats
+ hotkeys.add({
+ combo: 'D',
+ callback: function () {
+ ARGUMENTS.disable_docker = !ARGUMENTS.disable_docker;
+ }
+ });
+
+ // b => Bytes or bits for network I/O
+ hotkeys.add({
+ combo: 'b',
+ callback: function () {
+ ARGUMENTS.byte = !ARGUMENTS.byte;
+ }
+ });
+
+ // 'B' => Switch between bit/s and IO/s for Disk IO
+ hotkeys.add({
+ combo: 'B',
+ callback: function () {
+ ARGUMENTS.diskio_iops = !ARGUMENTS.diskio_iops;
+ }
+ });
+
+ // l => Show/hide alert logs
+ hotkeys.add({
+ combo: 'l',
+ callback: function () {
+ ARGUMENTS.disable_alert = !ARGUMENTS.disable_alert;
+ }
+ });
+
+ // 1 => Global CPU or per-CPU stats
+ hotkeys.add({
+ combo: '1',
+ callback: function () {
+ ARGUMENTS.percpu = !ARGUMENTS.percpu;
+ }
+ });
+
+ // h => Show/hide this help screen
+ hotkeys.add({
+ combo: 'h',
+ callback: function () {
+ ARGUMENTS.help_tag = !ARGUMENTS.help_tag;
+ }
+ });
+
+ // T => View network I/O as combination
+ hotkeys.add({
+ combo: 'T',
+ callback: function () {
+ ARGUMENTS.network_sum = !ARGUMENTS.network_sum;
+ }
+ });
+
+ // U => View cumulative network I/O
+ hotkeys.add({
+ combo: 'U',
+ callback: function () {
+ ARGUMENTS.network_cumul = !ARGUMENTS.network_cumul;
+ }
+ });
+
+ // F => Show filesystem free space
+ hotkeys.add({
+ combo: 'F',
+ callback: function () {
+ ARGUMENTS.fs_free_space = !ARGUMENTS.fs_free_space;
+ }
+ });
+
+ // 3 => Enable/disable quick look plugin
+ hotkeys.add({
+ combo: '3',
+ callback: function () {
+ ARGUMENTS.disable_quicklook = !ARGUMENTS.disable_quicklook;
+ }
+ });
+
+ // 6 => Enable/disable mean gpu
+ hotkeys.add({
+ combo: '6',
+ callback: function () {
+ ARGUMENTS.meangpu = !ARGUMENTS.meangpu;
+ }
+ });
+
+ // G => Enable/disable gpu
+ hotkeys.add({
+ combo: 'G',
+ callback: function () {
+ ARGUMENTS.disable_gpu = !ARGUMENTS.disable_gpu;
+ }
+ });
+
+ hotkeys.add({
+ combo: '5',
+ callback: function () {
+ ARGUMENTS.disable_quicklook = !ARGUMENTS.disable_quicklook;
+ ARGUMENTS.disable_cpu = !ARGUMENTS.disable_cpu;
+ ARGUMENTS.disable_mem = !ARGUMENTS.disable_mem;
+ ARGUMENTS.disable_memswap = !ARGUMENTS.disable_memswap;
+ ARGUMENTS.disable_load = !ARGUMENTS.disable_load;
+ ARGUMENTS.disable_gpu = !ARGUMENTS.disable_gpu;
+ }
+ });
+
+ // I => Show/hide IP module
+ hotkeys.add({
+ combo: 'I',
+ callback: function () {
+ ARGUMENTS.disable_ip = !ARGUMENTS.disable_ip;
+ }
+ });
+
+ // P => Enable/disable ports module
+ hotkeys.add({
+ combo: 'P',
+ callback: function () {
+ ARGUMENTS.disable_ports = !ARGUMENTS.disable_ports;
+ }
+ });
+
+ // 'W' > Enable/Disable Wifi plugin
+ hotkeys.add({
+ combo: 'W',
+ callback: function () {
+ ARGUMENTS.disable_wifi = !ARGUMENTS.disable_wifi;
+ }
+ });
}
'use strict';
@@ -849,6 +1051,99 @@ function GlancesPluginPercpuController($scope, GlancesPluginHelper) {
'use strict';
+glancesApp.component('glancesPluginProcess', {
+ controller: GlancesPluginProcessController,
+ controllerAs: 'vm',
+ templateUrl: 'components/plugin-process/view.html'
+});
+
+'use strict';
+
+function GlancesPluginProcessController(ARGUMENTS, hotkeys) {
+ var vm = this;
+ vm.arguments = ARGUMENTS;
+
+ vm.sorter = {
+ column: "cpu_percent",
+ auto: true,
+ isReverseColumn: function (column) {
+ return !(column === 'username' || column === 'name');
+ },
+ getColumnLabel: function (column) {
+ if (_.isEqual(column, ['io_read', 'io_write'])) {
+ return 'io_counters';
+ } else {
+ return column;
+ }
+ }
+ };
+
+ // a => Sort processes automatically
+ hotkeys.add({
+ combo: 'a',
+ callback: function () {
+ vm.sorter.column = "cpu_percent";
+ vm.sorter.auto = true;
+ }
+ });
+
+ // c => Sort processes by CPU%
+ hotkeys.add({
+ combo: 'c',
+ callback: function () {
+ vm.sorter.column = "cpu_percent";
+ vm.sorter.auto = false;
+ }
+ });
+
+ // m => Sort processes by MEM%
+ hotkeys.add({
+ combo: 'm',
+ callback: function () {
+ vm.sorter.column = "memory_percent";
+ vm.sorter.auto = false;
+ }
+ });
+
+ // u => Sort processes by user
+ hotkeys.add({
+ combo: 'u',
+ callback: function () {
+ vm.sorter.column = "username";
+ vm.sorter.auto = false;
+ }
+ });
+
+ // p => Sort processes by name
+ hotkeys.add({
+ combo: 'p',
+ callback: function () {
+ vm.sorter.column = "name";
+ vm.sorter.auto = false;
+ }
+ });
+
+ // i => Sort processes by I/O rate
+ hotkeys.add({
+ combo: 'i',
+ callback: function () {
+ vm.sorter.column = ['io_read', 'io_write'];
+ vm.sorter.auto = false;
+ }
+ });
+
+ // t => Sort processes by time
+ hotkeys.add({
+ combo: 't',
+ callback: function () {
+ vm.sorter.column = "timemillis";
+ vm.sorter.auto = false;
+ }
+ });
+}
+
+'use strict';
+
glancesApp.component('glancesPluginPorts', {
controller: GlancesPluginPortsController,
controllerAs: 'vm',
@@ -890,36 +1185,6 @@ function GlancesPluginPortsController($scope) {
'use strict';
-glancesApp.component('glancesPluginProcess', {
- controller: GlancesPluginProcessController,
- controllerAs: 'vm',
- templateUrl: 'components/plugin-process/view.html'
-});
-
-'use strict';
-
-function GlancesPluginProcessController(ARGUMENTS) {
- var vm = this;
- vm.arguments = ARGUMENTS;
-
- vm.sorter = {
- column: "cpu_percent",
- auto: true,
- isReverseColumn: function (column) {
- return !(column === 'username' || column === 'name');
- },
- getColumnLabel: function (column) {
- if (_.isEqual(column, ['io_read', 'io_write'])) {
- return 'io_counters';
- } else {
- return column;
- }
- }
- };
-}
-
-'use strict';
-
glancesApp.component('glancesPluginProcesscount', {
controller: GlancesPluginProcesscountController,
controllerAs: 'vm',
@@ -953,6 +1218,58 @@ function GlancesPluginProcesscountController($scope) {
'use strict';
+glancesApp.component('glancesPluginQuicklook', {
+ controller: GlancesPluginQuicklookController,
+ controllerAs: 'vm',
+ templateUrl: 'components/plugin-quicklook/view.html'
+});
+
+'use strict';
+
+function GlancesPluginQuicklookController($scope, ARGUMENTS) {
+ var vm = this;
+ vm.arguments = ARGUMENTS;
+ var _view = {};
+
+ vm.mem = null;
+ vm.cpu = null;
+ vm.cpu_name = null;
+ vm.cpu_hz_current = null;
+ vm.cpu_hz = null;
+ vm.swap = null;
+ vm.percpus = [];
+
+ $scope.$on('data_refreshed', function(event, data) {
+ var stats = data.stats['quicklook'];
+ _view = data.view['quicklook'];
+
+ vm.mem = stats.mem;
+ vm.cpu = stats.cpu;
+ vm.cpu_name = stats.cpu_name;
+ vm.cpu_hz_current = stats.cpu_hz_current;
+ vm.cpu_hz = stats.cpu_hz;
+ vm.swap = stats.swap;
+ vm.percpus = [];
+
+ angular.forEach(stats.percpu, function(cpu) {
+ vm.percpus.push({
+ 'number': cpu.cpu_number,
+ 'total': cpu.total
+ });
+ }, this);
+ });
+
+ this.getDecoration = function (value) {
+ if (_view[value] === undefined) {
+ return;
+ }
+
+ return _view[value].decoration.toLowerCase();
+ };
+}
+
+'use strict';
+
glancesApp.component('glancesPluginProcesslist', {
controller: GlancesPluginProcesslistController,
controllerAs: 'vm',
@@ -1029,58 +1346,6 @@ function GlancesPluginProcesslistController($scope, GlancesPluginHelper, $filter
'use strict';
-glancesApp.component('glancesPluginQuicklook', {
- controller: GlancesPluginQuicklookController,
- controllerAs: 'vm',
- templateUrl: 'components/plugin-quicklook/view.html'
-});
-
-'use strict';
-
-function GlancesPluginQuicklookController($scope, ARGUMENTS) {
- var vm = this;
- vm.arguments = ARGUMENTS;
- var _view = {};
-
- vm.mem = null;
- vm.cpu = null;
- vm.cpu_name = null;
- vm.cpu_hz_current = null;
- vm.cpu_hz = null;
- vm.swap = null;
- vm.percpus = [];
-
- $scope.$on('data_refreshed', function(event, data) {
- var stats = data.stats['quicklook'];
- _view = data.view['quicklook'];
-
- vm.mem = stats.mem;
- vm.cpu = stats.cpu;
- vm.cpu_name = stats.cpu_name;
- vm.cpu_hz_current = stats.cpu_hz_current;
- vm.cpu_hz = stats.cpu_hz;
- vm.swap = stats.swap;
- vm.percpus = [];
-
- angular.forEach(stats.percpu, function(cpu) {
- vm.percpus.push({
- 'number': cpu.cpu_number,
- 'total': cpu.total
- });
- }, this);
- });
-
- this.getDecoration = function (value) {
- if (_view[value] === undefined) {
- return;
- }
-
- return _view[value].decoration.toLowerCase();
- };
-}
-
-'use strict';
-
glancesApp.component('glancesPluginSystem', {
controller: GlancesPluginSystemController,
controllerAs: 'vm',
@@ -1118,25 +1383,6 @@ function GlancesPluginSystemController($scope) {
'use strict';
-glancesApp.component('glancesPluginUptime', {
- controller: GlancesPluginUptimeController,
- controllerAs: 'vm',
- templateUrl: 'components/plugin-uptime/view.html'
-});
-
-'use strict';
-
-function GlancesPluginUptimeController($scope) {
- var vm = this;
- vm.value = null;
-
- $scope.$on('data_refreshed', function(event, data) {
- vm.value = data.stats['uptime'];
- });
-}
-
-'use strict';
-
glancesApp.component('glancesPluginWifi', {
controller: GlancesPluginWifiController,
controllerAs: 'vm',
@@ -1182,3 +1428,22 @@ function GlancesPluginWifiController($scope, $filter) {
return _view[hotpost.ssid][field].decoration.toLowerCase();
};
}
+
+'use strict';
+
+glancesApp.component('glancesPluginUptime', {
+ controller: GlancesPluginUptimeController,
+ controllerAs: 'vm',
+ templateUrl: 'components/plugin-uptime/view.html'
+});
+
+'use strict';
+
+function GlancesPluginUptimeController($scope) {
+ var vm = this;
+ vm.value = null;
+
+ $scope.$on('data_refreshed', function(event, data) {
+ vm.value = data.stats['uptime'];
+ });
+}
diff --git a/glances/outputs/static/public/js/templates.min.js b/glances/outputs/static/public/js/templates.min.js
index 2f36b3b6..1321093a 100644
--- a/glances/outputs/static/public/js/templates.min.js
+++ b/glances/outputs/static/public/js/templates.min.js
@@ -12,11 +12,11 @@ $templateCache.put('components/plugin-mem-more/view.html','\n \n
\n
SWAP
\n
{{ vm.percent }}%
\n
\n
\n
total:
\n
{{ vm.total | bytes }}
\n
\n
\n
used:
\n
\n {{ vm.used | bytes }}\n
\n
\n
\n
free:
\n
{{ vm.free | bytes }}
\n
\n
\n\n');
$templateCache.put('components/plugin-network/view.html','
\n
NETWORK
\n
Rx/s
\n
Tx/s
\n\n
\n
Rx+Tx/s
\n\n
Rx
\n
Tx
\n\n
\n
Rx+Tx
\n
\n\n
{{ network.interfaceName | min_size }}
\n
{{ vm.arguments.byte ? (network.rx / network.time_since_update | bytes) : (network.rx / network.time_since_update | bits) }}
\n
{{ vm.arguments.byte ? (network.tx / network.time_since_update | bytes) : (network.tx / network.time_since_update | bits) }}
\n\n
\n
{{ vm.arguments.byte ? (network.cx / network.time_since_update | bytes) : (network.cx / network.time_since_update | bits) }}
\n\n
{{ vm.arguments.byte ? (network.cumulativeRx | bytes) : (network.cumulativeRx | bits) }}
\n
{{ vm.arguments.byte ? (network.cumulativeTx | bytes) : (network.cumulativeTx | bits) }}
\n\n
\n
{{ vm.arguments.byte ? (network.cumulativeCx | bytes) : (network.cumulativeCx | bits) }}
\n
\n');
$templateCache.put('components/plugin-percpu/view.html','\n \n
\n
PER CPU
\n
{{ percpu.total }}%
\n
\n
\n
user:
\n
\n {{ percpu.user }}%\n
\n
\n
\n
system:
\n
\n {{ percpu.system }}%\n
\n
\n
\n
idle:
\n
{{ percpu.idle }}%
\n
\n
\n
iowait:
\n
\n {{ percpu.iowait }}%\n
\n
\n
\n
steal:
\n
\n {{ percpu.steal }}%\n
\n
\n
\n\n');
-$templateCache.put('components/plugin-ports/view.html','\n
{{(port.description ? port.description : port.host + \' \' + port.port) | min_size: 20}}
\n
\n
\n Scanning\n Timeout\n Open\n {{port.status * 1000.0 | number:0}}ms\n
\n
');
$templateCache.put('components/plugin-process/view.html','\n \n \n \n
\nPROCESSES DISABLED (press \'z\' to display)
\n');
+$templateCache.put('components/plugin-ports/view.html','\n
{{(port.description ? port.description : port.host + \' \' + port.port) | min_size: 20}}
\n
\n
\n Scanning\n Timeout\n Open\n {{port.status * 1000.0 | number:0}}ms\n
\n
');
$templateCache.put('components/plugin-processcount/view.html','\n TASKS\n {{ vm.total }} ({{ vm.thread }} thr),\n {{ vm.running }} run,\n {{ vm.sleeping }} slp,\n {{ vm.stopped }} oth\n sorted {{ vm.sorter.auto ? \'automatically\' : \'\' }} by {{ vm.sorter.getColumnLabel(vm.sorter.column) }}, flat view\n');
-$templateCache.put('components/plugin-processlist/view.html','\n \n
\n
CPU%
\n
MEM%
\n
VIRT
\n
RES
\n
PID
\n
USER
\n
NI
\n
S
\n
TIME+
\n
IOR/s
\n
IOW/s
\n
Command
\n
\n
\n
{{process.cpu_percent | number:1}}
\n
{{process.memory_percent | number:1}}
\n
{{process.memvirt | bytes}}
\n
{{process.memres | bytes}}
\n
{{process.pid}}
\n
{{process.username}}
\n
{{process.nice | exclamation}}
\n
{{process.status}}
\n
\n {{ process.timeplus.hours }}h{{ process.timeplus.minutes | leftPad:2:\'0\' }}:{{ process.timeplus.seconds | leftPad:2:\'0\' }}.{{ process.timeplus.milliseconds | leftPad:2:\'0\' }}\n
\n
{{process.ioRead}}
\n
{{process.ioWrite}}
\n
{{process.name}}
\n
{{process.cmdline}}
\n
\n
\n\n');
$templateCache.put('components/plugin-quicklook/view.html','\n \n {{ vm.cpu_name }}\n
\n \n
\n
CPU
\n
\n
\n {{ vm.cpu }}%\n
\n
\n
\n
CPU{{ percpu.number }}
\n
\n
\n {{ percpu.total }}%\n
\n
\n
\n
MEM
\n
\n
\n {{ vm.mem }}%\n
\n
\n
\n
SWAP
\n
\n
\n {{ vm.swap }}%\n
\n
\n
\n\n');
+$templateCache.put('components/plugin-processlist/view.html','\n \n
\n
CPU%
\n
MEM%
\n
VIRT
\n
RES
\n
PID
\n
USER
\n
NI
\n
S
\n
TIME+
\n
IOR/s
\n
IOW/s
\n
Command
\n
\n
\n
{{process.cpu_percent | number:1}}
\n
{{process.memory_percent | number:1}}
\n
{{process.memvirt | bytes}}
\n
{{process.memres | bytes}}
\n
{{process.pid}}
\n
{{process.username}}
\n
{{process.nice | exclamation}}
\n
{{process.status}}
\n
\n {{ process.timeplus.hours }}h{{ process.timeplus.minutes | leftPad:2:\'0\' }}:{{ process.timeplus.seconds | leftPad:2:\'0\' }}.{{ process.timeplus.milliseconds | leftPad:2:\'0\' }}\n
\n
{{process.ioRead}}
\n
{{process.ioWrite}}
\n
{{process.name}}
\n
{{process.cmdline}}
\n
\n
\n\n');
$templateCache.put('components/plugin-system/view.html','\n Disconnected from\n {{ vm.hostname }}\n ({{ vm.humanReadableName }} / {{ vm.os.name }} {{ vm.os.version }})\n ({{ vm.os.name }} {{ vm.os.version }} {{ vm.platform }})\n\n');
-$templateCache.put('components/plugin-uptime/view.html','\n Uptime: {{ vm.value }}\n\n');
-$templateCache.put('components/plugin-wifi/view.html','\n \n \n
{{ hotspot.ssid|limitTo:20 }} {{ hotspot.encryption_type }}
\n
\n
{{ hotspot.signal }}
\n
\n\n');}]);
\ No newline at end of file
+$templateCache.put('components/plugin-wifi/view.html','\n \n \n
{{ hotspot.ssid|limitTo:20 }} {{ hotspot.encryption_type }}
\n
\n
{{ hotspot.signal }}
\n
\n\n');
+$templateCache.put('components/plugin-uptime/view.html','\n Uptime: {{ vm.value }}\n\n');}]);
\ No newline at end of file
diff --git a/glances/outputs/static/public/js/vendor.min.js b/glances/outputs/static/public/js/vendor.min.js
index c2bad8ba..0c6b6ac5 100644
--- a/glances/outputs/static/public/js/vendor.min.js
+++ b/glances/outputs/static/public/js/vendor.min.js
@@ -52140,5 +52140,1664 @@ function ngViewFillContentFactory($compile, $controller, $route) {
})();
-!function(a){!function(a){a.module("fps.hotkeys",[]).service("Hotkeys",["$Hotkeys",function(a){return new a}]).provider("$Hotkeys",function(){function a(){function a(){return Math.floor(65536*(1+Math.random())).toString(16).substring(1)}return a()+a()+"-"+a()+"-"+a()+"-"+a()+"-"+a()+a()+a()}var b=Array.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)},c=function(a){for(var b={},c=[],d=0;d=0;d--){var e=a.clone();e.key=a.key[d];var f=this._deregisterHotkey(e);void 0!==f&&c.push(f[0])}else c.push(this._deregisterHotkey(a));return c}}),Object.defineProperty(i.prototype,"_validateHotkey",{value:function(a){if(!(a instanceof h))throw new TypeError("Hotkeys: Expected a hotkey object be instance of HotKey")}}),Object.defineProperty(i.prototype,"_registerKey",{value:function(a){var b=this._hotkeys;return a.onKeyUp&&(b=this._hotkeysUp),b[a.key]=b[a.key]||[],b[a.key].push(a),a}}),Object.defineProperty(i.prototype,"_registerKeys",{value:function(c){var d=[];if(b(c.key))for(var e=c.key.length-1;e>=0;e--){var f=c.clone();f.id=a(),f.key=c.key[e],d.push(this._registerKey(f))}else d.push(this._registerKey(c));return d}}),Object.defineProperty(i.prototype,"registerHotkey",{value:function(a){return this._validateHotkey(a),this._registerKeys(a)}}),Object.defineProperty(i.prototype,"registerHotkeyUp",{value:function(a){this._validateHotkey(a),a.onKeyUp=!0,this._registerKeys(a)}}),Object.defineProperty(i.prototype,"createHotkey",{value:function(a){if(null===a.key||void 0===a.key)throw new TypeError('HotKeys: Argument "key" is required');if(null===a.callback||void 0===a.callback)throw new TypeError('HotKeys: Argument "callback" is required');return a.callback=g(a.callback),new h(a)}}),Object.defineProperty(i.prototype,"match",{value:function(a,c){b(c)||(c=[c]);var d=this.keyStringFromEvent(a);return Boolean(~c.indexOf(d))}}),i}]})}(window.angular)}(angular);
-//# sourceMappingURL=angular-hotkeys-light.map.js
\ No newline at end of file
+/*!
+ * angular-hotkeys v1.7.0
+ * https://chieffancypants.github.io/angular-hotkeys
+ * Copyright (c) 2016 Wes Cruver
+ * License: MIT
+ */
+/*
+ * angular-hotkeys
+ *
+ * Automatic keyboard shortcuts for your angular apps
+ *
+ * (c) 2016 Wes Cruver
+ * License: MIT
+ */
+
+(function() {
+
+ 'use strict';
+
+ angular.module('cfp.hotkeys', []).provider('hotkeys', ['$injector', function($injector) {
+
+ /**
+ * Configurable setting to disable the cheatsheet entirely
+ * @type {Boolean}
+ */
+ this.includeCheatSheet = true;
+
+ /**
+ * Configurable setting to disable ngRoute hooks
+ * @type {Boolean}
+ */
+ this.useNgRoute = $injector.has('ngViewDirective');
+
+ /**
+ * Configurable setting for the cheat sheet title
+ * @type {String}
+ */
+
+ this.templateTitle = 'Keyboard Shortcuts:';
+
+ /**
+ * Configurable settings for the cheat sheet header and footer. Both are HTML, and the header
+ * overrides the normal title if specified.
+ * @type {String}
+ */
+ this.templateHeader = null;
+ this.templateFooter = null;
+
+ /**
+ * Cheat sheet template in the event you want to totally customize it.
+ * @type {String}
+ */
+ this.template = '' +
+ '
{{ title }}
' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '{{ key }}' +
+ ' | ' +
+ '{{ hotkey.description }} | ' +
+ '
' +
+ '
' +
+ '
' +
+ '
×
' +
+ '
';
+
+ /**
+ * Configurable setting for the cheat sheet hotkey
+ * @type {String}
+ */
+ this.cheatSheetHotkey = '?';
+
+ /**
+ * Configurable setting for the cheat sheet description
+ * @type {String}
+ */
+ this.cheatSheetDescription = 'Show / hide this help menu';
+
+ this.$get = ['$rootElement', '$rootScope', '$compile', '$window', '$document', function ($rootElement, $rootScope, $compile, $window, $document) {
+
+ var mouseTrapEnabled = true;
+
+ function pause() {
+ mouseTrapEnabled = false;
+ }
+
+ function unpause() {
+ mouseTrapEnabled = true;
+ }
+
+ // monkeypatch Mousetrap's stopCallback() function
+ // this version doesn't return true when the element is an INPUT, SELECT, or TEXTAREA
+ // (instead we will perform this check per-key in the _add() method)
+ Mousetrap.prototype.stopCallback = function(event, element) {
+ if (!mouseTrapEnabled) {
+ return true;
+ }
+
+ // if the element has the class "mousetrap" then no need to stop
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
+ return false;
+ }
+
+ return (element.contentEditable && element.contentEditable == 'true');
+ };
+
+ /**
+ * Convert strings like cmd into symbols like ⌘
+ * @param {String} combo Key combination, e.g. 'mod+f'
+ * @return {String} The key combination with symbols
+ */
+ function symbolize (combo) {
+ var map = {
+ command : '\u2318', // ⌘
+ shift : '\u21E7', // ⇧
+ left : '\u2190', // ←
+ right : '\u2192', // →
+ up : '\u2191', // ↑
+ down : '\u2193', // ↓
+ 'return' : '\u23CE', // ⏎
+ backspace : '\u232B' // ⌫
+ };
+ combo = combo.split('+');
+
+ for (var i = 0; i < combo.length; i++) {
+ // try to resolve command / ctrl based on OS:
+ if (combo[i] === 'mod') {
+ if ($window.navigator && $window.navigator.platform.indexOf('Mac') >=0 ) {
+ combo[i] = 'command';
+ } else {
+ combo[i] = 'ctrl';
+ }
+ }
+
+ combo[i] = map[combo[i]] || combo[i];
+ }
+
+ return combo.join(' + ');
+ }
+
+ /**
+ * Hotkey object used internally for consistency
+ *
+ * @param {array} combo The keycombo. it's an array to support multiple combos
+ * @param {String} description Description for the keycombo
+ * @param {Function} callback function to execute when keycombo pressed
+ * @param {string} action the type of event to listen for (for mousetrap)
+ * @param {array} allowIn an array of tag names to allow this combo in ('INPUT', 'SELECT', and/or 'TEXTAREA')
+ * @param {Boolean} persistent Whether the hotkey persists navigation events
+ */
+ function Hotkey (combo, description, callback, action, allowIn, persistent) {
+ // TODO: Check that the values are sane because we could
+ // be trying to instantiate a new Hotkey with outside dev's
+ // supplied values
+
+ this.combo = combo instanceof Array ? combo : [combo];
+ this.description = description;
+ this.callback = callback;
+ this.action = action;
+ this.allowIn = allowIn;
+ this.persistent = persistent;
+ this._formated = null;
+ }
+
+ /**
+ * Helper method to format (symbolize) the key combo for display
+ *
+ * @return {[Array]} An array of the key combination sequence
+ * for example: "command+g c i" becomes ["⌘ + g", "c", "i"]
+ *
+ */
+ Hotkey.prototype.format = function() {
+ if (this._formated === null) {
+ // Don't show all the possible key combos, just the first one. Not sure
+ // of usecase here, so open a ticket if my assumptions are wrong
+ var combo = this.combo[0];
+
+ var sequence = combo.split(/[\s]/);
+ for (var i = 0; i < sequence.length; i++) {
+ sequence[i] = symbolize(sequence[i]);
+ }
+ this._formated = sequence;
+ }
+
+ return this._formated;
+ };
+
+ /**
+ * A new scope used internally for the cheatsheet
+ * @type {$rootScope.Scope}
+ */
+ var scope = $rootScope.$new();
+
+ /**
+ * Holds an array of Hotkey objects currently bound
+ * @type {Array}
+ */
+ scope.hotkeys = [];
+
+ /**
+ * Contains the state of the help's visibility
+ * @type {Boolean}
+ */
+ scope.helpVisible = false;
+
+ /**
+ * Holds the title string for the help menu
+ * @type {String}
+ */
+ scope.title = this.templateTitle;
+
+ /**
+ * Holds the header HTML for the help menu
+ * @type {String}
+ */
+ scope.header = this.templateHeader;
+
+ /**
+ * Holds the footer HTML for the help menu
+ * @type {String}
+ */
+ scope.footer = this.templateFooter;
+
+ /**
+ * Expose toggleCheatSheet to hotkeys scope so we can call it using
+ * ng-click from the template
+ * @type {function}
+ */
+ scope.toggleCheatSheet = toggleCheatSheet;
+
+
+ /**
+ * Holds references to the different scopes that have bound hotkeys
+ * attached. This is useful to catch when the scopes are `$destroy`d and
+ * then automatically unbind the hotkey.
+ *
+ * @type {Object}
+ */
+ var boundScopes = {};
+
+ if (this.useNgRoute) {
+ $rootScope.$on('$routeChangeSuccess', function (event, route) {
+ purgeHotkeys();
+
+ if (route && route.hotkeys) {
+ angular.forEach(route.hotkeys, function (hotkey) {
+ // a string was given, which implies this is a function that is to be
+ // $eval()'d within that controller's scope
+ // TODO: hotkey here is super confusing. sometimes a function (that gets turned into an array), sometimes a string
+ var callback = hotkey[2];
+ if (typeof(callback) === 'string' || callback instanceof String) {
+ hotkey[2] = [callback, route];
+ }
+
+ // todo: perform check to make sure not already defined:
+ // this came from a route, so it's likely not meant to be persistent
+ hotkey[5] = false;
+ _add.apply(this, hotkey);
+ });
+ }
+ });
+ }
+
+
+
+ // Auto-create a help menu:
+ if (this.includeCheatSheet) {
+ var document = $document[0];
+ var element = $rootElement[0];
+ var helpMenu = angular.element(this.template);
+ _add(this.cheatSheetHotkey, this.cheatSheetDescription, toggleCheatSheet);
+
+ // If $rootElement is document or documentElement, then body must be used
+ if (element === document || element === document.documentElement) {
+ element = document.body;
+ }
+
+ angular.element(element).append($compile(helpMenu)(scope));
+ }
+
+
+ /**
+ * Purges all non-persistent hotkeys (such as those defined in routes)
+ *
+ * Without this, the same hotkey would get recreated everytime
+ * the route is accessed.
+ */
+ function purgeHotkeys() {
+ var i = scope.hotkeys.length;
+ while (i--) {
+ var hotkey = scope.hotkeys[i];
+ if (hotkey && !hotkey.persistent) {
+ _del(hotkey);
+ }
+ }
+ }
+
+ /**
+ * Toggles the help menu element's visiblity
+ */
+ var previousEsc = false;
+
+ function toggleCheatSheet() {
+ scope.helpVisible = !scope.helpVisible;
+
+ // Bind to esc to remove the cheat sheet. Ideally, this would be done
+ // as a directive in the template, but that would create a nasty
+ // circular dependency issue that I don't feel like sorting out.
+ if (scope.helpVisible) {
+ previousEsc = _get('esc');
+ _del('esc');
+
+ // Here's an odd way to do this: we're going to use the original
+ // description of the hotkey on the cheat sheet so that it shows up.
+ // without it, no entry for esc will ever show up (#22)
+ _add('esc', previousEsc.description, toggleCheatSheet, null, ['INPUT', 'SELECT', 'TEXTAREA']);
+ } else {
+ _del('esc');
+
+ // restore the previously bound ESC key
+ if (previousEsc !== false) {
+ _add(previousEsc);
+ }
+ }
+ }
+
+ /**
+ * Creates a new Hotkey and creates the Mousetrap binding
+ *
+ * @param {string} combo mousetrap key binding
+ * @param {string} description description for the help menu
+ * @param {Function} callback method to call when key is pressed
+ * @param {string} action the type of event to listen for (for mousetrap)
+ * @param {array} allowIn an array of tag names to allow this combo in ('INPUT', 'SELECT', and/or 'TEXTAREA')
+ * @param {boolean} persistent if true, the binding is preserved upon route changes
+ */
+ function _add (combo, description, callback, action, allowIn, persistent) {
+
+ // used to save original callback for "allowIn" wrapping:
+ var _callback;
+
+ // these elements are prevented by the default Mousetrap.stopCallback():
+ var preventIn = ['INPUT', 'SELECT', 'TEXTAREA'];
+
+ // Determine if object format was given:
+ var objType = Object.prototype.toString.call(combo);
+
+ if (objType === '[object Object]') {
+ description = combo.description;
+ callback = combo.callback;
+ action = combo.action;
+ persistent = combo.persistent;
+ allowIn = combo.allowIn;
+ combo = combo.combo;
+ }
+
+ // no duplicates please
+ _del(combo);
+
+ // description is optional:
+ if (description instanceof Function) {
+ action = callback;
+ callback = description;
+ description = '$$undefined$$';
+ } else if (angular.isUndefined(description)) {
+ description = '$$undefined$$';
+ }
+
+ // any items added through the public API are for controllers
+ // that persist through navigation, and thus undefined should mean
+ // true in this case.
+ if (persistent === undefined) {
+ persistent = true;
+ }
+ // if callback is defined, then wrap it in a function
+ // that checks if the event originated from a form element.
+ // the function blocks the callback from executing unless the element is specified
+ // in allowIn (emulates Mousetrap.stopCallback() on a per-key level)
+ if (typeof callback === 'function') {
+
+ // save the original callback
+ _callback = callback;
+
+ // make sure allowIn is an array
+ if (!(allowIn instanceof Array)) {
+ allowIn = [];
+ }
+
+ // remove anything from preventIn that's present in allowIn
+ var index;
+ for (var i=0; i < allowIn.length; i++) {
+ allowIn[i] = allowIn[i].toUpperCase();
+ index = preventIn.indexOf(allowIn[i]);
+ if (index !== -1) {
+ preventIn.splice(index, 1);
+ }
+ }
+
+ // create the new wrapper callback
+ callback = function(event) {
+ var shouldExecute = true;
+
+ // if the callback is executed directly `hotkey.get('w').callback()`
+ // there will be no event, so just execute the callback.
+ if (event) {
+ var target = event.target || event.srcElement; // srcElement is IE only
+ var nodeName = target.nodeName.toUpperCase();
+
+ // check if the input has a mousetrap class, and skip checking preventIn if so
+ if ((' ' + target.className + ' ').indexOf(' mousetrap ') > -1) {
+ shouldExecute = true;
+ } else {
+ // don't execute callback if the event was fired from inside an element listed in preventIn
+ for (var i=0; i -1) {
+ // if the combo has other combos bound, don't unbind the whole thing, just the one combo:
+ if (scope.hotkeys[index].combo.length > 1) {
+ scope.hotkeys[index].combo.splice(scope.hotkeys[index].combo.indexOf(combo), 1);
+ } else {
+
+ // remove hotkey from bound scopes
+ angular.forEach(boundScopes, function (boundScope) {
+ var scopeIndex = boundScope.indexOf(scope.hotkeys[index]);
+ if (scopeIndex !== -1) {
+ boundScope.splice(scopeIndex, 1);
+ }
+ });
+
+ scope.hotkeys.splice(index, 1);
+ }
+ return true;
+ }
+ }
+
+ return false;
+
+ }
+
+ /**
+ * Get a Hotkey object by key binding
+ *
+ * @param {[string]} [combo] the key the Hotkey is bound to. Returns all key bindings if no key is passed
+ * @return {Hotkey} The Hotkey object
+ */
+ function _get (combo) {
+
+ if (!combo) {
+ return scope.hotkeys;
+ }
+
+ var hotkey;
+
+ for (var i = 0; i < scope.hotkeys.length; i++) {
+ hotkey = scope.hotkeys[i];
+
+ if (hotkey.combo.indexOf(combo) > -1) {
+ return hotkey;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Binds the hotkey to a particular scope. Useful if the scope is
+ * destroyed, we can automatically destroy the hotkey binding.
+ *
+ * @param {Object} scope The scope to bind to
+ */
+ function bindTo (scope) {
+ // Only initialize once to allow multiple calls for same scope.
+ if (!(scope.$id in boundScopes)) {
+
+ // Add the scope to the list of bound scopes
+ boundScopes[scope.$id] = [];
+
+ scope.$on('$destroy', function () {
+ var i = boundScopes[scope.$id].length;
+ while (i--) {
+ _del(boundScopes[scope.$id].pop());
+ }
+ });
+ }
+ // return an object with an add function so we can keep track of the
+ // hotkeys and their scope that we added via this chaining method
+ return {
+ add: function (args) {
+ var hotkey;
+
+ if (arguments.length > 1) {
+ hotkey = _add.apply(this, arguments);
+ } else {
+ hotkey = _add(args);
+ }
+
+ boundScopes[scope.$id].push(hotkey);
+ return this;
+ }
+ };
+ }
+
+ /**
+ * All callbacks sent to Mousetrap are wrapped using this function
+ * so that we can force a $scope.$apply()
+ *
+ * @param {Function} callback [description]
+ * @return {[type]} [description]
+ */
+ function wrapApply (callback) {
+ // return mousetrap a function to call
+ return function (event, combo) {
+
+ // if this is an array, it means we provided a route object
+ // because the scope wasn't available yet, so rewrap the callback
+ // now that the scope is available:
+ if (callback instanceof Array) {
+ var funcString = callback[0];
+ var route = callback[1];
+ callback = function (event) {
+ route.scope.$eval(funcString);
+ };
+ }
+
+ // this takes place outside angular, so we'll have to call
+ // $apply() to make sure angular's digest happens
+ $rootScope.$apply(function() {
+ // call the original hotkey callback with the keyboard event
+ callback(event, _get(combo));
+ });
+ };
+ }
+
+ var publicApi = {
+ add : _add,
+ del : _del,
+ get : _get,
+ bindTo : bindTo,
+ template : this.template,
+ toggleCheatSheet : toggleCheatSheet,
+ includeCheatSheet : this.includeCheatSheet,
+ cheatSheetHotkey : this.cheatSheetHotkey,
+ cheatSheetDescription : this.cheatSheetDescription,
+ useNgRoute : this.useNgRoute,
+ purgeHotkeys : purgeHotkeys,
+ templateTitle : this.templateTitle,
+ pause : pause,
+ unpause : unpause
+ };
+
+ return publicApi;
+
+ }];
+
+
+ }])
+
+ .directive('hotkey', ['hotkeys', function (hotkeys) {
+ return {
+ restrict: 'A',
+ link: function (scope, el, attrs) {
+ var keys = [],
+ allowIn;
+
+ angular.forEach(scope.$eval(attrs.hotkey), function (func, hotkey) {
+ // split and trim the hotkeys string into array
+ allowIn = typeof attrs.hotkeyAllowIn === "string" ? attrs.hotkeyAllowIn.split(/[\s,]+/) : [];
+
+ keys.push(hotkey);
+
+ hotkeys.add({
+ combo: hotkey,
+ description: attrs.hotkeyDescription,
+ callback: func,
+ action: attrs.hotkeyAction,
+ allowIn: allowIn
+ });
+ });
+
+ // remove the hotkey if the directive is destroyed:
+ el.bind('$destroy', function() {
+ angular.forEach(keys, hotkeys.del);
+ });
+ }
+ };
+ }])
+
+ .run(['hotkeys', function(hotkeys) {
+ // force hotkeys to run by injecting it. Without this, hotkeys only runs
+ // when a controller or something else asks for it via DI.
+ }]);
+
+})();
+
+/*global define:false */
+/**
+ * Copyright 2015 Craig Campbell
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Mousetrap is a simple keyboard shortcut library for Javascript with
+ * no external dependencies
+ *
+ * @version 1.5.2
+ * @url craig.is/killing/mice
+ */
+(function(window, document, undefined) {
+
+ /**
+ * mapping of special keycodes to their corresponding keys
+ *
+ * everything in this dictionary cannot use keypress events
+ * so it has to be here to map to the correct keycodes for
+ * keyup/keydown events
+ *
+ * @type {Object}
+ */
+ var _MAP = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 16: 'shift',
+ 17: 'ctrl',
+ 18: 'alt',
+ 20: 'capslock',
+ 27: 'esc',
+ 32: 'space',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 45: 'ins',
+ 46: 'del',
+ 91: 'meta',
+ 93: 'meta',
+ 224: 'meta'
+ };
+
+ /**
+ * mapping for special characters so they can support
+ *
+ * this dictionary is only used incase you want to bind a
+ * keyup or keydown event to one of these keys
+ *
+ * @type {Object}
+ */
+ var _KEYCODE_MAP = {
+ 106: '*',
+ 107: '+',
+ 109: '-',
+ 110: '.',
+ 111 : '/',
+ 186: ';',
+ 187: '=',
+ 188: ',',
+ 189: '-',
+ 190: '.',
+ 191: '/',
+ 192: '`',
+ 219: '[',
+ 220: '\\',
+ 221: ']',
+ 222: '\''
+ };
+
+ /**
+ * this is a mapping of keys that require shift on a US keypad
+ * back to the non shift equivelents
+ *
+ * this is so you can use keyup events with these keys
+ *
+ * note that this will only work reliably on US keyboards
+ *
+ * @type {Object}
+ */
+ var _SHIFT_MAP = {
+ '~': '`',
+ '!': '1',
+ '@': '2',
+ '#': '3',
+ '$': '4',
+ '%': '5',
+ '^': '6',
+ '&': '7',
+ '*': '8',
+ '(': '9',
+ ')': '0',
+ '_': '-',
+ '+': '=',
+ ':': ';',
+ '\"': '\'',
+ '<': ',',
+ '>': '.',
+ '?': '/',
+ '|': '\\'
+ };
+
+ /**
+ * this is a list of special strings you can use to map
+ * to modifier keys when you specify your keyboard shortcuts
+ *
+ * @type {Object}
+ */
+ var _SPECIAL_ALIASES = {
+ 'option': 'alt',
+ 'command': 'meta',
+ 'return': 'enter',
+ 'escape': 'esc',
+ 'plus': '+',
+ 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
+ };
+
+ /**
+ * variable to store the flipped version of _MAP from above
+ * needed to check if we should use keypress or not when no action
+ * is specified
+ *
+ * @type {Object|undefined}
+ */
+ var _REVERSE_MAP;
+
+ /**
+ * loop through the f keys, f1 to f19 and add them to the map
+ * programatically
+ */
+ for (var i = 1; i < 20; ++i) {
+ _MAP[111 + i] = 'f' + i;
+ }
+
+ /**
+ * loop through to map numbers on the numeric keypad
+ */
+ for (i = 0; i <= 9; ++i) {
+ _MAP[i + 96] = i;
+ }
+
+ /**
+ * cross browser add event method
+ *
+ * @param {Element|HTMLDocument} object
+ * @param {string} type
+ * @param {Function} callback
+ * @returns void
+ */
+ function _addEvent(object, type, callback) {
+ if (object.addEventListener) {
+ object.addEventListener(type, callback, false);
+ return;
+ }
+
+ object.attachEvent('on' + type, callback);
+ }
+
+ /**
+ * takes the event and returns the key character
+ *
+ * @param {Event} e
+ * @return {string}
+ */
+ function _characterFromEvent(e) {
+
+ // for keypress events we should return the character as is
+ if (e.type == 'keypress') {
+ var character = String.fromCharCode(e.which);
+
+ // if the shift key is not pressed then it is safe to assume
+ // that we want the character to be lowercase. this means if
+ // you accidentally have caps lock on then your key bindings
+ // will continue to work
+ //
+ // the only side effect that might not be desired is if you
+ // bind something like 'A' cause you want to trigger an
+ // event when capital A is pressed caps lock will no longer
+ // trigger the event. shift+a will though.
+ if (!e.shiftKey) {
+ character = character.toLowerCase();
+ }
+
+ return character;
+ }
+
+ // for non keypress events the special maps are needed
+ if (_MAP[e.which]) {
+ return _MAP[e.which];
+ }
+
+ if (_KEYCODE_MAP[e.which]) {
+ return _KEYCODE_MAP[e.which];
+ }
+
+ // if it is not in the special map
+
+ // with keydown and keyup events the character seems to always
+ // come in as an uppercase character whether you are pressing shift
+ // or not. we should make sure it is always lowercase for comparisons
+ return String.fromCharCode(e.which).toLowerCase();
+ }
+
+ /**
+ * checks if two arrays are equal
+ *
+ * @param {Array} modifiers1
+ * @param {Array} modifiers2
+ * @returns {boolean}
+ */
+ function _modifiersMatch(modifiers1, modifiers2) {
+ return modifiers1.sort().join(',') === modifiers2.sort().join(',');
+ }
+
+ /**
+ * takes a key event and figures out what the modifiers are
+ *
+ * @param {Event} e
+ * @returns {Array}
+ */
+ function _eventModifiers(e) {
+ var modifiers = [];
+
+ if (e.shiftKey) {
+ modifiers.push('shift');
+ }
+
+ if (e.altKey) {
+ modifiers.push('alt');
+ }
+
+ if (e.ctrlKey) {
+ modifiers.push('ctrl');
+ }
+
+ if (e.metaKey) {
+ modifiers.push('meta');
+ }
+
+ return modifiers;
+ }
+
+ /**
+ * prevents default for this event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _preventDefault(e) {
+ if (e.preventDefault) {
+ e.preventDefault();
+ return;
+ }
+
+ e.returnValue = false;
+ }
+
+ /**
+ * stops propogation for this event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _stopPropagation(e) {
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ return;
+ }
+
+ e.cancelBubble = true;
+ }
+
+ /**
+ * determines if the keycode specified is a modifier key or not
+ *
+ * @param {string} key
+ * @returns {boolean}
+ */
+ function _isModifier(key) {
+ return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
+ }
+
+ /**
+ * reverses the map lookup so that we can look for specific keys
+ * to see what can and can't use keypress
+ *
+ * @return {Object}
+ */
+ function _getReverseMap() {
+ if (!_REVERSE_MAP) {
+ _REVERSE_MAP = {};
+ for (var key in _MAP) {
+
+ // pull out the numeric keypad from here cause keypress should
+ // be able to detect the keys from the character
+ if (key > 95 && key < 112) {
+ continue;
+ }
+
+ if (_MAP.hasOwnProperty(key)) {
+ _REVERSE_MAP[_MAP[key]] = key;
+ }
+ }
+ }
+ return _REVERSE_MAP;
+ }
+
+ /**
+ * picks the best action based on the key combination
+ *
+ * @param {string} key - character for key
+ * @param {Array} modifiers
+ * @param {string=} action passed in
+ */
+ function _pickBestAction(key, modifiers, action) {
+
+ // if no action was picked in we should try to pick the one
+ // that we think would work best for this key
+ if (!action) {
+ action = _getReverseMap()[key] ? 'keydown' : 'keypress';
+ }
+
+ // modifier keys don't work as expected with keypress,
+ // switch to keydown
+ if (action == 'keypress' && modifiers.length) {
+ action = 'keydown';
+ }
+
+ return action;
+ }
+
+ /**
+ * Converts from a string key combination to an array
+ *
+ * @param {string} combination like "command+shift+l"
+ * @return {Array}
+ */
+ function _keysFromString(combination) {
+ if (combination === '+') {
+ return ['+'];
+ }
+
+ combination = combination.replace(/\+{2}/g, '+plus');
+ return combination.split('+');
+ }
+
+ /**
+ * Gets info for a specific key combination
+ *
+ * @param {string} combination key combination ("command+s" or "a" or "*")
+ * @param {string=} action
+ * @returns {Object}
+ */
+ function _getKeyInfo(combination, action) {
+ var keys;
+ var key;
+ var i;
+ var modifiers = [];
+
+ // take the keys from this pattern and figure out what the actual
+ // pattern is all about
+ keys = _keysFromString(combination);
+
+ for (i = 0; i < keys.length; ++i) {
+ key = keys[i];
+
+ // normalize key names
+ if (_SPECIAL_ALIASES[key]) {
+ key = _SPECIAL_ALIASES[key];
+ }
+
+ // if this is not a keypress event then we should
+ // be smart about using shift keys
+ // this will only work for US keyboards however
+ if (action && action != 'keypress' && _SHIFT_MAP[key]) {
+ key = _SHIFT_MAP[key];
+ modifiers.push('shift');
+ }
+
+ // if this key is a modifier then add it to the list of modifiers
+ if (_isModifier(key)) {
+ modifiers.push(key);
+ }
+ }
+
+ // depending on what the key combination is
+ // we will try to pick the best event for it
+ action = _pickBestAction(key, modifiers, action);
+
+ return {
+ key: key,
+ modifiers: modifiers,
+ action: action
+ };
+ }
+
+ function _belongsTo(element, ancestor) {
+ if (element === document) {
+ return false;
+ }
+
+ if (element === ancestor) {
+ return true;
+ }
+
+ return _belongsTo(element.parentNode, ancestor);
+ }
+
+ function Mousetrap(targetElement) {
+ var self = this;
+
+ targetElement = targetElement || document;
+
+ if (!(self instanceof Mousetrap)) {
+ return new Mousetrap(targetElement);
+ }
+
+ /**
+ * element to attach key events to
+ *
+ * @type {Element}
+ */
+ self.target = targetElement;
+
+ /**
+ * a list of all the callbacks setup via Mousetrap.bind()
+ *
+ * @type {Object}
+ */
+ self._callbacks = {};
+
+ /**
+ * direct map of string combinations to callbacks used for trigger()
+ *
+ * @type {Object}
+ */
+ self._directMap = {};
+
+ /**
+ * keeps track of what level each sequence is at since multiple
+ * sequences can start out with the same sequence
+ *
+ * @type {Object}
+ */
+ var _sequenceLevels = {};
+
+ /**
+ * variable to store the setTimeout call
+ *
+ * @type {null|number}
+ */
+ var _resetTimer;
+
+ /**
+ * temporary state where we will ignore the next keyup
+ *
+ * @type {boolean|string}
+ */
+ var _ignoreNextKeyup = false;
+
+ /**
+ * temporary state where we will ignore the next keypress
+ *
+ * @type {boolean}
+ */
+ var _ignoreNextKeypress = false;
+
+ /**
+ * are we currently inside of a sequence?
+ * type of action ("keyup" or "keydown" or "keypress") or false
+ *
+ * @type {boolean|string}
+ */
+ var _nextExpectedAction = false;
+
+ /**
+ * resets all sequence counters except for the ones passed in
+ *
+ * @param {Object} doNotReset
+ * @returns void
+ */
+ function _resetSequences(doNotReset) {
+ doNotReset = doNotReset || {};
+
+ var activeSequences = false,
+ key;
+
+ for (key in _sequenceLevels) {
+ if (doNotReset[key]) {
+ activeSequences = true;
+ continue;
+ }
+ _sequenceLevels[key] = 0;
+ }
+
+ if (!activeSequences) {
+ _nextExpectedAction = false;
+ }
+ }
+
+ /**
+ * finds all callbacks that match based on the keycode, modifiers,
+ * and action
+ *
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {Event|Object} e
+ * @param {string=} sequenceName - name of the sequence we are looking for
+ * @param {string=} combination
+ * @param {number=} level
+ * @returns {Array}
+ */
+ function _getMatches(character, modifiers, e, sequenceName, combination, level) {
+ var i;
+ var callback;
+ var matches = [];
+ var action = e.type;
+
+ // if there are no events related to this keycode
+ if (!self._callbacks[character]) {
+ return [];
+ }
+
+ // if a modifier key is coming up on its own we should allow it
+ if (action == 'keyup' && _isModifier(character)) {
+ modifiers = [character];
+ }
+
+ // loop through all callbacks for the key that was pressed
+ // and see if any of them match
+ for (i = 0; i < self._callbacks[character].length; ++i) {
+ callback = self._callbacks[character][i];
+
+ // if a sequence name is not specified, but this is a sequence at
+ // the wrong level then move onto the next match
+ if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
+ continue;
+ }
+
+ // if the action we are looking for doesn't match the action we got
+ // then we should keep going
+ if (action != callback.action) {
+ continue;
+ }
+
+ // if this is a keypress event and the meta key and control key
+ // are not pressed that means that we need to only look at the
+ // character, otherwise check the modifiers as well
+ //
+ // chrome will not fire a keypress if meta or control is down
+ // safari will fire a keypress if meta or meta+shift is down
+ // firefox will fire a keypress if meta or control is down
+ if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
+
+ // when you bind a combination or sequence a second time it
+ // should overwrite the first one. if a sequenceName or
+ // combination is specified in this call it does just that
+ //
+ // @todo make deleting its own method?
+ var deleteCombo = !sequenceName && callback.combo == combination;
+ var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
+ if (deleteCombo || deleteSequence) {
+ self._callbacks[character].splice(i, 1);
+ }
+
+ matches.push(callback);
+ }
+ }
+
+ return matches;
+ }
+
+ /**
+ * actually calls the callback function
+ *
+ * if your callback function returns false this will use the jquery
+ * convention - prevent default and stop propogation on the event
+ *
+ * @param {Function} callback
+ * @param {Event} e
+ * @returns void
+ */
+ function _fireCallback(callback, e, combo, sequence) {
+
+ // if this event should not happen stop here
+ if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
+ return;
+ }
+
+ if (callback(e, combo) === false) {
+ _preventDefault(e);
+ _stopPropagation(e);
+ }
+ }
+
+ /**
+ * handles a character key event
+ *
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {Event} e
+ * @returns void
+ */
+ self._handleKey = function(character, modifiers, e) {
+ var callbacks = _getMatches(character, modifiers, e);
+ var i;
+ var doNotReset = {};
+ var maxLevel = 0;
+ var processedSequenceCallback = false;
+
+ // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
+ for (i = 0; i < callbacks.length; ++i) {
+ if (callbacks[i].seq) {
+ maxLevel = Math.max(maxLevel, callbacks[i].level);
+ }
+ }
+
+ // loop through matching callbacks for this key event
+ for (i = 0; i < callbacks.length; ++i) {
+
+ // fire for all sequence callbacks
+ // this is because if for example you have multiple sequences
+ // bound such as "g i" and "g t" they both need to fire the
+ // callback for matching g cause otherwise you can only ever
+ // match the first one
+ if (callbacks[i].seq) {
+
+ // only fire callbacks for the maxLevel to prevent
+ // subsequences from also firing
+ //
+ // for example 'a option b' should not cause 'option b' to fire
+ // even though 'option b' is part of the other sequence
+ //
+ // any sequences that do not match here will be discarded
+ // below by the _resetSequences call
+ if (callbacks[i].level != maxLevel) {
+ continue;
+ }
+
+ processedSequenceCallback = true;
+
+ // keep a list of which sequences were matches for later
+ doNotReset[callbacks[i].seq] = 1;
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
+ continue;
+ }
+
+ // if there were no sequence matches but we are still here
+ // that means this is a regular match so we should fire that
+ if (!processedSequenceCallback) {
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
+ }
+ }
+
+ // if the key you pressed matches the type of sequence without
+ // being a modifier (ie "keyup" or "keypress") then we should
+ // reset all sequences that were not matched by this event
+ //
+ // this is so, for example, if you have the sequence "h a t" and you
+ // type "h e a r t" it does not match. in this case the "e" will
+ // cause the sequence to reset
+ //
+ // modifier keys are ignored because you can have a sequence
+ // that contains modifiers such as "enter ctrl+space" and in most
+ // cases the modifier key will be pressed before the next key
+ //
+ // also if you have a sequence such as "ctrl+b a" then pressing the
+ // "b" key will trigger a "keypress" and a "keydown"
+ //
+ // the "keydown" is expected when there is a modifier, but the
+ // "keypress" ends up matching the _nextExpectedAction since it occurs
+ // after and that causes the sequence to reset
+ //
+ // we ignore keypresses in a sequence that directly follow a keydown
+ // for the same character
+ var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
+ if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
+ _resetSequences(doNotReset);
+ }
+
+ _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
+ };
+
+ /**
+ * handles a keydown event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _handleKeyEvent(e) {
+
+ // normalize e.which for key events
+ // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
+ if (typeof e.which !== 'number') {
+ e.which = e.keyCode;
+ }
+
+ var character = _characterFromEvent(e);
+
+ // no character found then stop
+ if (!character) {
+ return;
+ }
+
+ // need to use === for the character check because the character can be 0
+ if (e.type == 'keyup' && _ignoreNextKeyup === character) {
+ _ignoreNextKeyup = false;
+ return;
+ }
+
+ self.handleKey(character, _eventModifiers(e), e);
+ }
+
+ /**
+ * called to set a 1 second timeout on the specified sequence
+ *
+ * this is so after each key press in the sequence you have 1 second
+ * to press the next key before you have to start over
+ *
+ * @returns void
+ */
+ function _resetSequenceTimer() {
+ clearTimeout(_resetTimer);
+ _resetTimer = setTimeout(_resetSequences, 1000);
+ }
+
+ /**
+ * binds a key sequence to an event
+ *
+ * @param {string} combo - combo specified in bind call
+ * @param {Array} keys
+ * @param {Function} callback
+ * @param {string=} action
+ * @returns void
+ */
+ function _bindSequence(combo, keys, callback, action) {
+
+ // start off by adding a sequence level record for this combination
+ // and setting the level to 0
+ _sequenceLevels[combo] = 0;
+
+ /**
+ * callback to increase the sequence level for this sequence and reset
+ * all other sequences that were active
+ *
+ * @param {string} nextAction
+ * @returns {Function}
+ */
+ function _increaseSequence(nextAction) {
+ return function() {
+ _nextExpectedAction = nextAction;
+ ++_sequenceLevels[combo];
+ _resetSequenceTimer();
+ };
+ }
+
+ /**
+ * wraps the specified callback inside of another function in order
+ * to reset all sequence counters as soon as this sequence is done
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _callbackAndReset(e) {
+ _fireCallback(callback, e, combo);
+
+ // we should ignore the next key up if the action is key down
+ // or keypress. this is so if you finish a sequence and
+ // release the key the final key will not trigger a keyup
+ if (action !== 'keyup') {
+ _ignoreNextKeyup = _characterFromEvent(e);
+ }
+
+ // weird race condition if a sequence ends with the key
+ // another sequence begins with
+ setTimeout(_resetSequences, 10);
+ }
+
+ // loop through keys one at a time and bind the appropriate callback
+ // function. for any key leading up to the final one it should
+ // increase the sequence. after the final, it should reset all sequences
+ //
+ // if an action is specified in the original bind call then that will
+ // be used throughout. otherwise we will pass the action that the
+ // next key in the sequence should match. this allows a sequence
+ // to mix and match keypress and keydown events depending on which
+ // ones are better suited to the key provided
+ for (var i = 0; i < keys.length; ++i) {
+ var isFinal = i + 1 === keys.length;
+ var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
+ _bindSingle(keys[i], wrappedCallback, action, combo, i);
+ }
+ }
+
+ /**
+ * binds a single keyboard combination
+ *
+ * @param {string} combination
+ * @param {Function} callback
+ * @param {string=} action
+ * @param {string=} sequenceName - name of sequence if part of sequence
+ * @param {number=} level - what part of the sequence the command is
+ * @returns void
+ */
+ function _bindSingle(combination, callback, action, sequenceName, level) {
+
+ // store a direct mapped reference for use with Mousetrap.trigger
+ self._directMap[combination + ':' + action] = callback;
+
+ // make sure multiple spaces in a row become a single space
+ combination = combination.replace(/\s+/g, ' ');
+
+ var sequence = combination.split(' ');
+ var info;
+
+ // if this pattern is a sequence of keys then run through this method
+ // to reprocess each pattern one key at a time
+ if (sequence.length > 1) {
+ _bindSequence(combination, sequence, callback, action);
+ return;
+ }
+
+ info = _getKeyInfo(combination, action);
+
+ // make sure to initialize array if this is the first time
+ // a callback is added for this key
+ self._callbacks[info.key] = self._callbacks[info.key] || [];
+
+ // remove an existing match if there is one
+ _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
+
+ // add this call back to the array
+ // if it is a sequence put it at the beginning
+ // if not put it at the end
+ //
+ // this is important because the way these are processed expects
+ // the sequence ones to come first
+ self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({
+ callback: callback,
+ modifiers: info.modifiers,
+ action: info.action,
+ seq: sequenceName,
+ level: level,
+ combo: combination
+ });
+ }
+
+ /**
+ * binds multiple combinations to the same callback
+ *
+ * @param {Array} combinations
+ * @param {Function} callback
+ * @param {string|undefined} action
+ * @returns void
+ */
+ self._bindMultiple = function(combinations, callback, action) {
+ for (var i = 0; i < combinations.length; ++i) {
+ _bindSingle(combinations[i], callback, action);
+ }
+ };
+
+ // start!
+ _addEvent(targetElement, 'keypress', _handleKeyEvent);
+ _addEvent(targetElement, 'keydown', _handleKeyEvent);
+ _addEvent(targetElement, 'keyup', _handleKeyEvent);
+ }
+
+ /**
+ * binds an event to mousetrap
+ *
+ * can be a single key, a combination of keys separated with +,
+ * an array of keys, or a sequence of keys separated by spaces
+ *
+ * be sure to list the modifier keys first to make sure that the
+ * correct key ends up getting bound (the last key in the pattern)
+ *
+ * @param {string|Array} keys
+ * @param {Function} callback
+ * @param {string=} action - 'keypress', 'keydown', or 'keyup'
+ * @returns void
+ */
+ Mousetrap.prototype.bind = function(keys, callback, action) {
+ var self = this;
+ keys = keys instanceof Array ? keys : [keys];
+ self._bindMultiple.call(self, keys, callback, action);
+ return self;
+ };
+
+ /**
+ * unbinds an event to mousetrap
+ *
+ * the unbinding sets the callback function of the specified key combo
+ * to an empty function and deletes the corresponding key in the
+ * _directMap dict.
+ *
+ * TODO: actually remove this from the _callbacks dictionary instead
+ * of binding an empty function
+ *
+ * the keycombo+action has to be exactly the same as
+ * it was defined in the bind method
+ *
+ * @param {string|Array} keys
+ * @param {string} action
+ * @returns void
+ */
+ Mousetrap.prototype.unbind = function(keys, action) {
+ var self = this;
+ return self.bind.call(self, keys, function() {}, action);
+ };
+
+ /**
+ * triggers an event that has already been bound
+ *
+ * @param {string} keys
+ * @param {string=} action
+ * @returns void
+ */
+ Mousetrap.prototype.trigger = function(keys, action) {
+ var self = this;
+ if (self._directMap[keys + ':' + action]) {
+ self._directMap[keys + ':' + action]({}, keys);
+ }
+ return self;
+ };
+
+ /**
+ * resets the library back to its initial state. this is useful
+ * if you want to clear out the current keyboard shortcuts and bind
+ * new ones - for example if you switch to another page
+ *
+ * @returns void
+ */
+ Mousetrap.prototype.reset = function() {
+ var self = this;
+ self._callbacks = {};
+ self._directMap = {};
+ return self;
+ };
+
+ /**
+ * should we stop this event before firing off callbacks
+ *
+ * @param {Event} e
+ * @param {Element} element
+ * @return {boolean}
+ */
+ Mousetrap.prototype.stopCallback = function(e, element) {
+ var self = this;
+
+ // if the element has the class "mousetrap" then no need to stop
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
+ return false;
+ }
+
+ if (_belongsTo(element, self.target)) {
+ return false;
+ }
+
+ // stop for input, select, and textarea
+ return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
+ };
+
+ /**
+ * exposes _handleKey publicly so it can be overwritten by extensions
+ */
+ Mousetrap.prototype.handleKey = function() {
+ var self = this;
+ return self._handleKey.apply(self, arguments);
+ };
+
+ /**
+ * Init the global mousetrap functions
+ *
+ * This method is needed to allow the global mousetrap functions to work
+ * now that mousetrap is a constructor function.
+ */
+ Mousetrap.init = function() {
+ var documentMousetrap = Mousetrap(document);
+ for (var method in documentMousetrap) {
+ if (method.charAt(0) !== '_') {
+ Mousetrap[method] = (function(method) {
+ return function() {
+ return documentMousetrap[method].apply(documentMousetrap, arguments);
+ };
+ } (method));
+ }
+ }
+ };
+
+ Mousetrap.init();
+
+ // expose mousetrap to the global object
+ window.Mousetrap = Mousetrap;
+
+ // expose as a common js module
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Mousetrap;
+ }
+
+ // expose mousetrap as an AMD module
+ if (typeof define === 'function' && define.amd) {
+ define(function() {
+ return Mousetrap;
+ });
+ }
+}) (window, document);