From 57a735753fd43f42342f69cd81535a60f9cdd8a9 Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Tue, 27 Sep 2016 15:19:24 -0400 Subject: [PATCH] (js,css) Improve keyboard shortcuts - Defined some hotkeys in all modules; - Added generation of cheat sheet. --- NEWS | 1 + UI/Common/English.lproj/Localizable.strings | 50 +++++- UI/Contacts/English.lproj/Localizable.strings | 6 + UI/MailerUI/English.lproj/Localizable.strings | 5 +- .../English.lproj/Localizable.strings | 23 ++- UI/Templates/SchedulerUI/UIxCalDayView.wox | 8 +- UI/Templates/SchedulerUI/UIxCalMainView.wox | 5 +- UI/Templates/SchedulerUI/UIxCalMonthView.wox | 8 +- UI/Templates/SchedulerUI/UIxCalWeekView.wox | 8 +- .../js/Common/sgHotkeys.service.js | 94 ++++++++++- .../js/Contacts/AddressBook.service.js | 12 +- .../js/Contacts/AddressBookController.js | 158 ++++++++++++++++-- .../js/Contacts/AddressBooksController.js | 43 ++++- .../js/Contacts/CardController.js | 38 ++++- .../js/Mailer/MailboxController.js | 42 +++-- .../js/Mailer/MailboxesController.js | 34 +++- .../js/Mailer/MessageController.js | 6 +- .../js/Scheduler/CalendarController.js | 91 +++++++++- .../js/Scheduler/CalendarListController.js | 50 +++++- .../scss/components/hotkeys/hotkeys.scss | 21 +++ UI/WebServerResources/scss/styles.scss | 1 + 21 files changed, 630 insertions(+), 74 deletions(-) create mode 100644 UI/WebServerResources/scss/components/hotkeys/hotkeys.scss diff --git a/NEWS b/NEWS index 83f9fef00..eeb57ebbf 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,7 @@ New features - [web] added IMAP folder subscriptions management (#255) + - [web] keyboard shortcuts - [eas] initial support for server-side mailbox search operations Enhancements diff --git a/UI/Common/English.lproj/Localizable.strings b/UI/Common/English.lproj/Localizable.strings index cc195d36d..76f795465 100644 --- a/UI/Common/English.lproj/Localizable.strings +++ b/UI/Common/English.lproj/Localizable.strings @@ -119,4 +119,52 @@ "Wrong username or password." = "Wrong username or password."; /* Error message display bellow search field when the search string has less than the required number of characters */ -"Enter at least %{minimumSearchLength} characters" = "Enter at least %{minimumSearchLength} characters"; \ No newline at end of file +"Enter at least %{minimumSearchLength} characters" = "Enter at least %{minimumSearchLength} characters"; + +/* Question mark shows list of hotkeys */ +"Show or hide this help" = "Show or hide this help"; + +/* Space key */ +"key_space" = "space"; + +/* Up arrow key */ +"key_up" = "↑"; + +/* Down arrow key */ +"key_down" = "↓"; + +/* Left arrow key */ +"key_left" = "←"; + +/* Right arrow key */ +"key_right" = "→"; + +/* Shift and up arrow combo keys */ +"key_shift+up" = "shift + ↑"; + +/* Shift and down arrow combo keys */ +"key_shift+down" = "shift + ↓"; + +/* Backspace key */ +"key_backspace" = "backspace"; + +/* Hotkey to start a search */ +"hotkey_search" = "s"; + +/* Hotkey description to select next list item */ +"View next item" = "View next item"; + +/* Hotkey description to select previous list item */ +"View previous item" = "View previous item"; + +/* Hotkey description to add next list item to selection */ +"Add next item to selection" = "Add next item to selection"; + +/* Hotkey description to add previous list item to selection */ +"Add previous item to selection" = "Add previous item to selection"; + +/* Hotkey description to move backward in current view */ +"Move backward" = "Move backward"; + +/* Hotkey description to move forward in current view */ +"Move forward" = "Move forward"; \ No newline at end of file diff --git a/UI/Contacts/English.lproj/Localizable.strings b/UI/Contacts/English.lproj/Localizable.strings index 6dce6e5e1..21d62b4ac 100644 --- a/UI/Contacts/English.lproj/Localizable.strings +++ b/UI/Contacts/English.lproj/Localizable.strings @@ -253,3 +253,9 @@ /* Aria label for avatar button to select and unselect a card */ "Toggle item" = "Toggle item"; + +/* Hotkey to create a new card */ +"key_create_card" = "c"; + +/* Hotkey to create a new list */ +"key_create_list" = "l"; \ No newline at end of file diff --git a/UI/MailerUI/English.lproj/Localizable.strings b/UI/MailerUI/English.lproj/Localizable.strings index 35b7841eb..94ee8f7d2 100644 --- a/UI/MailerUI/English.lproj/Localizable.strings +++ b/UI/MailerUI/English.lproj/Localizable.strings @@ -365,4 +365,7 @@ "Search scope" = "Search scope"; /* Subscriptions Dialog */ -"Manage Subscriptions" = "Manage Subscriptions"; \ No newline at end of file +"Manage Subscriptions" = "Manage Subscriptions"; + +/* Hotkey to write a new message */ +"hotkey_compose" = "w"; \ No newline at end of file diff --git a/UI/Scheduler/English.lproj/Localizable.strings b/UI/Scheduler/English.lproj/Localizable.strings index f3d76c9ab..97ed7027b 100644 --- a/UI/Scheduler/English.lproj/Localizable.strings +++ b/UI/Scheduler/English.lproj/Localizable.strings @@ -579,4 +579,25 @@ vtodo_class2 = "(Confidential task)"; "Toggle item" = "Toggle item"; /* Aria label for scope of search on events or tasks */ -"Search scope" = "Search scope"; \ No newline at end of file +"Search scope" = "Search scope"; + +/* Hotkey to create an event */ +"hotkey_create_event" = "e"; + +/* Hotkey to create a task */ +"hotkey_create_task" = "t"; + +/* Hotkey to go to today */ +"hotkey_today" = "n"; + +/* Hotkey to switch to day view */ +"hotkey_dayview" = "d"; + +/* Hotkey to switch to week view */ +"hotkey_weekview" = "w"; + +/* Hotkey to switch to month view */ +"hotkey_monthview" = "m"; + +/* Hotkey to switch to multicolumn day view */ +"hotkey_multicolumndayview" = "c"; \ No newline at end of file diff --git a/UI/Templates/SchedulerUI/UIxCalDayView.wox b/UI/Templates/SchedulerUI/UIxCalDayView.wox index 02dd6ea58..a41b19dd1 100644 --- a/UI/Templates/SchedulerUI/UIxCalDayView.wox +++ b/UI/Templates/SchedulerUI/UIxCalDayView.wox @@ -34,16 +34,16 @@ view_day + ng-click="calendar.changeView($event, 'day')">view_day view_week + ng-click="calendar.changeView($event, 'week')">view_week view_module + ng-click="calendar.changeView($event, 'month')">view_module view_array + ng-click="calendar.changeView($event, 'multicolumnday')">view_array
+ ng-click="list.searchMode()"> search
@@ -530,7 +530,8 @@ arrow_back - +
diff --git a/UI/Templates/SchedulerUI/UIxCalMonthView.wox b/UI/Templates/SchedulerUI/UIxCalMonthView.wox index bed5da909..a58798e7c 100644 --- a/UI/Templates/SchedulerUI/UIxCalMonthView.wox +++ b/UI/Templates/SchedulerUI/UIxCalMonthView.wox @@ -33,17 +33,17 @@ view_day + ng-click="calendar.changeView($event, 'day')">view_day view_week + ng-click="calendar.changeView($event, 'week')">view_week view_module + ng-click="calendar.changeView($event, 'month')">view_module view_array + ng-click="calendar.changeView($event, 'multicolumnday')">view_array diff --git a/UI/Templates/SchedulerUI/UIxCalWeekView.wox b/UI/Templates/SchedulerUI/UIxCalWeekView.wox index eb436ef47..abe17ef25 100644 --- a/UI/Templates/SchedulerUI/UIxCalWeekView.wox +++ b/UI/Templates/SchedulerUI/UIxCalWeekView.wox @@ -33,17 +33,17 @@ view_day + ng-click="calendar.changeView($event, 'day')">view_day view_week + ng-click="calendar.changeView($event, 'week')">view_week view_module + ng-click="calendar.changeView($event, 'month')">view_module view_array + ng-click="calendar.changeView($event, 'multicolumnday')">view_array 1) + // Automatically translate common hotkeys + this.lkey = l('key_' + this.key); }; HotKey.prototype.clone = function() { @@ -114,10 +122,11 @@ /** * Keybindings are ignored by default when coming from a form input field. */ - this._preventIn = ['INPUT', 'SELECT', 'MD-SELECT', 'TEXTAREA']; - + this._preventIn = ['INPUT', 'BUTTON', 'SELECT', 'TEXTAREA']; + this._onKeydown = this._onKeydown.bind(this); this._onKeyup = this._onKeyup.bind(this); + this._onKeypress = this._onKeypress.bind(this); this.initialize(); }; @@ -126,8 +135,17 @@ * 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); }; /** @@ -176,6 +194,20 @@ } }; + /** + * 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 @@ -360,12 +392,62 @@ 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: [ + '', + ' ', + ' ', + ' ', + '
', + ' {{keys[0].lkey || hotkey}}', + '
', + ' {{keys[0].description}}', + '
', + '
', + '
', + '
' + ].join(''), + controller: CheatSheetController, + locals: { + hotkeys: _this._hotkeys + } + }); + } + + CheatSheetController.$inject = ['$scope', 'hotkeys']; + function CheatSheetController($scope, hotkeys) { + $scope.hotkeys = hotkeys; + $scope.closeDialog = function() { + Hotkeys.$modal.hide(); + }; + } + }; + return Hotkeys; } } - sgHotkeys.$inject = ['$sgHotkeys']; - function sgHotkeys($sgHotkeys) { + sgHotkeys.$inject = ['$mdDialog', '$sgHotkeys']; + function sgHotkeys($mdDialog, $sgHotkeys) { + angular.extend($sgHotkeys, { $modal: $mdDialog }); + return new $sgHotkeys(); } diff --git a/UI/WebServerResources/js/Contacts/AddressBook.service.js b/UI/WebServerResources/js/Contacts/AddressBook.service.js index 92aa87844..e176472ff 100644 --- a/UI/WebServerResources/js/Contacts/AddressBook.service.js +++ b/UI/WebServerResources/js/Contacts/AddressBook.service.js @@ -212,7 +212,7 @@ this.$$cards = []; } this.idsMap = {}; - this.$cards = []; // TODO Keep the "selected" state of cards + this.$cards = []; // Extend instance with all attributes of data except headers angular.forEach(data, function(value, key) { if (key != 'headers' && key != 'cards') { @@ -370,6 +370,16 @@ return _.find(this.$cards, function(card) { return card.id == _this.selectedCard; }); }; + /** + * @function $selectedCardIndex + * @memberof AddressBook.prototype + * @desc Return the index of the currently visible card. + * @returns a number or undefined if no card is selected + */ + AddressBook.prototype.$selectedCardIndex = function() { + return _.indexOf(_.map(this.$cards, 'id'), this.selectedCard); + }; + /** * @function $selectedCards * @memberof AddressBook.prototype diff --git a/UI/WebServerResources/js/Contacts/AddressBookController.js b/UI/WebServerResources/js/Contacts/AddressBookController.js index a6c5a38bb..8b1830529 100644 --- a/UI/WebServerResources/js/Contacts/AddressBookController.js +++ b/UI/WebServerResources/js/Contacts/AddressBookController.js @@ -6,9 +6,9 @@ /** * @ngInject */ - AddressBookController.$inject = ['$scope', '$q', '$window', '$state', '$timeout', '$mdDialog', '$mdToast', 'Account', 'Card', 'AddressBook', 'Dialog', 'sgSettings', 'stateAddressbooks', 'stateAddressbook']; - function AddressBookController($scope, $q, $window, $state, $timeout, $mdDialog, $mdToast, Account, Card, AddressBook, Dialog, Settings, stateAddressbooks, stateAddressbook) { - var vm = this; + AddressBookController.$inject = ['$scope', '$q', '$window', '$state', '$timeout', '$mdDialog', '$mdToast', 'Account', 'Card', 'AddressBook', 'Dialog', 'sgSettings', 'sgHotkeys', 'stateAddressbooks', 'stateAddressbook']; + function AddressBookController($scope, $q, $window, $state, $timeout, $mdDialog, $mdToast, Account, Card, AddressBook, Dialog, Settings, sgHotkeys, stateAddressbooks, stateAddressbook) { + var vm = this, hotkeys = []; AddressBook.selectedFolder = stateAddressbook; @@ -29,12 +29,72 @@ vm.newMessageWithSelectedCards = newMessageWithSelectedCards; vm.newMessageWithRecipient = newMessageWithRecipient; vm.mode = { search: false, multiple: 0 }; - + + + _registerHotkeys(hotkeys); + + $scope.$on('$destroy', function() { + // Deregister hotkeys + _.forEach(hotkeys, function(key) { + sgHotkeys.deregisterHotkey(key); + }); + }); + + + function _registerHotkeys(keys) { + keys.push(sgHotkeys.createHotkey({ + key: l('key_create_card'), + description: l('Create a new address book card'), + callback: angular.bind(vm, newComponent, 'card') + })); + keys.push(sgHotkeys.createHotkey({ + key: l('key_create_list'), + description: l('Create a new list'), + callback: angular.bind(vm, newComponent, 'list') + })); + keys.push(sgHotkeys.createHotkey({ + key: 'space', + description: l('Toggle item'), + callback: toggleCardSelection + })); + keys.push(sgHotkeys.createHotkey({ + key: 'up', + description: l('View next item'), + callback: _nextCard + })); + keys.push(sgHotkeys.createHotkey({ + key: 'down', + description: l('View previous item'), + callback: _previousCard + })); + keys.push(sgHotkeys.createHotkey({ + key: 'shift+up', + description: l('Add next item to selection'), + callback: _addNextCardToSelection + })); + keys.push(sgHotkeys.createHotkey({ + key: 'shift+down', + description: l('Add previous item to selection'), + callback: _addPreviousCardToSelection + })); + keys.push(sgHotkeys.createHotkey({ + key: 'backspace', + description: l('Delete selected card or address book'), + callback: confirmDeleteSelectedCards + })); + + // Register the hotkeys + _.forEach(keys, function(key) { + sgHotkeys.registerHotkey(key); + }); + } + function selectCard(card) { $state.go('app.addressbook.card.view', {cardId: card.id}); } - + function toggleCardSelection($event, card) { + if (!card) card = vm.selectedFolder.$selectedCard(); card.selected = !card.selected; vm.mode.multiple += card.selected? 1 : -1; $event.preventDefault(); @@ -51,20 +111,92 @@ }); vm.mode.multiple = 0; } - - function confirmDeleteSelectedCards() { - Dialog.confirm(l('Warning'), - l('Are you sure you want to delete the selected contacts?'), - { ok: l('Delete') }) + + /** + * User has pressed up arrow key + */ + function _nextCard($event) { + var index = vm.selectedFolder.$selectedCardIndex(); + + if (angular.isDefined(index)) + index--; + else + // No message is selected, show oldest message + index = vm.selectedFolder.$cards.length() - 1; + + if (index > -1) + selectCard(vm.selectedFolder.$cards[index]); + + if (vm.selectedFolder.$topIndex > 0) + vm.selectedFolder.$topIndex--; + + $event.preventDefault(); + + return index; + } + + /** + * User has pressed the down arrow key + */ + function _previousCard($event) { + var index = vm.selectedFolder.$selectedCardIndex(); + + if (angular.isDefined(index)) + index++; + else + // No message is selected, show newest + index = 0; + + if (index < vm.selectedFolder.$cards.length) + selectCard(vm.selectedFolder.$cards[index]); + else + index = -1; + + if (vm.selectedFolder.$topIndex < vm.selectedFolder.$cards.length) + vm.selectedFolder.$topIndex++; + + $event.preventDefault(); + + return index; + } + + function _addNextCardToSelection($event) { + var index; + + if (vm.selectedFolder.hasSelectedCard()) { + index = _nextCard($event); + if (index >= 0) + toggleCardSelection($event, vm.selectedFolder.$cards[index]); + } + } + + function _addPreviousCardToSelection($event) { + var index; + + if (vm.selectedFolder.hasSelectedCard()) { + index = _previousCard($event); + if (index >= 0) + toggleCardSelection($event, vm.selectedFolder.$cards[index]); + } + } + + function confirmDeleteSelectedCards($event) { + var selectedCards = vm.selectedFolder.$selectedCards(); + + if (_.size(selectedCards) > 0) + Dialog.confirm(l('Warning'), + l('Are you sure you want to delete the selected contacts?'), + { ok: l('Delete') }) .then(function() { // User confirmed the deletion - var selectedCards = _.filter(vm.selectedFolder.$cards, function(card) { return card.selected; }); vm.selectedFolder.$deleteCards(selectedCards).then(function() { vm.mode.multiple = 0; if (!vm.selectedFolder.selectedCard) $state.go('app.addressbook'); }); }); + + $event.preventDefault(); } /** @@ -214,6 +346,6 @@ } angular - .module('SOGo.ContactsUI') - .controller('AddressBookController', AddressBookController); + .module('SOGo.ContactsUI') + .controller('AddressBookController', AddressBookController); })(); diff --git a/UI/WebServerResources/js/Contacts/AddressBooksController.js b/UI/WebServerResources/js/Contacts/AddressBooksController.js index 2e1505eec..f465f7956 100644 --- a/UI/WebServerResources/js/Contacts/AddressBooksController.js +++ b/UI/WebServerResources/js/Contacts/AddressBooksController.js @@ -6,9 +6,9 @@ /** * @ngInject */ - AddressBooksController.$inject = ['$state', '$scope', '$rootScope', '$stateParams', '$timeout', '$window', '$mdDialog', '$mdToast', '$mdMedia', '$mdSidenav', 'FileUploader', 'sgConstant', 'sgFocus', 'Card', 'AddressBook', 'Dialog', 'sgSettings', 'User', 'stateAddressbooks']; - function AddressBooksController($state, $scope, $rootScope, $stateParams, $timeout, $window, $mdDialog, $mdToast, $mdMedia, $mdSidenav, FileUploader, sgConstant, focus, Card, AddressBook, Dialog, Settings, User, stateAddressbooks) { - var vm = this; + AddressBooksController.$inject = ['$state', '$scope', '$rootScope', '$stateParams', '$timeout', '$window', '$mdDialog', '$mdToast', '$mdMedia', '$mdSidenav', 'FileUploader', 'sgConstant', 'sgHotkeys', 'sgFocus', 'Card', 'AddressBook', 'Dialog', 'sgSettings', 'User', 'stateAddressbooks']; + function AddressBooksController($state, $scope, $rootScope, $stateParams, $timeout, $window, $mdDialog, $mdToast, $mdMedia, $mdSidenav, FileUploader, sgConstant, sgHotkeys, focus, Card, AddressBook, Dialog, Settings, User, stateAddressbooks) { + var vm = this, hotkeys = []; vm.activeUser = Settings.activeUser; vm.service = AddressBook; @@ -26,6 +26,33 @@ vm.isDroppableFolder = isDroppableFolder; vm.dragSelectedCards = dragSelectedCards; + + _registerHotkeys(hotkeys); + + $scope.$on('$destroy', function() { + // Deregister hotkeys + _.forEach(hotkeys, function(key) { + sgHotkeys.deregisterHotkey(key); + }); + }); + + + function _registerHotkeys(keys) { + keys.push(sgHotkeys.createHotkey({ + key: 'backspace', + description: l('Delete selected card or address book'), + callback: function() { + if (AddressBook.selectedFolder && !AddressBook.selectedFolder.hasSelectedCard()) + confirmDelete(); + } + })); + + // Register the hotkeys + _.forEach(keys, function(key) { + sgHotkeys.registerHotkey(key); + }); + } + function select($event, folder) { if ($state.params.addressbookId != folder.id && vm.editMode != folder.id) { @@ -109,10 +136,12 @@ return true; }) .catch(function(response) { - var message = response.data.message || response.statusText; - Dialog.alert(l('An error occured while deleting the addressbook "%{0}".', - vm.service.selectedFolder.name), - message); + if (response) { + var message = response.data.message || response.statusText; + Dialog.alert(l('An error occured while deleting the addressbook "%{0}".', + vm.service.selectedFolder.name), + message); + } }); } } diff --git a/UI/WebServerResources/js/Contacts/CardController.js b/UI/WebServerResources/js/Contacts/CardController.js index 06b6c5517..a4f5533c5 100644 --- a/UI/WebServerResources/js/Contacts/CardController.js +++ b/UI/WebServerResources/js/Contacts/CardController.js @@ -7,9 +7,9 @@ * Controller to view and edit a card * @ngInject */ - CardController.$inject = ['$scope', '$timeout', '$window', '$mdDialog', 'AddressBook', 'Card', 'Dialog', 'sgFocus', '$state', '$stateParams', 'stateCard']; - function CardController($scope, $timeout, $window, $mdDialog, AddressBook, Card, Dialog, focus, $state, $stateParams, stateCard) { - var vm = this; + CardController.$inject = ['$scope', '$timeout', '$window', '$mdDialog', 'AddressBook', 'Card', 'Dialog', 'sgHotkeys', 'sgFocus', '$state', '$stateParams', 'stateCard']; + function CardController($scope, $timeout, $window, $mdDialog, AddressBook, Card, Dialog, sgHotkeys, focus, $state, $stateParams, stateCard) { + var vm = this, hotkeys = []; vm.card = stateCard; @@ -37,6 +37,34 @@ vm.toggleRawSource = toggleRawSource; vm.showRawSource = false; + + _registerHotkeys(hotkeys); + + $scope.$on('$destroy', function() { + // Deregister hotkeys + _.forEach(hotkeys, function(key) { + sgHotkeys.deregisterHotkey(key); + }); + }); + + + function _registerHotkeys(keys) { + keys.push(sgHotkeys.createHotkey({ + key: 'backspace', + description: l('Delete'), + callback: function($event) { + if (vm.currentFolder.$selectedCount() === 0) + confirmDelete(); + $event.preventDefault(); + } + })); + + // Register the hotkeys + _.forEach(keys, function(key) { + sgHotkeys.registerHotkey(key); + }); + } + function transformCategory(input) { if (angular.isString(input)) return { value: input }; @@ -112,7 +140,9 @@ $state.go('app.addressbook.card.view', { cardId: vm.card.id }); } } - function confirmDelete(card) { + function confirmDelete() { + var card = stateCard; + Dialog.confirm(l('Warning'), l('Are you sure you want to delete the card of %{0}?', '' + card.$fullname() + ''), { ok: l('Delete') }) diff --git a/UI/WebServerResources/js/Mailer/MailboxController.js b/UI/WebServerResources/js/Mailer/MailboxController.js index 5588d0fe9..fc5d241ed 100644 --- a/UI/WebServerResources/js/Mailer/MailboxController.js +++ b/UI/WebServerResources/js/Mailer/MailboxController.js @@ -15,8 +15,6 @@ // Expose controller for eventual popup windows $window.$mailboxController = vm; - stateMailbox.selectFolder(); - vm.service = Mailbox; vm.accounts = stateAccounts; vm.account = stateAccount; @@ -38,6 +36,11 @@ vm.selectAll = selectAll; vm.unselectMessages = unselectMessages; + + stateMailbox.selectFolder(); + + _registerHotkeys(hotkeys); + // Expunge mailbox when leaving the Mail module angular.element($window).on('beforeunload', _compactBeforeUnload); $scope.$on('$destroy', function() { @@ -57,45 +60,55 @@ $window.document.title = title; }); - _registerHotkeys(hotkeys); - function _registerHotkeys(keys) { keys.push(sgHotkeys.createHotkey({ - key: 'c', + key: l('hotkey_search'), + description: l('Search'), + callback: searchMode + })); + keys.push(sgHotkeys.createHotkey({ + key: l('hotkey_compose'), + description: l('Write a new message'), callback: newMessage })); keys.push(sgHotkeys.createHotkey({ key: 'space', + description: l('Toggle item'), callback: toggleMessageSelection })); keys.push(sgHotkeys.createHotkey({ key: 'up', + description: l('View next item'), callback: _nextMessage, preventInClass: ['sg-mail-part'] })); keys.push(sgHotkeys.createHotkey({ key: 'down', + description: l('View previous item'), callback: _previousMessage, preventInClass: ['sg-mail-part'] })); keys.push(sgHotkeys.createHotkey({ key: 'shift+up', + description: l('Add next item to selection'), callback: _addNextMessageToSelection, preventInClass: ['sg-mail-part'] })); keys.push(sgHotkeys.createHotkey({ key: 'shift+down', + description: l('Add previous item to selection'), callback: _addPreviousMessageToSelection, preventInClass: ['sg-mail-part'] })); keys.push(sgHotkeys.createHotkey({ key: 'backspace', + description: l('Delete selected message or folder'), callback: confirmDeleteSelectedMessages })); // Register the hotkeys - _.forEach(hotkeys, function(key) { + _.forEach(keys, function(key) { sgHotkeys.registerHotkey(key); }); } @@ -169,6 +182,9 @@ if (index > -1) selectMessage(vm.selectedFolder.$messages[index]); + if (vm.selectedFolder.$topIndex > 0) + vm.selectedFolder.$topIndex--; + $event.preventDefault(); return index; @@ -191,6 +207,9 @@ else index = -1; + if (vm.selectedFolder.$topIndex < vm.selectedFolder.getLength()) + vm.selectedFolder.$topIndex++; + $event.preventDefault(); return index; @@ -284,10 +303,10 @@ function confirmDeleteSelectedMessages($event) { var selectedMessages = vm.selectedFolder.$selectedMessages(); - if (_.size(selectedMessages) > 0) - Dialog.confirm(l('Warning'), - l('Are you sure you want to delete the selected messages?'), - { ok: l('Delete') }) + if (messageDialog === null && _.size(selectedMessages) > 0) + messageDialog = Dialog.confirm(l('Warning'), + l('Are you sure you want to delete the selected messages?'), + { ok: l('Delete') }) .then(function() { var deleteSelectedMessage = vm.selectedFolder.hasSelectedMessage(); vm.selectedFolder.$deleteMessages(selectedMessages).then(function(index) { @@ -302,6 +321,9 @@ _unselectMessage(deleteSelectedMessage, index); } }); + }) + .finally(function() { + messageDialog = null; }); $event.preventDefault(); diff --git a/UI/WebServerResources/js/Mailer/MailboxesController.js b/UI/WebServerResources/js/Mailer/MailboxesController.js index f8f685bfa..6a5ee28ec 100644 --- a/UI/WebServerResources/js/Mailer/MailboxesController.js +++ b/UI/WebServerResources/js/Mailer/MailboxesController.js @@ -6,11 +6,12 @@ /** * @ngInject */ - MailboxesController.$inject = ['$state', '$timeout', '$window', '$mdDialog', '$mdToast', '$mdMedia', '$mdSidenav', 'sgConstant', 'sgFocus', 'encodeUriFilter', 'Dialog', 'sgSettings', 'Account', 'Mailbox', 'VirtualMailbox', 'User', 'Preferences', 'stateAccounts']; - function MailboxesController($state, $timeout, $window, $mdDialog, $mdToast, $mdMedia, $mdSidenav, sgConstant, focus, encodeUriFilter, Dialog, Settings, Account, Mailbox, VirtualMailbox, User, Preferences, stateAccounts) { + MailboxesController.$inject = ['$scope', '$state', '$timeout', '$window', '$mdDialog', '$mdToast', '$mdMedia', '$mdSidenav', 'sgConstant', 'sgFocus', 'encodeUriFilter', 'Dialog', 'sgSettings', 'sgHotkeys', 'Account', 'Mailbox', 'VirtualMailbox', 'User', 'Preferences', 'stateAccounts']; + function MailboxesController($scope, $state, $timeout, $window, $mdDialog, $mdToast, $mdMedia, $mdSidenav, sgConstant, focus, encodeUriFilter, Dialog, Settings, sgHotkeys, Account, Mailbox, VirtualMailbox, User, Preferences, stateAccounts) { var vm = this, account, - mailbox; + mailbox, + hotkeys = []; vm.service = Mailbox; vm.accounts = stateAccounts; @@ -55,12 +56,39 @@ params: [] }; + Preferences.ready().then(function() { vm.showSubscribedOnly = Preferences.defaults.SOGoMailShowSubscribedFoldersOnly; }); vm.refreshUnseenCount(); + _registerHotkeys(hotkeys); + + $scope.$on('$destroy', function() { + // Deregister hotkeys + _.forEach(hotkeys, function(key) { + sgHotkeys.deregisterHotkey(key); + }); + }); + + + function _registerHotkeys(keys) { + keys.push(sgHotkeys.createHotkey({ + key: 'backspace', + description: l('Delete selected message or folder'), + callback: function() { + if (Mailbox.selectedFolder && !Mailbox.selectedFolder.hasSelectedMessage()) + confirmDelete(Mailbox.selectedFolder); + } + })); + + // Register the hotkeys + _.forEach(keys, function(key) { + sgHotkeys.registerHotkey(key); + }); + } + function showAdvancedSearch(path) { vm.showingAdvancedSearch = true; vm.search.mailbox = path; diff --git a/UI/WebServerResources/js/Mailer/MessageController.js b/UI/WebServerResources/js/Mailer/MessageController.js index f5602d7de..f28071863 100644 --- a/UI/WebServerResources/js/Mailer/MessageController.js +++ b/UI/WebServerResources/js/Mailer/MessageController.js @@ -39,6 +39,8 @@ vm.convertToEvent = convertToEvent; vm.convertToTask = convertToTask; + _registerHotkeys(hotkeys); + // One-way refresh of the parent window when modifying the message from a popup window. if ($window.opener) { // Update the message flags. The message must be displayed in the parent window. @@ -100,8 +102,6 @@ }); }); - _registerHotkeys(hotkeys); - function _registerHotkeys(keys) { keys.push(sgHotkeys.createHotkey({ @@ -114,7 +114,7 @@ })); // Register the hotkeys - _.forEach(hotkeys, function(key) { + _.forEach(keys, function(key) { sgHotkeys.registerHotkey(key); }); } diff --git a/UI/WebServerResources/js/Scheduler/CalendarController.js b/UI/WebServerResources/js/Scheduler/CalendarController.js index 50321df8a..1b777127d 100644 --- a/UI/WebServerResources/js/Scheduler/CalendarController.js +++ b/UI/WebServerResources/js/Scheduler/CalendarController.js @@ -6,9 +6,9 @@ /** * @ngInject */ - CalendarController.$inject = ['$scope', '$rootScope', '$state', '$stateParams', 'Calendar', 'Component', 'Preferences', 'stateEventsBlocks']; - function CalendarController($scope, $rootScope, $state, $stateParams, Calendar, Component, Preferences, stateEventsBlocks) { - var vm = this, deregisterCalendarsList; + CalendarController.$inject = ['$scope', '$rootScope', '$state', '$stateParams', 'sgHotkeys', 'Calendar', 'Component', 'Preferences', 'stateEventsBlocks']; + function CalendarController($scope, $rootScope, $state, $stateParams, sgHotkeys, Calendar, Component, Preferences, stateEventsBlocks) { + var vm = this, deregisterCalendarsList, hotkeys = []; // Make the toolbar state of all-day events persistent if (angular.isUndefined(CalendarController.expandedAllDays)) @@ -21,6 +21,9 @@ vm.changeDate = changeDate; vm.changeView = changeView; + + _registerHotkeys(hotkeys); + Preferences.ready().then(function() { _formatDate(vm.selectedDate); }); @@ -28,8 +31,84 @@ // Refresh current view when the list of calendars is modified deregisterCalendarsList = $rootScope.$on('calendars:list', updateView); - // Destroy event listener when the controller is being deactivated - $scope.$on('$destroy', deregisterCalendarsList); + $scope.$on('$destroy', function() { + // Destroy event listener when the controller is being deactivated + deregisterCalendarsList(); + // Deregister hotkeys + _.forEach(hotkeys, function(key) { + sgHotkeys.deregisterHotkey(key); + }); + }); + + + function _registerHotkeys(keys) { + keys.push(sgHotkeys.createHotkey({ + key: l('hotkey_today'), + description: l('Today'), + callback: changeDate, + args: new Date() + })); + keys.push(sgHotkeys.createHotkey({ + key: l('hotkey_dayview'), + description: l('Day'), + callback: changeView, + args: 'day' + })); + keys.push(sgHotkeys.createHotkey({ + key: l('hotkey_weekview'), + description: l('Week'), + callback: changeView, + args: 'week' + })); + keys.push(sgHotkeys.createHotkey({ + key: l('hotkey_monthview'), + description: l('Month'), + callback: changeView, + args: 'month' + })); + keys.push(sgHotkeys.createHotkey({ + key: l('hotkey_multicolumndayview'), + description: l('Multicolumn Day View'), + callback: changeView, + args: 'multicolumnday' + })); + keys.push(sgHotkeys.createHotkey({ + key: 'left', + description: l('Move backward'), + callback: _goToPeriod, + args: -1 + })); + keys.push(sgHotkeys.createHotkey({ + key: 'right', + description: l('Move forward'), + callback: _goToPeriod, + args: +1 + })); + + // Register the hotkeys + _.forEach(keys, function(key) { + sgHotkeys.registerHotkey(key); + }); + } + + + function _goToPeriod($event, direction) { + var date; + + if ($stateParams.view == 'week') { + date = vm.selectedDate.beginOfWeek(Preferences.defaults.SOGoFirstDayOfWeek).addDays(7 * direction); + } + else if ($stateParams.view == 'month') { + date = vm.selectedDate; + date.setDate(1); + date.setMonth(date.getMonth() + direction); + } + else { + date = vm.selectedDate.addDays(direction); + } + + changeDate($event, date); + } function _formatDate(date) { if ($stateParams.view == 'month') { @@ -75,7 +154,7 @@ } // Change calendar's view - function changeView(view) { + function changeView($event, view) { $state.go('calendars.view', { view: view }); } } diff --git a/UI/WebServerResources/js/Scheduler/CalendarListController.js b/UI/WebServerResources/js/Scheduler/CalendarListController.js index 21cab081b..475ea661b 100644 --- a/UI/WebServerResources/js/Scheduler/CalendarListController.js +++ b/UI/WebServerResources/js/Scheduler/CalendarListController.js @@ -6,9 +6,9 @@ /** * @ngInject */ - CalendarListController.$inject = ['$rootScope', '$timeout', '$state', '$mdDialog', 'Dialog', 'Preferences', 'Calendar', 'Component']; - function CalendarListController($rootScope, $timeout, $state, $mdDialog, Dialog, Preferences, Calendar, Component) { - var vm = this; + CalendarListController.$inject = ['$rootScope', '$scope', '$timeout', '$state', '$mdDialog', 'sgHotkeys', 'sgFocus', 'Dialog', 'Preferences', 'Calendar', 'Component']; + function CalendarListController($rootScope, $scope, $timeout, $state, $mdDialog, sgHotkeys, focus, Dialog, Preferences, Calendar, Component) { + var vm = this, hotkeys = []; vm.component = Component; vm.componentType = 'events'; @@ -16,6 +16,7 @@ vm.selectComponentType = selectComponentType; vm.unselectComponents = unselectComponents; vm.selectAll = selectAll; + vm.searchMode = searchMode; vm.toggleComponentSelection = toggleComponentSelection; vm.confirmDeleteSelectedComponents = confirmDeleteSelectedComponents; vm.openEvent = openEvent; @@ -30,6 +31,9 @@ vm.cancelSearch = cancelSearch; vm.mode = { search: false, multiple: 0 }; + + _registerHotkeys(hotkeys); + // Select list based on user's settings Preferences.ready().then(function() { var type = 'events'; @@ -48,6 +52,39 @@ // Update the component being dragged $rootScope.$on('calendar:dragend', updateComponentFromGhost); + $scope.$on('$destroy', function() { + // Deregister hotkeys + _.forEach(hotkeys, function(key) { + sgHotkeys.deregisterHotkey(key); + }); + }); + + + function _registerHotkeys(keys) { + keys.push(sgHotkeys.createHotkey({ + key: l('hotkey_search'), + description: l('Search'), + callback: searchMode + })); + keys.push(sgHotkeys.createHotkey({ + key: l('hotkey_create_event'), + description: l('Create a new event'), + callback: newComponent, + args: 'appointment' + })); + keys.push(sgHotkeys.createHotkey({ + key: l('hotkey_create_task'), + description: l('Create a new task'), + callback: newComponent, + args: 'task' + })); + + // Register the hotkeys + _.forEach(keys, function(key) { + sgHotkeys.registerHotkey(key); + }); + } + // Switch between components tabs function selectComponentType(type, options) { if (options && options.reload || vm.componentType != type) { @@ -80,6 +117,11 @@ $event.stopPropagation(); } + function searchMode() { + vm.mode.search = true; + focus('search'); + } + function confirmDeleteSelectedComponents() { Dialog.confirm(l('Warning'), l('Are you sure you want to delete the selected components?'), @@ -317,7 +359,7 @@ Component.$filter(vm.componentType, { value: '' }); } } - + angular .module('SOGo.SchedulerUI') .controller('CalendarListController', CalendarListController); diff --git a/UI/WebServerResources/scss/components/hotkeys/hotkeys.scss b/UI/WebServerResources/scss/components/hotkeys/hotkeys.scss new file mode 100644 index 000000000..58d2c3619 --- /dev/null +++ b/UI/WebServerResources/scss/components/hotkeys/hotkeys.scss @@ -0,0 +1,21 @@ +/// hotkeys.scss -*- Mode: scss; indent-tabs-mode: nil; basic-offset: 2 -*- + +.sg-hotkey-container { + display: inline; + margin-right: 1em; + text-align: right; + max-width: 7em; + min-width: 3em; +} + +sg-hotkey { + background-color: #333; + border-radius: 5px; + border: 1px solid #333; + box-shadow: inset 0 1px 0 #666, 0 1px 0 #bbb; + color: #fff; + display: inline-block; + margin-right: 5px; + padding: 5px 9px; + text-align: center; +} diff --git a/UI/WebServerResources/scss/styles.scss b/UI/WebServerResources/scss/styles.scss index 81b6a42fd..478947462 100755 --- a/UI/WebServerResources/scss/styles.scss +++ b/UI/WebServerResources/scss/styles.scss @@ -65,6 +65,7 @@ // Inverse components @import 'components/draggable-droppable/draggable'; @import 'components/draggable-droppable/droppable'; +@import 'components/hotkeys/hotkeys'; @import 'components/ripple/ripple'; @import 'components/timepicker/timepicker'; @import 'components/pseudo-input/pseudo-input';