diff options
Diffstat (limited to 'web-ui/public/js/mail_view/ui/recipients')
4 files changed, 544 insertions, 0 deletions
diff --git a/web-ui/public/js/mail_view/ui/recipients/recipient.js b/web-ui/public/js/mail_view/ui/recipients/recipient.js new file mode 100644 index 00000000..c13a52b1 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipient.js @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'page/events' + ], + + function (defineComponent, templates, events) { + 'use strict'; + + return defineComponent(recipient); + + function recipient() { + this.renderAndPrepend = function (nodeToPrependTo, recipient) { + var html = $(templates.compose.fixedRecipient(recipient)); + html.insertBefore(nodeToPrependTo.children().last()); + var component = new this.constructor(); + component.initialize(html, recipient); + component.attr.recipient = recipient; + return component; + }; + + this.recipientDelActions = function () { + this.on(this.$node.find('.recipient-del'), 'click', function (event) { + this.doSelect(); + this.trigger(events.ui.recipients.deleteRecipient, this); + event.preventDefault(); + }); + + this.on(this.$node.find('.recipient-del'), 'mouseover', function () { + this.$node.find('.recipient-value').addClass('deleting'); + this.$node.find('.recipient-del').addClass('deleteTooltip'); + }); + + this.on(this.$node.find('.recipient-del'), 'mouseout', function () { + this.$node.find('.recipient-value').removeClass('deleting'); + this.$node.find('.recipient-del').removeClass('deleteTooltip'); + }); + }; + + this.destroy = function () { + this.$node.remove(); + this.teardown(); + }; + + this.doSelect = function () { + this.$node.find('.recipient-value').addClass('selected'); + }; + + this.doUnselect = function () { + this.$node.find('.recipient-value').removeClass('selected'); + }; + + this.isSelected = function () { + return this.$node.find('.recipient-value').hasClass('selected'); + }; + + this.sinalizeInvalid = function () { + this.$node.find('.recipient-value>span').addClass('invalid-format'); + }; + + this.discoverEncryption = function () { + this.$node.addClass('discover-encryption'); + var p = $.getJSON('/keys?search=' + this.attr.address).promise(); + p.done(function () { + this.$node.find('.recipient-value').addClass('encrypted'); + this.$node.removeClass('discover-encryption'); + }.bind(this)); + p.fail(function () { + this.$node.find('.recipient-value').addClass('not-encrypted'); + this.$node.removeClass('discover-encryption'); + }.bind(this)); + }; + + this.getMailAddress = function() { + return this.$node.find('input[type=hidden]').val(); + }; + + this.triggerEditRecipient = function(event, element) { + this.trigger(this.$node.closest('.recipients-area'), events.ui.recipients.clickToEdit, this); + }; + + this.after('initialize', function () { + this.recipientDelActions(); + this.on('click', this.triggerEditRecipient); + + if (this.attr.invalidAddress){ + this.sinalizeInvalid(); + } else { + this.discoverEncryption(); + } + }); + } + } +); diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients.js b/web-ui/public/js/mail_view/ui/recipients/recipients.js new file mode 100644 index 00000000..2caa8d14 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipients.js @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'page/events', + 'helpers/iterator', + 'mail_view/ui/recipients/recipients_input', + 'mail_view/ui/recipients/recipient', + 'mail_view/ui/recipients/recipients_iterator' + ], + function (defineComponent, templates, events, Iterator, RecipientsInput, Recipient, RecipientsIterator) { + 'use strict'; + + return defineComponent(recipients); + + function recipients() { + this.defaultAttrs({ + navigationHandler: '.recipients-navigation-handler', + recipientsList: '.recipients-list' + }); + + function getAddresses(recipients) { + return _.flatten(_.map(recipients, function (e) { return e.attr.address;})); + } + + function moveLeft() { this.attr.iterator.moveLeft(); } + function moveRight() { this.attr.iterator.moveRight(); } + function deleteCurrentRecipient() { + this.attr.iterator.deleteCurrent(); + this.addressesUpdated(); + } + + function editCurrentRecipient(event, recipient) { + var mailAddr = this.attr.iterator.current().getMailAddress(); + this.attr.iterator.deleteCurrent(); + this.attr.input.$node.val(mailAddr).focus(); + this.unselectAllRecipients(); + this.addressesUpdated(); + } + + this.clickToEditRecipient = function(event, recipient) { + this.attr.iterator = null; + var mailAddr = recipient.getMailAddress(); + + var position = this.getRecipientPosition(recipient); + this.attr.recipients.splice(position, 1); + recipient.destroy(); + + this.addressesUpdated(); + this.unselectAllRecipients(); + this.attr.input.$node.val(mailAddr).focus(); + }; + + this.getRecipientPosition = function(recipient) { + return recipient.$node.closest('.recipients-area').find('.fixed-recipient').index(recipient.$node); + }; + + this.unselectAllRecipients = function() { + this.$node.find('.recipient-value.selected').removeClass('selected'); + }; + + var SPECIAL_KEYS_ACTIONS = { + 8: deleteCurrentRecipient, + 46: deleteCurrentRecipient, + 32: editCurrentRecipient, + 13: editCurrentRecipient, + 37: moveLeft, + 39: moveRight + }; + + this.addRecipient = function(recipient) { + var newRecipient = Recipient.prototype.renderAndPrepend(this.$node.find(this.attr.recipientsList), recipient); + this.attr.recipients.push(newRecipient); + }; + + this.recipientEntered = function (event, recipient) { + this.addRecipient(recipient); + this.addressesUpdated(); + }; + + this.invalidRecipientEntered = function(event, recipient) { + recipient.invalidAddress = true; + this.addRecipient(recipient); + }; + + this.deleteRecipient = function (event, recipient) { + this.attr.iterator = null; + var position = this.getRecipientPosition(recipient); + + this.attr.recipients.splice(position, 1); + recipient.destroy(); + + this.addressesUpdated(); + }; + + this.deleteLastRecipient = function () { + this.attr.recipients.pop().destroy(); + this.addressesUpdated(); + }; + + this.enterNavigationMode = function () { + this.attr.iterator = new RecipientsIterator({ + elements: this.attr.recipients, + exitInput: this.attr.input.$node + }); + + this.attr.iterator.current().doSelect(); + this.attr.input.$node.blur(); + this.select('navigationHandler').focus(); + }; + + this.leaveNavigationMode = function () { + if(this.attr.iterator) { this.attr.iterator.current().unselect(); } + this.attr.iterator = null; + }; + + this.selectLastRecipient = function () { + if (this.attr.recipients.length === 0) { return; } + this.enterNavigationMode(); + }; + + this.attachInput = function () { + this.attr.input = RecipientsInput.prototype.attachAndReturn(this.$node.find('input[type=text]'), this.attr.name); + }; + + this.processSpecialKey = function (event) { + if(SPECIAL_KEYS_ACTIONS.hasOwnProperty(event.which)) { SPECIAL_KEYS_ACTIONS[event.which].apply(this); } + }; + + this.initializeAddresses = function () { + _.each(_.flatten(this.attr.addresses), function (address) { + this.addRecipient({ address: address, name: this.attr.name }); + }.bind(this)); + }; + + this.addressesUpdated = function() { + this.trigger(document, events.ui.recipients.updated, {recipientsName: this.attr.name, newRecipients: getAddresses(this.attr.recipients)}); + }; + + this.doCompleteRecipients = function () { + var address = this.attr.input.$node.val(); + if (!_.isEmpty(address)) { + var recipient = Recipient.prototype.renderAndPrepend(this.$node, { name: this.attr.name, address: address }); + this.attr.recipients.push(recipient); + this.attr.input.$node.val(''); + } + + this.trigger(document, events.ui.recipients.updated, { + recipientsName: this.attr.name, + newRecipients: getAddresses(this.attr.recipients), + skipSaveDraft: true + }); + + }; + + this.after('initialize', function () { + this.attr.recipients = []; + this.attachInput(); + this.initializeAddresses(); + + this.on(events.ui.recipients.deleteRecipient, this.deleteRecipient); + this.on(events.ui.recipients.deleteLast, this.deleteLastRecipient); + this.on(events.ui.recipients.selectLast, this.selectLastRecipient); + this.on(events.ui.recipients.entered, this.recipientEntered); + this.on(events.ui.recipients.enteredInvalid, this.invalidRecipientEntered); + this.on(events.ui.recipients.clickToEdit, this.clickToEditRecipient); + + this.on(document, events.ui.recipients.doCompleteInput, this.doCompleteRecipients); + + this.on(this.attr.input.$node, 'focus', this.leaveNavigationMode); + this.on(this.select('navigationHandler'), 'keydown', this.processSpecialKey); + + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + }); + } + }); diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients_input.js b/web-ui/public/js/mail_view/ui/recipients/recipients_input.js new file mode 100644 index 00000000..8a9c4eaf --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipients_input.js @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define([ + 'flight/lib/component', + 'page/events', + 'features' + ], + function (defineComponent, events, features) { + 'use strict'; + + function recipientsInput() { + var EXIT_KEY_CODE_MAP = { + 8: 'backspace', + 37: 'left' + }, + ENTER_ADDRESS_KEY_CODE_MAP = { + 9: 'tab', + 186: 'semicolon', + 188: 'comma', + 13: 'enter', + 27: 'esc' + }, + EVENT_FOR = { + 8: events.ui.recipients.deleteLast, + 37: events.ui.recipients.selectLast + }, + self; + + var simpleAddressMatch = /[^<\w,;]?([^\s<;,]+@[\w-]+\.[^\s>;,]+)/; + var canonicalAddressMatch = /([^,;\s][^,;@]+<[^\s;,]+@[\w-]+\.[^\s;,]+>)/; + var emailAddressMatch = new RegExp([simpleAddressMatch.source, '|', canonicalAddressMatch.source].join(''), 'g'); + + var extractContactNames = function (response) { + return _.map(response, function(a) { return { value: a }; }); + }; + + function createEmailCompleter() { + var emailCompleter = new Bloodhound({ + datumTokenizer: function (d) { + return [d.value]; + }, + queryTokenizer: function (q) { + return [q.trim()]; + }, + remote: { + url: '/contacts?q=%QUERY', + filter: extractContactNames + } + }); + emailCompleter.initialize(); + return emailCompleter; + } + + function reset(node) { + node.typeahead('val', ''); + } + + function caretIsInTheBeginningOfInput(input) { + return input.selectionStart === 0; + } + + function isExitKey(keyPressed) { + return EXIT_KEY_CODE_MAP.hasOwnProperty(keyPressed); + } + + function isEnterAddressKey(keyPressed) { + return ENTER_ADDRESS_KEY_CODE_MAP.hasOwnProperty(keyPressed); + } + + this.processSpecialKey = function (event) { + var keyPressed = event.which; + + if (isExitKey(keyPressed) && caretIsInTheBeginningOfInput(this.$node[0])) { + this.trigger(EVENT_FOR[keyPressed]); + return; + } + + if (!event.shiftKey && isEnterAddressKey(keyPressed)) { + this.tokenizeRecipient(event); + + if ((keyPressed !== 9 /* tab */)) { + event.preventDefault(); + } + } + + }; + + this.tokenizeRecipient = function (event) { + if (_.isEmpty(this.$node.val().trim())) { + return; + } + + this.recipientSelected(null, {value: this.$node.val() }); + event.preventDefault(); + }; + + this.recipientSelected = function (event, data) { + var value = (data && data.value) || this.$node.val(); + + var validAddresses = this.extractValidAddresses(value); + var invalidAddresses = this.extractInvalidAddresses(value); + + this.triggerEventForEach(validAddresses, events.ui.recipients.entered); + this.triggerEventForEach(invalidAddresses, events.ui.recipients.enteredInvalid); + + reset(this.$node); + }; + + this.triggerEventForEach = function (addresses, event) { + var that = this; + _.each(addresses, function(address) { + if (!_.isEmpty(address.trim())) { + that.trigger(that.$node, event, { name: that.attr.name, address: address.trim() }); + } + }); + }; + + this.extractValidAddresses = function(rawAddresses) { + return rawAddresses.match(emailAddressMatch); + }; + + this.extractInvalidAddresses = function(rawAddresses) { + return rawAddresses.replace(emailAddressMatch, '').split(/[,;]/); + }; + + this.init = function () { + this.$node.typeahead({ + hint: true, + highlight: true, + minLength: 1 + }, { + source: createEmailCompleter().ttAdapter(), + templates: { + suggestion: function (o) { return _.escape(o.value); } + } + }); + }; + + this.attachAndReturn = function (node, name) { + var input = new this.constructor(); + input.initialize(node, { name: name}); + return input; + }; + + this.warnSendButtonOfInputState = function () { + var toTrigger = _.isEmpty(this.$node.val()) ? events.ui.recipients.inputFieldIsEmpty : events.ui.recipients.inputFieldHasCharacters; + this.trigger(document, toTrigger, { name: this.attr.name }); + }; + + this.after('initialize', function () { + self = this; + this.init(); + this.on('typeahead:selected typeahead:autocompleted', this.recipientSelected); + this.on(this.$node, 'focusout', this.tokenizeRecipient); + this.on(this.$node, 'keydown', this.processSpecialKey); + this.on(this.$node, 'keyup', this.warnSendButtonOfInputState); + + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + }); + } + + return defineComponent(recipientsInput); + + } +); diff --git a/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js b/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js new file mode 100644 index 00000000..624ac4f5 --- /dev/null +++ b/web-ui/public/js/mail_view/ui/recipients/recipients_iterator.js @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2014 ThoughtWorks, Inc. + * + * Pixelated is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Pixelated is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Pixelated. If not, see <http://www.gnu.org/licenses/>. + */ + +define(['helpers/iterator'], function (Iterator) { + 'use strict'; + + return RecipientsIterator; + + function RecipientsIterator(options) { + + this.iterator = new Iterator(options.elements, options.elements.length - 1); + this.input = options.exitInput; + + this.current = function () { + return this.iterator.current(); + }; + + this.moveLeft = function () { + if (this.iterator.hasPrevious()) { + this.iterator.current().doUnselect(); + this.iterator.previous().doSelect(); + } + }; + + this.moveRight = function () { + this.iterator.current().doUnselect(); + if (this.iterator.hasNext()) { + this.iterator.next().doSelect(); + } else { + this.input.focus(); + } + }; + + this.deleteCurrent = function () { + this.iterator.removeCurrent().destroy(); + + if (this.iterator.hasElements()) { + this.iterator.current().doSelect(); + } else { + this.input.focus(); + } + }; + } + +}); |