diff options
| author | Dharmang Soni <dpr@odoo.com> | 2018-07-25 13:14:57 +0000 |
|---|---|---|
| committer | Alexandre Kühn <aku@odoo.com> | 2018-11-26 11:34:00 +0000 |
| commit | fb051d342085b4558de7cdf489527e8b888181df (patch) | |
| tree | c80f784887d172c9f9ff6ecccd4880e7fa2d80e4 | |
| parent | db0f80ee537424c15e1bd1852b2e1dbdbedf4fea (diff) | |
[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
| -rw-r--r-- | addons/mail/static/src/js/composers/basic_composer.js | 187 | ||||
| -rw-r--r-- | addons/mail/static/src/scss/composer.scss | 23 | ||||
| -rw-r--r-- | addons/mail/static/src/scss/thread_window.scss | 3 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/composer.xml | 6 | ||||
| -rw-r--r-- | 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); }, @@ -254,6 +266,24 @@ var BasicComposer = Widget.extend({ 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 * @returns {$.Deferred<Object[]>} @@ -378,6 +408,67 @@ var BasicComposer = Widget.extend({ }); }, /** + * Allowing to upload attachment with file selector as well drag drop feature. + * + * @private + * @param {Array<File>} 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 */ _renderAttachments: function () { @@ -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.$('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: '', - }; + this._processAttachmentChange({ + files: ev.currentTarget.files, + submitForm: true }); - 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(); @@ -574,6 +683,32 @@ var BasicComposer = Widget.extend({ 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 * * @private 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 @@ <templates xml:space="preserve"> <div t-name="mail.Composer" t-attf-class="o_thread_composer #{widget.extended ? 'o_thread_composer_extended' : (widget.notInline ? '' : 'o_chat_inline_composer')} #{widget.isMini ? 'o_chat_mini_composer' : ''}"> + <div t-if="widget.isMini || !widget.notInline" class="o_file_drop_zone_container d-none"> + <h4>Drag Files Here <span class="fa fa-download"/></h4> + </div> <div class="o_composer_container"> <img t-if="widget.avatarURL" height="36" width="36" class="o_thread_message_sidebar o_chatter_avatar rounded-circle" t-att-src="widget.avatarURL" alt="Avatar"/> <div t-if="widget.extended" class="o_composer_subject"> <input tabindex="1" type="text" placeholder="Subject" class="o_input"/> </div> <div t-attf-class="o_composer #{widget.extended ? 'o_extended_composer' : ''}"> + <div t-if="widget.notInline" class="o_file_drop_zone_container d-none"> + <h4>Drag Files Here <span class="fa fa-download"/></h4> + </div> <div class="o_composer_input"> <textarea class="o_input o_composer_text_field" tabindex="2" placeholder="Write something..."/> <div class="o_chatter_composer_tools"> diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index db43fcd02ea..619db9e1403 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1177,7 +1177,8 @@ class Binary(http.Controller): args.append({ 'filename': filename, 'mimetype': ufile.content_type, - 'id': attachment.id + 'id': attachment.id, + 'size': attachment.file_size }) return out % (json.dumps(callback), json.dumps(args)) |
