diff options
author | Ola Bini <ola.bini@gmail.com> | 2014-07-31 19:29:33 -0300 |
---|---|---|
committer | Ola Bini <ola.bini@gmail.com> | 2014-07-31 19:29:33 -0300 |
commit | 04cf441c5ae18400c6b4865b0b37a71718dc9d46 (patch) | |
tree | dd0b0d049ec00389e2d4561b226c46eb1682b997 /web-ui/app/js/mail_view | |
parent | 639a663a4c37020003586438fdcd7ac529a00f10 (diff) |
Add web-ui based on previous code
Diffstat (limited to 'web-ui/app/js/mail_view')
-rw-r--r-- | web-ui/app/js/mail_view/data/mail_builder.js | 79 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/data/mail_sender.js | 74 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/compose_box.js | 63 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/draft_box.js | 74 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/draft_save_status.js | 25 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/forward_box.js | 66 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/mail_actions.js | 70 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/mail_view.js | 242 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/no_message_selected_pane.js | 25 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/recipients/recipient.js | 36 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/recipients/recipients.js | 127 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/recipients/recipients_input.js | 140 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/recipients/recipients_iterator.js | 41 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/reply_box.js | 102 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/reply_section.js | 102 | ||||
-rw-r--r-- | web-ui/app/js/mail_view/ui/send_button.js | 85 |
16 files changed, 1351 insertions, 0 deletions
diff --git a/web-ui/app/js/mail_view/data/mail_builder.js b/web-ui/app/js/mail_view/data/mail_builder.js new file mode 100644 index 00000000..27820c1a --- /dev/null +++ b/web-ui/app/js/mail_view/data/mail_builder.js @@ -0,0 +1,79 @@ +/*global _ */ + +define(['services/model/mail'], function (mailModel) { + 'use strict'; + + var mail; + + function recipients(mail, place, v) { + if (v !== '' && !_.isUndefined(v)) { + if(_.isArray(v)) { + mail[place] = v; + } else { + mail[place] = v.split(' '); + } + } else { + mail[place] = []; + } + } + + return { + newMail: function(ident) { + ident = _.isUndefined(ident) ? '' : ident; + + mail = { + header: { + to: [], + cc: [], + bcc: [], + from: undefined, + subject: '' + }, + tags: [], + body: '', + ident: ident + }; + return this; + }, + + subject: function (subject) { + mail.header.subject = subject; + return this; + }, + + body: function(body) { + mail.body = body; + return this; + }, + + to: function (to) { + recipients(mail.header, 'to', to); + return this; + }, + + cc: function (cc) { + recipients(mail.header, 'cc', cc); + return this; + }, + + bcc: function (bcc) { + recipients(mail.header, 'bcc', bcc); + return this; + }, + + header: function(name, value) { + mail.header[name] = value; + return this; + }, + + tag: function(tag) { + if(_.isUndefined(tag)) { tag = 'drafts'; } + mail.tags.push(tag); + return this; + }, + + build: function() { + return mailModel.create(mail); + } + }; +}); diff --git a/web-ui/app/js/mail_view/data/mail_sender.js b/web-ui/app/js/mail_view/data/mail_sender.js new file mode 100644 index 00000000..7440f5a7 --- /dev/null +++ b/web-ui/app/js/mail_view/data/mail_sender.js @@ -0,0 +1,74 @@ +define( + [ + 'flight/lib/component', + 'mail_view/data/mail_builder', + 'page/events' + ], + function (defineComponent, mailBuilder, events) { + 'use strict'; + + return defineComponent(mailSender); + + function mailSender() { + function successSendMail(on){ + return function(result) { + on.trigger(document, events.mail.sent, result); + }; + } + + function successSaveDraft(on){ + return function(result){ + on.trigger(document, events.mail.draftSaved, result); + }; + } + + function failure(on) { + return function(xhr, status, error) { + on.trigger(events.ui.userAlerts.displayMessage, {message: 'Ops! something went wrong, try again later.'}); + }; + } + + this.defaultAttrs({ + mailsResource: '/mails' + }); + + this.sendMail = function(event, data) { + $.ajax(this.attr.mailsResource, { + type: 'POST', + dataType: 'json', + contentType: 'application/json; charset=utf-8', + data: JSON.stringify(data) + }).done(successSendMail(this)) + .fail(failure(this)); + }; + + this.saveMail = function(mail) { + var method = (mail.ident === '') ? 'POST' : 'PUT'; + + return $.ajax(this.attr.mailsResource, { + type: method, + dataType: 'json', + contentType: 'application/json; charset=utf-8', + data: JSON.stringify(mail) + }); + }; + + this.saveDraft = function(event, data) { + this.saveMail(data) + .done(successSaveDraft(this)) + .fail(failure(this)); + }; + + this.saveMailWithCallback = function(event, data) { + this.saveMail(data.mail) + .done(function(result) { return data.callback(result); }) + .fail(function(result) { return data.callback(result); }); + }; + + this.after('initialize', function () { + this.on(events.mail.send, this.sendMail); + this.on(events.mail.saveDraft, this.saveDraft); + this.on(document, events.mail.save, this.saveMailWithCallback); + }); + } + }); diff --git a/web-ui/app/js/mail_view/ui/compose_box.js b/web-ui/app/js/mail_view/ui/compose_box.js new file mode 100644 index 00000000..41954192 --- /dev/null +++ b/web-ui/app/js/mail_view/ui/compose_box.js @@ -0,0 +1,63 @@ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_mail_edit_base', + 'page/events', + 'mail_view/data/mail_builder' + ], + + function (defineComponent, templates, withMailEditBase, events, mailBuilder) { + 'use strict'; + + return defineComponent(composeBox, withMailEditBase); + + function composeBox() { + + this.defaultAttrs({ + 'closeButton': '.close-mail-button' + }); + + this.showNoMessageSelected = function() { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.buildMail = function(tag) { + return this.builtMail(tag).build(); + }; + + this.builtMail = function(tag) { + return mailBuilder.newMail(this.attr.ident) + .subject(this.select('subjectBox').val()) + .to(this.attr.recipientValues.to) + .cc(this.attr.recipientValues.cc) + .bcc(this.attr.recipientValues.bcc) + .body(this.select('bodyBox').val()) + .tag(tag); + }; + + this.renderComposeBox = function() { + this.render(templates.compose.box, {}); + this.select('recipientsFields').show(); + this.on(this.select('closeButton'), 'click', this.showNoMessageSelected); + this.enableAutoSave(); + }; + + this.mailDeleted = function(event, data) { + if (_.contains(_.pluck(data.mails, 'ident'), this.attr.ident)) { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + } + }; + + this.after('initialize', function () { + this.renderComposeBox(); + + this.select('subjectBox').focus(); + this.on(this.select('cancelButton'), 'click', this.showNoMessageSelected); + this.on(document, events.mail.deleted, this.mailDeleted); + + this.on(document, events.mail.sent, this.showNoMessageSelected); + }); + } + } +); diff --git a/web-ui/app/js/mail_view/ui/draft_box.js b/web-ui/app/js/mail_view/ui/draft_box.js new file mode 100644 index 00000000..95cffd14 --- /dev/null +++ b/web-ui/app/js/mail_view/ui/draft_box.js @@ -0,0 +1,74 @@ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_mail_edit_base', + 'page/events', + 'mail_view/data/mail_builder' + ], + + function (defineComponent, templates, withMailEditBase, events, mailBuilder) { + 'use strict'; + + return defineComponent(draftBox, withMailEditBase); + + function draftBox() { + this.defaultAttrs({ + closeMailButton: '.close-mail-button' + }); + + this.showNoMessageSelected = function() { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.buildMail = function(tag) { + return this.builtMail(tag).build(); + }; + + this.builtMail = function(tag) { + return mailBuilder.newMail(this.attr.ident) + .subject(this.select('subjectBox').val()) + .to(this.attr.recipientValues.to) + .cc(this.attr.recipientValues.cc) + .bcc(this.attr.recipientValues.bcc) + .body(this.select('bodyBox').val()) + .tag(tag); + }; + + this.renderDraftBox = function(ev, data) { + var mail = data.mail; + this.attr.ident = mail.ident; + + this.render(templates.compose.box, { + recipients: { + to: mail.header.to, + cc: mail.header.cc, + bcc: mail.header.bcc + }, + subject: mail.header.subject, + body: mail.body + }); + + this.select('recipientsFields').show(); + this.select('bodyBox').focus(); + this.select('tipMsg').hide(); + this.enableAutoSave(); + this.on(this.select('cancelButton'), 'click', this.showNoMessageSelected); + this.on(this.select('closeMailButton'), 'click', this.showNoMessageSelected); + }; + + this.mailDeleted = function(event, data) { + if (_.contains(_.pluck(data.mails, 'ident'), this.attr.ident)) { + this.trigger(events.dispatchers.rightPane.openNoMessageSelected); + } + }; + + this.after('initialize', function () { + this.on(this, events.mail.here, this.renderDraftBox); + this.on(document, events.mail.sent, this.showNoMessageSelected); + this.on(document, events.mail.deleted, this.mailDeleted); + this.trigger(document, events.mail.want, { mail: this.attr.mailIdent, caller: this }); + }); + } + } +); diff --git a/web-ui/app/js/mail_view/ui/draft_save_status.js b/web-ui/app/js/mail_view/ui/draft_save_status.js new file mode 100644 index 00000000..f56f82f1 --- /dev/null +++ b/web-ui/app/js/mail_view/ui/draft_save_status.js @@ -0,0 +1,25 @@ +define( + [ + 'flight/lib/component', + 'page/events' + ], + + function (defineComponent, events) { + 'use strict'; + + return defineComponent(draftSaveStatus); + + function draftSaveStatus() { + this.setMessage = function(msg) { + var node = this.$node; + return function () { node.text(msg); }; + }; + + this.after('initialize', function () { + this.on(document, events.mail.saveDraft, this.setMessage('Saving to Drafts...')); + this.on(document, events.mail.draftSaved, this.setMessage('Draft Saved.')); + this.on(document, events.ui.mail.changedSinceLastSave, this.setMessage('')); + }); + } + } +); diff --git a/web-ui/app/js/mail_view/ui/forward_box.js b/web-ui/app/js/mail_view/ui/forward_box.js new file mode 100644 index 00000000..e112b43d --- /dev/null +++ b/web-ui/app/js/mail_view/ui/forward_box.js @@ -0,0 +1,66 @@ +/*global Smail */ +/*global _ */ + +define( + [ + 'flight/lib/component', + 'helpers/view_helper', + 'mixins/with_hide_and_show', + 'mixins/with_compose_inline', + 'page/events', + 'views/i18n' + ], + + function (defineComponent, viewHelper, withHideAndShow, withComposeInline, events, i18n) { + 'use strict'; + + return defineComponent(forwardBox, withHideAndShow, withComposeInline); + + function forwardBox() { + var fwd = function(v) { return i18n('Fwd: ') + v; }; + + this.fetchTargetMail = function (ev) { + this.trigger(document, events.mail.want, { mail: this.attr.ident, caller: this }); + }; + + this.setupForwardBox = function() { + var mail = this.attr.mail; + this.attr.subject = fwd(mail.header.subject); + + this.renderInlineCompose('forward-box', { + subject: this.attr.subject, + recipients: { to: [], cc: []}, + body: viewHelper.quoteMail(mail) + }); + + this.on(this.select('subjectDisplay'), 'click', this.showSubjectInput); + this.select('recipientsDisplay').hide(); + this.select('recipientsFields').show(); + }; + + this.showSubjectInput = function() { + this.select('subjectDisplay').hide(); + this.select('subjectInput').show(); + this.select('subjectInput').focus(); + }; + + this.buildMail = function(tag) { + var builder = this.builtMail(tag).subject(this.select('subjectInput').val()); + + var headersToFwd = ['bcc', 'cc', 'date', 'from', 'message_id', 'reply_to', 'sender', 'to']; + var header = this.attr.mail.header; + _.each(headersToFwd, function (h) { + if (!_.isUndefined(header[h])) { + builder.header('resent_' + h, header[h]); + } + }); + + return builder.build(); + }; + + this.after('initialize', function () { + this.setupForwardBox(); + }); + } + } +); diff --git a/web-ui/app/js/mail_view/ui/mail_actions.js b/web-ui/app/js/mail_view/ui/mail_actions.js new file mode 100644 index 00000000..dc16ea9f --- /dev/null +++ b/web-ui/app/js/mail_view/ui/mail_actions.js @@ -0,0 +1,70 @@ +/*global Smail */ +/*global _ */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'page/events' + ], + + function (defineComponent, templates, events) { + 'use strict'; + + return defineComponent(mailActions); + + function mailActions() { + + this.defaultAttrs({ + replyButtonTop: '#reply-button-top', + viewMoreActions: '#view-more-actions', + replyAllButtonTop: '#reply-all-button-top', + deleteButtonTop: '#delete-button-top', + moreActions: '#more-actions' + }); + + + this.displayMailActions = function () { + + this.$node.html(templates.mails.mailActions()); + + this.select('moreActions').hide(); + + this.on(this.select('replyButtonTop'), 'click', function () { + this.trigger(document, events.ui.replyBox.showReply); + }.bind(this)); + + this.on(this.select('replyAllButtonTop'), 'click', function () { + this.trigger(document, events.ui.replyBox.showReplyAll); + this.select('moreActions').hide(); + }.bind(this)); + + this.on(this.select('deleteButtonTop'), 'click', function () { + this.trigger(document, events.ui.mail.delete, {mail: this.attr.mail}); + this.select('moreActions').hide(); + }.bind(this)); + + this.on(this.select('viewMoreActions'), 'click', function () { + this.select('moreActions').toggle(); + }.bind(this)); + + this.on(this.select('viewMoreActions'), 'blur', function (event) { + var replyButtonTopHover = this.select('replyAllButtonTop').is(':hover'); + var deleteButtonTopHover = this.select('deleteButtonTop').is(':hover'); + + if (replyButtonTopHover || deleteButtonTopHover) { + event.preventDefault(); + } else { + this.select('moreActions').hide(); + } + }.bind(this)); + + }; + + this.after('initialize', function () { + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + this.displayMailActions(); + }); + } + } +); diff --git a/web-ui/app/js/mail_view/ui/mail_view.js b/web-ui/app/js/mail_view/ui/mail_view.js new file mode 100644 index 00000000..1e27c879 --- /dev/null +++ b/web-ui/app/js/mail_view/ui/mail_view.js @@ -0,0 +1,242 @@ +/*global Smail */ +/*global _ */ +/*global Bloodhound */ + +'use strict'; + +define( + [ + 'flight/lib/component', + 'views/templates', + 'mail_view/ui/mail_actions', + 'helpers/view_helper', + 'mixins/with_hide_and_show', + 'page/events', + 'views/i18n' + ], + + function (defineComponent, templates, mailActions, viewHelpers, withHideAndShow, events, i18n) { + + return defineComponent(mailView, mailActions, withHideAndShow); + + function mailView() { + this.defaultAttrs({ + tags: '.tag', + newTagInput: '#new-tag-input', + newTagButton: '#new-tag-button', + addNew: '.add-new', + deleteModal: '#delete-modal', + trashButton: '#trash-button', + archiveButton: '#archive-button', + closeModalButton: '.close-reveal-modal', + closeMailButton: '.close-mail-button' + }); + + this.attachTagCompletion = function() { + this.attr.tagCompleter = new Bloodhound({ + datumTokenizer: function(d) { return [d.value]; }, + queryTokenizer: function(q) { return [q.trim()]; }, + remote: { + url: '/tags?q=%QUERY', + filter: function(pr) { return _.map(pr, function(pp) { return {value: pp.name}; }); } + } + }); + + this.attr.tagCompleter.initialize(); + + this.select('newTagInput').typeahead({ + hint: true, + highlight: true, + minLength: 1 + }, { + source: this.attr.tagCompleter.ttAdapter() + }); + }; + + this.displayMail = function (event, data) { + this.attr.mail = data.mail; + + var date = new Date(data.mail.header.date); + data.mail.header.formattedDate = viewHelpers.getFormattedDate(date); + + data.mail.security_casing = data.mail.security_casing || {}; + var signed = this.checkSigned(data.mail); + var encrypted = this.checkEncrypted(data.mail); + + this.$node.html(templates.mails.fullView({ + header: data.mail.header, + body: [], + statuses: viewHelpers.formatStatusClasses(data.mail.status), + ident: data.mail.ident, + tags: data.mail.tags, + encryptionStatus: encrypted, + signatureStatus: signed + })); + + this.$node.find('.bodyArea').html(viewHelpers.formatMailBody(data.mail)); + this.trigger(document, events.search.highlightResults, {where: '.bodyArea'}); + this.trigger(document, events.search.highlightResults, {where: '.subjectArea'}); + this.trigger(document, events.search.highlightResults, {where: '.msg-header .recipients'}); + + this.attachTagCompletion(); + + this.select('tags').on('click', function (event) { + this.removeTag($(event.target).data('tag')); + }.bind(this)); + + this.addTagLoseFocus(); + this.on(this.select('newTagButton'), 'click', this.showNewTagInput); + this.on(this.select('newTagInput'), 'keydown', this.handleKeyDown); + this.on(this.select('newTagInput'), 'typeahead:selected typeahead:autocompleted', this.createNewTag.bind(this)); + this.on(this.select('newTagInput'), 'blur', this.addTagLoseFocus); + this.on(this.select('trashButton'), 'click', this.moveToTrash); + this.on(this.select('archiveButton'), 'click', this.archiveIt); + this.on(this.select('closeModalButton'), 'click', this.closeModal); + this.on(this.select('closeMailButton'), 'click', this.openNoMessageSelectedPane); + + mailActions.attachTo('#mail-actions', data); + this.resetScroll(); + }; + + this.resetScroll = function(){ + $('#right-pane').scrollTop(0); + }; + + this.checkEncrypted = function(mail) { + if(_.isEmpty(mail.security_casing.locks)) { return 'not-encrypted'; } + + var status = ['encrypted']; + + if(_.any(mail.security_casing.locks, function (lock) { return lock.state === 'valid'; })) { status.push('encryption-valid'); } + else { status.push('encryption-failure'); } + + return status.join(' '); + }; + + this.checkSigned = function(mail) { + if(_.isEmpty(mail.security_casing.imprints)) { return 'not-signed'; } + + var status = ['signed']; + + if(_.any(mail.security_casing.imprints, function(imprint) { return imprint.state === 'from_revoked'; })) { + status.push('signature-revoked'); + } + if(_.any(mail.security_casing.imprints, function(imprint) { return imprint.state === 'from_expired'; })) { + status.push('signature-expired'); + } + + if(this.isNotTrusted(mail)) { + status.push('signature-not-trusted'); + } + + + return status.join(' '); + }; + + this.isNotTrusted = function(mail){ + return _.any(mail.security_casing.imprints, function(imprint) { + if(_.isNull(imprint.seal)){ + return true; + } + var currentTrust = _.isUndefined(imprint.seal.trust) ? imprint.seal.validity : imprint.seal.trust; + return currentTrust === 'no_trust'; + }); + }; + + this.openNoMessageSelectedPane = function(ev, data) { + this.trigger(document, events.dispatchers.rightPane.openNoMessageSelected); + }; + + this.createNewTag = function() { + var tagsCopy = this.attr.mail.tags.slice(); + tagsCopy.push(this.select('newTagInput').val()); + this.attr.tagCompleter.clear(); + this.attr.tagCompleter.clearPrefetchCache(); + this.attr.tagCompleter.clearRemoteCache(); + this.trigger(document, events.mail.tags.update, { ident: this.attr.mail.ident, tags: _.uniq(tagsCopy)}); + this.trigger(document, events.dispatchers.tags.refreshTagList); + }; + + this.handleKeyDown = function(event) { + var ENTER_KEY = 13; + var ESC_KEY = 27; + + if (event.which === ENTER_KEY){ + event.preventDefault(); + this.createNewTag(); + } else if (event.which === ESC_KEY) { + event.preventDefault(); + this.addTagLoseFocus(); + } + }; + + this.addTagLoseFocus = function () { + this.select('newTagInput').hide(); + this.select('newTagInput').typeahead('val', ''); + this.select('addNew').show(); + }; + + this.showNewTagInput = function () { + this.select('newTagInput').show(); + this.select('newTagInput').focus(); + this.select('addNew').hide(); + }; + + this.removeTag = function (tag) { + var filteredTags = _.without(this.attr.mail.tags, tag); + if (_.isEmpty(filteredTags)){ + this.displayMail({}, { mail: this.attr.mail }); + this.select('deleteModal').foundation('reveal', 'open'); + } else { + this.updateTags(filteredTags); + } + }; + + this.moveToTrash = function(){ + this.closeModal(); + this.trigger(document, events.ui.mail.delete, { mail: this.attr.mail }); + }; + + this.archiveIt = function() { + this.updateTags([]); + this.closeModal(); + this.trigger(document, events.ui.userAlerts.displayMessage, { message: i18n.get('Your message was archive it!') }); + this.openNoMessageSelectedPane(); + }; + + this.closeModal = function() { + $('#delete-modal').foundation('reveal', 'close'); + }; + + this.updateTags = function(tags) { + this.trigger(document, events.mail.tags.update, {ident: this.attr.mail.ident, tags: tags}); + }; + + this.tagsUpdated = function(ev, data) { + data = data || {}; + this.attr.mail.tags = data.tags; + this.displayMail({}, { mail: this.attr.mail }); + this.trigger(document, events.ui.tagList.refresh); + }; + + this.mailDeleted = function(ev, data) { + if (_.contains(_.pluck(data.mails, 'ident'), this.attr.mail.ident)) { + this.openNoMessageSelectedPane(); + } + }; + + this.fetchMailToShow = function () { + this.trigger(events.mail.want, {mail: this.attr.ident, caller: this}); + }; + + this.after('initialize', function () { + this.on(this, events.mail.here, this.displayMail); + this.on(this, events.mail.notFound, this.openNoMessageSelectedPane); + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + this.on(document, events.mail.tags.updated, this.tagsUpdated); + this.on(document, events.mail.deleted, this.mailDeleted); + this.fetchMailToShow(); + }); + } + } +); diff --git a/web-ui/app/js/mail_view/ui/no_message_selected_pane.js b/web-ui/app/js/mail_view/ui/no_message_selected_pane.js new file mode 100644 index 00000000..64b9a8ff --- /dev/null +++ b/web-ui/app/js/mail_view/ui/no_message_selected_pane.js @@ -0,0 +1,25 @@ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mixins/with_hide_and_show', + 'page/events' + ], + + function(defineComponent, templates, withHideAndShow, events) { + 'use strict'; + + return defineComponent(noMessageSelectedPane, withHideAndShow); + + function noMessageSelectedPane() { + this.render = function() { + this.$node.html(templates.noMessageSelected()); + }; + + this.after('initialize', function () { + this.render(); + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + }); + } + } +); diff --git a/web-ui/app/js/mail_view/ui/recipients/recipient.js b/web-ui/app/js/mail_view/ui/recipients/recipient.js new file mode 100644 index 00000000..967ff61b --- /dev/null +++ b/web-ui/app/js/mail_view/ui/recipients/recipient.js @@ -0,0 +1,36 @@ +'use strict'; + +define( + [ + 'flight/lib/component', + 'views/templates' + ], + + function (defineComponent, templates) { + + 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); + return component; + }; + + this.destroy = function () { + this.$node.remove(); + this.teardown(); + }; + + this.select = function () { + this.$node.find('.recipient-value').addClass('selected'); + }; + + this.unselect = function () { + this.$node.find('.recipient-value').removeClass('selected'); + }; + } + } +); diff --git a/web-ui/app/js/mail_view/ui/recipients/recipients.js b/web-ui/app/js/mail_view/ui/recipients/recipients.js new file mode 100644 index 00000000..86f9b9d3 --- /dev/null +++ b/web-ui/app/js/mail_view/ui/recipients/recipients.js @@ -0,0 +1,127 @@ +/*global _ */ + +define( + [ + 'flight/lib/component', + 'views/templates', + 'page/events', + 'mail_view/ui/recipients/recipients_input', + 'mail_view/ui/recipients/recipient', + 'mail_view/ui/recipients/recipients_iterator' + ], + function (defineComponent, templates, events, RecipientsInput, Recipient, RecipientsIterator) { + 'use strict'; + + return defineComponent(recipients); + + function recipients() { + this.defaultAttrs({ + navigationHandler: '.recipients-navigation-handler' + }); + + 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(); + } + + var SPECIAL_KEYS_ACTIONS = { + 8: deleteCurrentRecipient, + 46: deleteCurrentRecipient, + 37: moveLeft, + 39: moveRight + }; + + this.addRecipient = function(recipient) { + var newRecipient = Recipient.prototype.renderAndPrepend(this.$node, recipient); + this.attr.recipients.push(newRecipient); + }; + + this.recipientEntered = function (event, recipient) { + this.addRecipient(recipient); + 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().select(); + 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.deleteLast, this.deleteLastRecipient); + this.on(events.ui.recipients.selectLast, this.selectLastRecipient); + this.on(events.ui.recipients.entered, this.recipientEntered); + + 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/app/js/mail_view/ui/recipients/recipients_input.js b/web-ui/app/js/mail_view/ui/recipients/recipients_input.js new file mode 100644 index 00000000..79780ad2 --- /dev/null +++ b/web-ui/app/js/mail_view/ui/recipients/recipients_input.js @@ -0,0 +1,140 @@ +/*global _*/ +/*global Bloodhound */ +'use strict'; + +define([ + 'flight/lib/component', + 'page/events' + ], + function (defineComponent, events) { + + function recipientsInput() { + var EXIT_KEY_CODE_MAP = { + 8: 'backspace', + 37: 'left' + }, + ENTER_ADDRESS_KEY_CODE_MAP = { + 9: 'tab', + 186: 'semicolon', + 188: 'comma', + 32: 'space', + 13: 'enter', + 27: 'esc' + }, + EVENT_FOR = { + 8: events.ui.recipients.deleteLast, + 37: events.ui.recipients.selectLast + }, + self; + + var extractContactNames = function (response) { + return _.flatten(response.contacts, function (contact) { + var filterCriteria = contact.name ? + function (e) { + return { value: contact.name + ' <' + e + '>' }; + } : + function (e) { + return { value: e }; + }; + + return _.map(contact.addresses, filterCriteria); + }); + }; + + 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 (isEnterAddressKey(keyPressed)) { + if (!_.isEmpty(this.$node.val())) { + this.recipientSelected(null, { value: this.$node.val() }); + event.preventDefault(); + } + if((keyPressed !== 9 /* tab */)) { + event.preventDefault(); + } + } + + }; + + this.recipientSelected = function (event, data) { + var value = (data && data.value) || this.$node.val(); + + this.trigger(this.$node, events.ui.recipients.entered, { name: this.attr.name, address: value }); + reset(this.$node); + }; + + this.init = function () { + this.$node.typeahead({ + hint: true, + highlight: true, + minLength: 1 + }, { + source: createEmailCompleter().ttAdapter() + }); + }; + + 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.inputHasNoMail : events.ui.recipients.inputHasMail; + 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, '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/app/js/mail_view/ui/recipients/recipients_iterator.js b/web-ui/app/js/mail_view/ui/recipients/recipients_iterator.js new file mode 100644 index 00000000..73aefc79 --- /dev/null +++ b/web-ui/app/js/mail_view/ui/recipients/recipients_iterator.js @@ -0,0 +1,41 @@ +define(['helpers/iterator'], function (Iterator) { + + 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().unselect(); + this.iterator.previous().select(); + } + }; + + this.moveRight = function () { + this.iterator.current().unselect(); + if (this.iterator.hasNext()) { + this.iterator.next().select(); + } else { + this.input.focus(); + } + }; + + this.deleteCurrent = function () { + this.iterator.removeCurrent().destroy(); + + if (this.iterator.hasElements()) { + this.iterator.current().select() + } else { + this.input.focus(); + } + }; + } + +});
\ No newline at end of file diff --git a/web-ui/app/js/mail_view/ui/reply_box.js b/web-ui/app/js/mail_view/ui/reply_box.js new file mode 100644 index 00000000..2c39d8d6 --- /dev/null +++ b/web-ui/app/js/mail_view/ui/reply_box.js @@ -0,0 +1,102 @@ +/*global Smail */ +/*global _ */ + +define( + [ + 'flight/lib/component', + 'helpers/view_helper', + 'mixins/with_hide_and_show', + 'mixins/with_compose_inline', + 'page/events', + 'views/i18n' + ], + + function (defineComponent, viewHelper, withHideAndShow, withComposeInline, events, i18n) { + 'use strict'; + + return defineComponent(replyBox, withHideAndShow, withComposeInline); + + function replyBox() { + this.defaultAttrs({ + replyType: 'reply', + draftReply: false, + mail: null, + mailBeingRepliedIdent: undefined + }); + + this.getRecipients = function() { + if (this.attr.replyType === 'replyall') { + return this.attr.mail.replyToAllAddress(); + } else { + return this.attr.mail.replyToAddress(); + } + }; + + var re = function(v) { return i18n('re') + v; }; + + this.setupReplyBox = function() { + var recipients, body; + + if (this.attr.draftReply){ + this.attr.ident = this.attr.mail.ident; + this.attr.mailBeingRepliedIdent = this.attr.mail.draft_reply_for; + + recipients = this.attr.mail.recipients(); + body = this.attr.mail.body; + this.attr.subject = this.attr.mail.header.subject; + } else { + this.attr.mailBeingRepliedIdent = this.attr.mail.ident; + recipients = this.getRecipients(); + body = viewHelper.quoteMail(this.attr.mail); + this.attr.subject = re(this.attr.mail.header.subject); + } + + this.attr.recipientValues.to = recipients.to; + this.attr.recipientValues.cc = recipients.cc; + + this.renderInlineCompose('reply-box', { + recipients: recipients, + subject: this.attr.subject, + body: body + }); + + this.on(this.select('recipientsDisplay'), 'click keydown', this.showRecipientFields); + this.on(this.select('subjectDisplay'), 'click', this.showSubjectInput); + }; + + this.showRecipientFields = function(ev, data) { + if(!ev.keyCode || ev.keyCode === 13){ + this.select('recipientsDisplay').hide(); + this.select('recipientsFields').show(); + $('#recipients-to-area .tt-input').focus(); + } + }; + + this.showSubjectInput = function() { + this.select('subjectDisplay').hide(); + this.select('subjectInput').show(); + this.select('subjectInput').focus(); + }; + + this.buildMail = function(tag) { + var builder = this.builtMail(tag).subject(this.select('subjectInput').val()); + if(!_.isUndefined(this.attr.mail.header.message_id)) { + builder.header('in_reply_to', this.attr.mail.header.message_id); + } + + if(!_.isUndefined(this.attr.mail.header.list_id)) { + builder.header('list_id', this.attr.mail.header.list_id); + } + + var mail = builder.build(); + mail.setDraftReplyFor(this.attr.mailBeingRepliedIdent); + + return mail; + }; + + this.after('initialize', function () { + this.setupReplyBox(); + }); + } + } +); diff --git a/web-ui/app/js/mail_view/ui/reply_section.js b/web-ui/app/js/mail_view/ui/reply_section.js new file mode 100644 index 00000000..866a255a --- /dev/null +++ b/web-ui/app/js/mail_view/ui/reply_section.js @@ -0,0 +1,102 @@ +define( + [ + 'flight/lib/component', + 'views/templates', + 'mail_view/ui/reply_box', + 'mail_view/ui/forward_box', + 'mixins/with_hide_and_show', + 'page/events' + ], + + function (defineComponent, templates, ReplyBox, ForwardBox, withHideAndShow, events) { + 'use strict'; + + return defineComponent(replySection, withHideAndShow); + + function replySection() { + this.defaultAttrs({ + replyButton: '#reply-button', + replyAllButton: '#reply-all-button', + forwardButton: '#forward-button', + replyBox: '#reply-box', + replyType: 'reply' + }); + + this.showReply = function() { + this.attr.replyType = 'reply'; + this.fetchEmailToReplyTo(); + }; + + this.showReplyAll = function() { + this.attr.replyType = 'replyall'; + this.fetchEmailToReplyTo(); + }; + + this.showForward = function() { + this.attr.replyType = 'forward'; + this.fetchEmailToReplyTo(); + }; + + this.render = function () { + this.$node.html(templates.compose.replySection); + + this.on(this.select('replyButton'), 'click', this.showReply); + this.on(this.select('replyAllButton'), 'click', this.showReplyAll); + this.on(this.select('forwardButton'), 'click', this.showForward); + }; + + this.checkForDraftReply = function() { + this.render(); + this.select('replyButton').hide(); + this.select('replyAllButton').hide(); + this.select('forwardButton').hide(); + + this.trigger(document, events.mail.draftReply.want, {ident: this.attr.ident}); + }; + + this.fetchEmailToReplyTo = function (ev) { + this.trigger(document, events.mail.want, { mail: this.attr.ident, caller: this }); + }; + + this.showDraftReply = function(ev, data) { + this.hideButtons(); + ReplyBox.attachTo(this.select('replyBox'), { mail: data.mail, draftReply: true }); + }; + + this.showReplyComposeBox = function (ev, data) { + this.hideButtons(); + if(this.attr.replyType === 'forward') { + ForwardBox.attachTo(this.select('replyBox'), { mail: data.mail }); + } else { + ReplyBox.attachTo(this.select('replyBox'), { mail: data.mail, replyType: this.attr.replyType }); + } + }; + + this.hideButtons = function() { + this.select('replyButton').hide(); + this.select('replyAllButton').hide(); + this.select('forwardButton').hide(); + }; + + this.showButtons = function () { + this.select('replyBox').empty(); + this.select('replyButton').show(); + this.select('replyAllButton').show(); + this.select('forwardButton').show(); + }; + + this.after('initialize', function () { + this.on(document, events.ui.replyBox.showReply, this.showReply); + this.on(document, events.ui.replyBox.showReplyAll, this.showReplyAll); + this.on(document, events.ui.composeBox.trashReply, this.showButtons); + this.on(this, events.mail.here, this.showReplyComposeBox); + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + + this.on(document, events.mail.draftReply.notFound, this.showButtons); + this.on(document, events.mail.draftReply.here, this.showDraftReply); + + this.checkForDraftReply(); + }); + } + } +); diff --git a/web-ui/app/js/mail_view/ui/send_button.js b/web-ui/app/js/mail_view/ui/send_button.js new file mode 100644 index 00000000..44df3ae6 --- /dev/null +++ b/web-ui/app/js/mail_view/ui/send_button.js @@ -0,0 +1,85 @@ +/*global _ */ +'use strict'; + +define([ + 'flight/lib/component', + 'flight/lib/utils', + 'page/events' + ], + function (defineComponent, utils, events) { + + return defineComponent(sendButton); + + function sendButton() { + var RECIPIENTS_BOXES_COUNT = 3; + + this.enableButton = function () { + this.$node.prop('disabled', false); + }; + + this.disableButton = function () { + this.$node.prop('disabled', true); + }; + + this.atLeastOneFieldHasRecipients = function () { + return _.any(_.values(this.attr.recipients), function (e) { return !_.isEmpty(e); }); + }; + + this.atLeastOneInputHasMail = function () { + return _.any(_.values(this.attr.inputHasMail), function (e) { return e === true; }); + }; + + this.updateButton = function () { + if (this.atLeastOneInputHasMail() || this.atLeastOneFieldHasRecipients()) { + this.enableButton(); + } else { + this.disableButton(); + } + }; + + this.inputHasNoMail = function (ev, data) { + this.attr.inputHasMail[data.name] = false; + this.updateButton(); + }; + + this.inputHasMail = function (ev, data) { + this.attr.inputHasMail[data.name] = true; + this.updateButton(); + }; + + this.updateRecipientsForField = function (ev, data) { + this.attr.recipients[data.recipientsName] = data.newRecipients; + this.attr.inputHasMail[data.recipientsName] = false; + + this.updateButton(); + }; + + this.updateRecipientsAndSendMail = function () { + + this.on(document, events.ui.mail.recipientsUpdated, utils.countThen(RECIPIENTS_BOXES_COUNT, function () { + this.trigger(document, events.ui.mail.send); + this.off(document, events.ui.mail.recipientsUpdated); + }.bind(this))); + + this.trigger(document, events.ui.recipients.doCompleteInput); + }; + + this.after('initialize', function () { + this.attr.recipients = {}; + this.attr.inputHasMail = {}; + + this.on(document, events.ui.recipients.inputHasMail, this.inputHasMail); + this.on(document, events.ui.recipients.inputHasNoMail, this.inputHasNoMail); + this.on(document, events.ui.recipients.updated, this.updateRecipientsForField); + + this.on(this.$node, 'click', this.updateRecipientsAndSendMail); + + this.on(document, events.dispatchers.rightPane.clear, this.teardown); + this.on(document, events.ui.sendbutton.enable, this.enableButton); + + this.disableButton(); + }); + } + + } +); |