From fb051d342085b4558de7cdf489527e8b888181df Mon Sep 17 00:00:00 2001 From: Dharmang Soni Date: Wed, 25 Jul 2018 13:14:57 +0000 Subject: [PATCH] [IMP] mail: Allowing drag/drop files/images on mail composer By this commit user can upload attachments by drag and drop in chatter composer. This feature can be used from discuss UI, chat windows and form view chatter. Before this commit chatter unlinks the attachment if another attachment uploaded with same name. Criteria for removing duplicate attachment is changed with this commit. Now it will remove the attachment based on name and size of attachment. The reason for this change is, when you upload image with same name from different directory with old condition it was not possible to attach more then one image/file of same name. Related to task #32469 Closes #18914 --- .../static/src/js/composers/basic_composer.js | 187 +++++++++++++++--- addons/mail/static/src/scss/composer.scss | 23 ++- .../mail/static/src/scss/thread_window.scss | 3 + addons/mail/static/src/xml/composer.xml | 6 + addons/web/controllers/main.py | 3 +- 5 files changed, 193 insertions(+), 29 deletions(-) diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js index 8a533cd4515..c8f7d702af4 100644 --- a/addons/mail/static/src/js/composers/basic_composer.js +++ b/addons/mail/static/src/js/composers/basic_composer.js @@ -28,6 +28,8 @@ var BasicComposer = Widget.extend({ 'focusout .o_composer_button_emoji': '_onEmojiButtonFocusout', 'click .o_mail_emoji_container .o_mail_emoji': '_onEmojiImageClick', 'focus .o_mail_emoji_container .o_mail_emoji': '_onEmojiImageFocus', + 'dragover .o_file_drop_zone_container': '_onFileDragover', + 'drop .o_file_drop_zone_container': '_onFileDrop', 'input .o_input': '_onInput', 'keydown .o_composer_input textarea': '_onKeydown', 'keyup .o_composer_input': '_onKeyup', @@ -124,9 +126,19 @@ var BasicComposer = Widget.extend({ // Mention this._mentionManager.prependTo(this.$('.o_composer')); + // Drag-Drop files + // Allowing body to detect dragenter and dragleave for display + var $body = $('body'); + this._dropZoneNS = _.uniqueId('o_dz_'); // For event namespace used when multiple chat window is open + $body.on('dragleave.' + this._dropZoneNS, this._onBodyFileDragLeave.bind(this)); + $body.on("dragover." + this._dropZoneNS, this._onBodyFileDragover.bind(this)); + $body.on("drop." + this._dropZoneNS, this._onBodyFileDrop.bind(this)); return this._super(); }, destroy: function () { + $("body").off('dragleave.' + this._dropZoneNS); + $("body").off('dragover.' + this._dropZoneNS); + $("body").off('drop.' + this._dropZoneNS); $(window).off(this.fileuploadID); return this._super.apply(this, arguments); }, @@ -253,6 +265,24 @@ var BasicComposer = Widget.extend({ _hideEmojis: function () { this._$emojisContainer.remove(); }, + /** + * Making sure that dragging content is external files. + * Ignoring other content draging like text. + * + * @private + * @param {DataTransfer} dataTransfer + * @returns {boolean} + */ + _isDragSourceExternalFile: function (dataTransfer) { + var DragDataType = dataTransfer.types; + if (DragDataType.constructor === DOMStringList) { + return DragDataType.contains('Files'); + } + if (DragDataType.constructor === Array) { + return DragDataType.indexOf('Files') !== -1; + } + return false; + }, /** * @private * @param {string} search @@ -377,6 +407,67 @@ var BasicComposer = Widget.extend({ command: commands.length > 0 ? commands[0].name : undefined, }); }, + /** + * Allowing to upload attachment with file selector as well drag drop feature. + * + * @private + * @param {Array} params.files + * @param {boolean} params.submitForm [optional] + */ + _processAttachmentChange: function (params) { + var self = this, + attachments = this.get('attachment_ids'), + files = params.files, + submitForm = params.submitForm; + _.each(files, function (file) { + var attachment = _.findWhere(attachments, { + name: file.name, + size: file.size + }); + // if the files already exits, delete the file before upload + if (attachment) { + self._attachmentDataSet.unlink([attachment.id]); + attachments = _.without(attachments, attachment); + } + }); + var $form = this.$('form.o_form_binary_form'); + if (submitForm) { + $form.submit(); + this._$attachmentButton.prop('disabled', true); + } else { + var data = new FormData($form[0]); + _.each(files, function (file) { + // removing existing key with blank data and appending again with file info + // In safari, existing key will not be updated when append with new file. + data.delete("ufile"); + data.append("ufile", file, file.name); + $.ajax({ + url: $form.attr("action"), + type: "POST", + enctype: 'multipart/form-data', + processData: false, + contentType: false, + data: data, + success: function (result) { + var $el = $(result); + $.globalEval($el.contents().text()); + } + }); + }); + } + var uploadAttachments = _.map(files, function (file){ + return { + id: 0, + name: file.name, + filename: file.name, + url: '', + upload: true, + mimetype: '', + }; + }); + attachments = attachments.concat(uploadAttachments); + this.set('attachment_ids', attachments); + }, /** * @private */ @@ -419,33 +510,10 @@ var BasicComposer = Widget.extend({ * @param {jQuery.Event} ev */ _onAttachmentChange: function (ev) { - var self = this; - var files = ev.target.files; - var attachments = this.get('attachment_ids'); - - _.each(files, function (file){ - var attachment = _.findWhere(attachments, {name: file.name}); - // if the files already exits, delete the file before upload - if (attachment){ - self._attachmentDataSet.unlink([attachment.id]); - attachments = _.without(attachments, attachment); - } + this._processAttachmentChange({ + files: ev.currentTarget.files, + submitForm: true }); - - this.$('form.o_form_binary_form').submit(); - this._$attachmentButton.prop('disabled', true); - var uploadAttachments = _.map(files, function (file){ - return { - id: 0, - name: file.name, - filename: file.name, - url: '', - upload: true, - mimetype: '', - }; - }); - attachments = attachments.concat(uploadAttachments); - this.set('attachment_ids', attachments); }, /** * @private @@ -497,6 +565,7 @@ var BasicComposer = Widget.extend({ name: file.name || file.filename, filename: file.filename, mimetype: file.mimetype, + size: file.size, url: session.url('/web/content', { id: file.id, download: true }), }); } @@ -517,6 +586,46 @@ var BasicComposer = Widget.extend({ attachmentViewer.appendTo($('body')); } }, + /** + * @private + * @param {MouseEvent} ev + */ + _onBodyFileDragLeave: function (ev) { + // On every dragenter chain created with parent child element + // That's why dragleave is fired every time when a child elemnt is hovered + // so here we hide dropzone based on mouse position + if (ev.originalEvent.clientX <= 0 + || ev.originalEvent.clientY <= 0 + || ev.originalEvent.clientX >= window.innerWidth + || ev.originalEvent.clientY >= window.innerHeight + ) { + this.$(".o_file_drop_zone_container").addClass("d-none"); + } + }, + /** + * When user start dragging on element drop area will be visible to drop selected files. + * + * @private + * @param {MouseEvent} ev + */ + _onBodyFileDragover: function (ev) { + ev.preventDefault(); + if (this._isDragSourceExternalFile(ev.originalEvent.dataTransfer)) { + this.$(".o_file_drop_zone_container").removeClass("d-none"); + } + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onBodyFileDrop: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + this.$(".o_file_drop_zone_container").addClass("d-none"); + }, + /** + * @private + */ _onClickAddAttachment: function () { this.$('input.o_input_file').click(); this.$input.focus(); @@ -573,6 +682,32 @@ var BasicComposer = Widget.extend({ _onEmojiImageFocus: function () { clearTimeout(this._hideEmojisTimeout); }, + /** + * Setting drop Effect to copy so when mouse pointer on dropzone + * cursor icon changed to copy ('+') + * + * @private + * @param {MouseEvent} ev + */ + _onFileDragover: function (ev) { + ev.originalEvent.dataTransfer.dropEffect = "copy"; + }, + /** + * Called when user drop selected files on drop area + * + * @private + * @param {MouseEvent} ev + */ + _onFileDrop: function (ev) { + ev.preventDefault(); + // FIX: In case multiple chat windows are opened, and file droped in one of them + // at that time, other chat windows are still displaing drop areas so here hide them all with $ selector + $(".o_file_drop_zone_container").addClass("d-none"); + if (this._isDragSourceExternalFile(ev.originalEvent.dataTransfer)) { + var files = ev.originalEvent.dataTransfer.files; + this._processAttachmentChange({ files: files }); + } + }, /** * Called when the input in the composer changes * diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss index 081e7a69a4c..e076629f667 100644 --- a/addons/mail/static/src/scss/composer.scss +++ b/addons/mail/static/src/scss/composer.scss @@ -13,7 +13,6 @@ } .o_thread_composer { - position: relative; flex: 0 0 auto; display: flex; margin: 1px 0; @@ -111,6 +110,7 @@ .o_composer { border-radius: 2px; textarea { + padding-top: $o-mail-chatter-gap !important; padding-right: $grid-gutter-width; // avoid overlaps betwenn textarea and o_composer_button_full_composer } @@ -143,7 +143,7 @@ flex: 1 0 100%; } .o_composer, .o_composer_subject { - padding: $o-mail-chatter-gap $o-mail-chatter-gap 0; + padding: 0 $o-mail-chatter-gap 0; } .o_composer_send { margin-top: $o-mail-chatter-gap; @@ -167,6 +167,10 @@ .o_composer_input { display: flex; flex-flow: row wrap; + + textarea { + padding-top: 0px !important; + } } .o_composer_text_field { flex: 1 1 50%; @@ -230,6 +234,21 @@ display: none !important; } } + + .o_file_drop_zone_container { + display: flex; + position: absolute; + left: 0px; + bottom: 0px; + height: 100%; + width: 100%; + color: $o-brand-primary; + background: rgba(255, 255, 255, 0.9); + border: 2px dashed $o-brand-primary; + z-index: 1; + align-items: center; + justify-content: center; + } } .o_composer_attachments_list:not(:empty) { diff --git a/addons/mail/static/src/scss/thread_window.scss b/addons/mail/static/src/scss/thread_window.scss index 4dc34b5f4c5..abc8ff2846d 100644 --- a/addons/mail/static/src/scss/thread_window.scss +++ b/addons/mail/static/src/scss/thread_window.scss @@ -26,6 +26,9 @@ } .o_thread_composer { + .o_file_drop_zone_container { + top: 35px; + } .o_composer { .o_composer_input { width: 80%; diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml index 7dd31393d34..883c9752035 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -2,12 +2,18 @@
+
+

Drag Files Here

+
Avatar
+
+

Drag Files Here

+