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

+