From fb051d342085b4558de7cdf489527e8b888181df Mon Sep 17 00:00:00 2001 From: Dharmang Soni Date: Wed, 25 Jul 2018 13:14:57 +0000 Subject: [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 --- .../mail/static/src/js/composers/basic_composer.js | 187 ++++++++++++++++++--- 1 file changed, 161 insertions(+), 26 deletions(-) (limited to 'addons/mail/static/src/js/composers/basic_composer.js') 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.$('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(); @@ -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 * -- cgit v1.2.3