Skip to content

Commit

Permalink
(js) Restore caret position in message editor
Browse files Browse the repository at this point in the history
Fixes #4517
  • Loading branch information
cgx committed Aug 17, 2018
1 parent e5d3c5f commit f0b4e1b
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 9 deletions.
1 change: 1 addition & 0 deletions NEWS
Expand Up @@ -13,6 +13,7 @@ Bug fixes
- [core] avoid displaying empty signed emails when using GNU TLS (#4433)
- [web] improve popup window detection in message viewer (#4518)
- [web] enable save button when editing the members of a list
- [web] restore caret position when replying or forwarding a message (#4517)

4.0.1 (2018-07-10)
------------------
Expand Down
7 changes: 5 additions & 2 deletions UI/Templates/MailerUI/UIxMailEditor.wox
Expand Up @@ -72,7 +72,7 @@
md-add-on-blur="true">
<md-autocomplete
class="sg-chips-autocomplete"
md-autofocus="true"
md-autofocus="editor.isNew()"
md-search-text="editor.autocomplete.to.searchText"
md-selected-item="editor.autocomplete.to.selected"
md-items="user in editor.contactFilter(editor.autocomplete.to.searchText)"
Expand Down Expand Up @@ -244,10 +244,13 @@
<!-- MESSAGE CONTENT -->
<md-input-container class="md-block sg-mail-editor-content">
<textarea name="content" var:class="editorClass"
rows="9"
ck-locale="editor.localeCode"
ck-margin="16px"
rows="9"
ck-focus="editor.onHTMLFocus($event)"
ng-model="editor.message.editable.text"
ng-focus="editor.onTextFocus($event)"
md-autofocus="!editor.isNew()"
md-no-resize="md-no-resize"
md-detect-hidden="md-detect-hidden" />
</md-input-container>
Expand Down
27 changes: 27 additions & 0 deletions UI/WebServerResources/js/Common/utils.js
Expand Up @@ -509,6 +509,33 @@ Date.prototype.format = function(localeProvider, format) {
return date.join('');
};

Element.prototype.setCaretTo = function(pos) {
if (this.setSelectionRange) { // For Mozilla and Safari
this.focus();
this.setSelectionRange(pos, pos);
}
else if (this.createTextRange) { // For IE
var range = this.createTextRange();
range.move('character', pos);
range.select();
}
};

Element.prototype.selectText = function(start, end) {
if (this.setSelectionRange) { // For Mozilla and Safari
this.setSelectionRange(start, end);
}
else if (this.createTextRange) { // For IE
var textRange = this.createTextRange();
textRange.moveStart('character', start);
textRange.moveEnd('character', end-element.value.length);
textRange.select();
}
else {
this.select();
}
};

/* Functions */

function l() {
Expand Down
10 changes: 8 additions & 2 deletions UI/WebServerResources/js/Mailer/MailboxController.js
Expand Up @@ -171,7 +171,7 @@
};

this.newMessage = function($event, inPopup) {
var message;
var message, onCompleteDeferred = $q.defer();

if (vm.messageDialog === null) {
if (inPopup || Preferences.defaults.SOGoMailComposeWindow == 'popup')
Expand All @@ -187,9 +187,15 @@
templateUrl: 'UIxMailEditor',
controller: 'MessageEditorController',
controllerAs: 'editor',
onComplete: function (scope, element) {
return onCompleteDeferred.resolve(element);
},
locals: {
stateAccount: vm.account,
stateMessage: message
stateMessage: message,
onCompletePromise: function () {
return onCompleteDeferred.promise;
}
}
})
.catch(_.noop) // Cancel
Expand Down
13 changes: 10 additions & 3 deletions UI/WebServerResources/js/Mailer/MessageController.js
Expand Up @@ -6,8 +6,8 @@
/**
* @ngInject
*/
MessageController.$inject = ['$window', '$scope', '$state', '$mdMedia', '$mdDialog', 'sgConstant', 'stateAccounts', 'stateAccount', 'stateMailbox', 'stateMessage', 'sgHotkeys', 'encodeUriFilter', 'sgSettings', 'ImageGallery', 'sgFocus', 'Dialog', 'Preferences', 'Calendar', 'Component', 'Account', 'Mailbox', 'Message'];
function MessageController($window, $scope, $state, $mdMedia, $mdDialog, sgConstant, stateAccounts, stateAccount, stateMailbox, stateMessage, sgHotkeys, encodeUriFilter, sgSettings, ImageGallery, focus, Dialog, Preferences, Calendar, Component, Account, Mailbox, Message) {
MessageController.$inject = ['$window', '$scope', '$q', '$state', '$mdMedia', '$mdDialog', 'sgConstant', 'stateAccounts', 'stateAccount', 'stateMailbox', 'stateMessage', 'sgHotkeys', 'encodeUriFilter', 'sgSettings', 'ImageGallery', 'sgFocus', 'Dialog', 'Preferences', 'Calendar', 'Component', 'Account', 'Mailbox', 'Message'];
function MessageController($window, $scope, $q, $state, $mdMedia, $mdDialog, sgConstant, stateAccounts, stateAccount, stateMailbox, stateMessage, sgHotkeys, encodeUriFilter, sgSettings, ImageGallery, focus, Dialog, Preferences, Calendar, Component, Account, Mailbox, Message) {
var vm = this, popupWindow = null, hotkeys = [];

this.$onInit = function() {
Expand Down Expand Up @@ -277,6 +277,7 @@

function _showMailEditor($event, message) {
if (_messageDialog() === null) {
var onCompleteDeferred = $q.defer();
_messageDialog(
$mdDialog
.show({
Expand All @@ -287,9 +288,15 @@
templateUrl: 'UIxMailEditor',
controller: 'MessageEditorController',
controllerAs: 'editor',
onComplete: function (scope, element) {
return onCompleteDeferred.resolve(element);
},
locals: {
stateAccount: vm.account,
stateMessage: message
stateMessage: message,
onCompletePromise: function () {
return onCompleteDeferred.promise;
}
}
})
.catch(_.noop) // Cancel
Expand Down
101 changes: 99 additions & 2 deletions UI/WebServerResources/js/Mailer/MessageEditorController.js
Expand Up @@ -6,8 +6,8 @@
/**
* @ngInject
*/
MessageEditorController.$inject = ['$scope', '$window', '$stateParams', '$mdConstant', '$mdUtil', '$mdDialog', '$mdToast', 'FileUploader', 'stateAccount', 'stateMessage', 'encodeUriFilter', '$timeout', 'Dialog', 'AddressBook', 'Card', 'Preferences'];
function MessageEditorController($scope, $window, $stateParams, $mdConstant, $mdUtil, $mdDialog, $mdToast, FileUploader, stateAccount, stateMessage, encodeUriFilter, $timeout, Dialog, AddressBook, Card, Preferences) {
MessageEditorController.$inject = ['$scope', '$window', '$stateParams', '$mdConstant', '$mdUtil', '$mdDialog', '$mdToast', 'FileUploader', 'stateAccount', 'stateMessage', 'onCompletePromise', 'encodeUriFilter', '$timeout', 'sgFocus', 'Dialog', 'AddressBook', 'Card', 'Preferences'];
function MessageEditorController($scope, $window, $stateParams, $mdConstant, $mdUtil, $mdDialog, $mdToast, FileUploader, stateAccount, stateMessage, onCompletePromise, encodeUriFilter, $timeout, focus, Dialog, AddressBook, Card, Preferences) {
var vm = this;

this.$onInit = function() {
Expand All @@ -33,6 +33,7 @@
vm.send = send;
vm.sendState = false;
vm.toggleFullscreen = toggleFullscreen;
this.firstFocus = true;

_initFileUploader();

Expand All @@ -43,6 +44,12 @@
// Set the locale of CKEditor
vm.localeCode = Preferences.defaults.LocaleCode;

this.replyPlacement = Preferences.defaults.SOGoMailReplyPlacement;
if (this.message.origin && this.message.origin.action == 'forward') {
// For forwards, place caret at top unconditionally
this.replyPlacement = 'above';
}

// Destroy file uploader when the controller is being deactivated
$scope.$on('$destroy', function() { vm.uploader.destroy(); });

Expand Down Expand Up @@ -338,6 +345,96 @@
vm.autosave = $timeout(vm.autosaveDrafts, Preferences.defaults.SOGoMailAutoSave*1000*60);
}

this.isNew = function () {
return typeof this.message.origin == 'undefined';
};

this.onTextFocus = function ($event) {
var textArea = $event.target;

function adjustOffset(val, offset) {
var newOffset = offset, matches;
if (val.indexOf("\r\n") > -1) {
matches = val.replace(/\r\n/g, "\n").slice(0, offset).match(/\n/g);
newOffset -= matches ? matches.length - 1 : 0;
}
return newOffset;
}

if (this.firstFocus) {
onCompletePromise().then(function(element) {
var textContent = angular.element(textArea).val(),
hasSignature = (Preferences.defaults.SOGoMailSignature &&
Preferences.defaults.SOGoMailSignature.length > 0),
signatureLength = 0,
sigLimit,
caretPosition;

if (vm.replyPlacement == 'above') {
textArea.setCaretTo(0);
element.find('md-dialog-content')[0].scrollTop = 0;
}
else {
if (hasSignature) {
sigLimit = textContent.lastIndexOf("--");
if (sigLimit > -1)
signatureLength = (textContent.length - sigLimit);
}
caretPosition = textContent.length - signatureLength;
caretPosition = adjustOffset(textContent, caretPosition);
if (hasSignature)
caretPosition -= 2;
textArea.setCaretTo(caretPosition);
}
});

this.firstFocus = false;
}
};

this.onHTMLFocus = function ($event) {
var caretAtTop = (this.replyPlacement == 'above');

if (this.firstFocus) {
onCompletePromise().then(function(element) {
var selected = $event.editor.getSelection(),
selected_ranges = selected.getRanges(),
children = $event.editor.document.getBody().getChildren(),
node;

if (caretAtTop) {
node = children.getItem(0);
}
else {
// Search for signature starting from bottom
node = children.getItem(children.count() - 1);
while (true) {
var x = node.getPrevious();
if (x === null) {
break;
}
if (x.getText() == '--') {
node = x.getPrevious().getPrevious();
break;
}
node = x;
}
}
selected.selectElement(node);

// Place the caret
if (caretAtTop)
selected.scrollIntoView(); // top
selected_ranges = selected.getRanges();
selected_ranges[0].collapse(true);
selected.selectRanges(selected_ranges);
if (!caretAtTop)
selected.scrollIntoView(); // bottom
});

this.firstFocus = false;
}
};
}

SendMessageToastController.$inject = ['$scope', '$mdToast'];
Expand Down
11 changes: 11 additions & 0 deletions UI/WebServerResources/js/vendor/ckeditor/ck.js
Expand Up @@ -81,6 +81,17 @@

ck = CKEDITOR.replace(elm[0], options);

if (attr.mdAutofocus && $parse(attr.mdAutofocus)($scope)) {
// Autofocus is enabled
ck.on('instanceReady', function(event) {
ck.focus();
});
ck.on('focus', function(event) {
if (attr.ckFocus) {
$parse(attr.ckFocus)($scope, {'$event': event});
}
});
}

// Update the model whenever the content changes
ck.on('change', function() {
Expand Down

0 comments on commit f0b4e1b

Please sign in to comment.