(js,css) Improve keyboard shortcuts

- Defined some hotkeys in all modules;
- Added generation of cheat sheet.
pull/222/head
Francis Lachapelle 2016-09-27 15:19:24 -04:00
parent 13dd21bebb
commit 57a735753f
21 changed files with 630 additions and 74 deletions

1
NEWS
View File

@ -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

View File

@ -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";
"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";

View File

@ -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";

View File

@ -365,4 +365,7 @@
"Search scope" = "Search scope";
/* Subscriptions Dialog */
"Manage Subscriptions" = "Manage Subscriptions";
"Manage Subscriptions" = "Manage Subscriptions";
/* Hotkey to write a new message */
"hotkey_compose" = "w";

View File

@ -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";
"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";

View File

@ -34,16 +34,16 @@
<a class="md-icon-button md-button"
label:aria-label="Day"
ng-disabled="true"
ng-click="calendar.changeView('day')"><md-icon>view_day</md-icon></a>
ng-click="calendar.changeView($event, 'day')"><md-icon>view_day</md-icon></a>
<a class="md-icon-button md-button"
label:aria-label="Week"
ng-click="calendar.changeView('week')"><md-icon>view_week</md-icon></a>
ng-click="calendar.changeView($event, 'week')"><md-icon>view_week</md-icon></a>
<a class="md-icon-button md-button"
label:aria-label="Month"
ng-click="calendar.changeView('month')"><md-icon>view_module</md-icon></a>
ng-click="calendar.changeView($event, 'month')"><md-icon>view_module</md-icon></a>
<a class="md-icon-button md-button"
label:aria-label="Multicolumn Day View"
ng-click="calendar.changeView('multicolumnday')"><md-icon>view_array</md-icon></a>
ng-click="calendar.changeView($event, 'multicolumnday')"><md-icon>view_array</md-icon></a>
</md-card-actions>
<var:component

View File

@ -348,7 +348,7 @@
<!-- sort/filter mode (default) -->
<div class="md-toolbar-tools" layout="row" ng-hide="list.mode.search">
<md-button class="sg-icon-button" label:aria-label="Search"
ng-click="list.mode.search = true">
ng-click="list.searchMode()">
<md-icon>search</md-icon>
</md-button>
<div class="md-flex"><!-- spacer --></div>
@ -530,7 +530,8 @@
<md-icon>arrow_back</md-icon>
</md-button>
<md-input-container class="md-flex" md-no-float="md-no-float">
<input name="folderSearch" type="search" var:minlength="minimumSearchLength" label:placeholder="Search"/>
<input name="folderSearch" type="search" var:minlength="minimumSearchLength" label:placeholder="Search"
sg-focus-on="search"/>
<div ng-messages="searchForm.folderSearch.$error" ng-show="searchForm.folderSearch.$dirty">
<div ng-message="minlength"><var:string value="minimumSearchLengthLabel"/></div>
</div>

View File

@ -33,17 +33,17 @@
</md-button>
<a class="md-icon-button md-button"
label:aria-label="Day"
ng-click="calendar.changeView('day')"><md-icon>view_day</md-icon></a>
ng-click="calendar.changeView($event, 'day')"><md-icon>view_day</md-icon></a>
<a class="md-icon-button md-button"
label:aria-label="Week"
ng-click="calendar.changeView('week')"><md-icon>view_week</md-icon></a>
ng-click="calendar.changeView($event, 'week')"><md-icon>view_week</md-icon></a>
<a class="md-icon-button md-button"
label:aria-label="Month"
ng-disabled="true"
ng-click="calendar.changeView('month')"><md-icon>view_module</md-icon></a>
ng-click="calendar.changeView($event, 'month')"><md-icon>view_module</md-icon></a>
<a class="md-icon-button md-button"
label:aria-label="Multicolumn Day View"
ng-click="calendar.changeView('multicolumnday')"><md-icon>view_array</md-icon></a>
ng-click="calendar.changeView($event, 'multicolumnday')"><md-icon>view_array</md-icon></a>
</md-card-actions>
<md-toolbar class="monthView">

View File

@ -33,17 +33,17 @@
</md-button>
<a class="md-icon-button md-button"
label:aria-label="Day"
ng-click="calendar.changeView('day')"><md-icon>view_day</md-icon></a>
ng-click="calendar.changeView($event, 'day')"><md-icon>view_day</md-icon></a>
<a class="md-icon-button md-button"
label:aria-label="Week"
ng-disabled="true"
ng-click="calendar.changeView('week')"><md-icon>view_week</md-icon></a>
ng-click="calendar.changeView($event, 'week')"><md-icon>view_week</md-icon></a>
<a class="md-icon-button md-button"
label:aria-label="Month"
ng-click="calendar.changeView('month')"><md-icon>view_module</md-icon></a>
ng-click="calendar.changeView($event, 'month')"><md-icon>view_module</md-icon></a>
<a class="md-icon-button md-button"
label:aria-label="Multicolumn Day View"
ng-click="calendar.changeView('multicolumnday')"><md-icon>view_array</md-icon></a>
ng-click="calendar.changeView($event, 'multicolumnday')"><md-icon>view_array</md-icon></a>
</md-card-actions>
<var:component
className="UIxCalDayTable"

View File

@ -3,7 +3,7 @@
(function() {
/* jshint validthis: true */
'use strict';
/**
* $sgHotkeys - A service to associate keyboard shortcuts to actions.
* @memberof SOGo.Common
@ -18,7 +18,6 @@
// Source : http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
// http://unixpapa.com/js/key.html
// Date: Oct 02, 2015.
//var KEY_CODES = this.KEY_CODES = {
var KEY_CODES = {
8: 'backspace',
9: 'tab',
@ -70,6 +69,10 @@
122: 'f11',
123: 'f12'
};
// Char-code values for characters that require a key combinations
var CHAR_CODES = {
63: '?'
};
this.$get = getService;
@ -87,11 +90,16 @@
var HotKey = function(params) {
this.id = params.id || guid();
this.key = params.key;
this.description = params.description || null;
this.context = params.context || null;
this.callback = params.callback;
this.preventInClass = params.preventInClass;
this.args = params.args;
this.onKeyUp = false;
if (this.key.length > 1)
// Automatically translate common hotkeys
this.lkey = l('key_' + this.key);
};
HotKey.prototype.clone = function() {
@ -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: [
'<md-dialog>',
' <md-dialog-content>',
' <md-list>',
' <md-list-item ng-repeat="(hotkey, keys) in hotkeys">',
' <div class="sg-hotkey-container">',
' <sg-hotkey>{{keys[0].lkey || hotkey}}</sg-hotkey>',
' </div>',
' {{keys[0].description}}',
' </md-list-item>',
' </md-list>',
' </md-dialog-content>',
'</md-dialog>'
].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();
}

View File

@ -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

View File

@ -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);
})();

View File

@ -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);
}
});
}
}

View File

@ -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}?', '<b>' + card.$fullname() + '</b>'),
{ ok: l('Delete') })

View File

@ -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();

View File

@ -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;

View File

@ -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);
});
}

View File

@ -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 });
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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';