From 5b0e086946155aa45b46ebf5010403af40634377 Mon Sep 17 00:00:00 2001 From: blackst0ne Date: Tue, 16 May 2017 19:52:17 +1100 Subject: Add an ability to cancel attaching file and redesign attaching files UI --- app/assets/javascripts/dropzone_input.js | 252 +++++++++++++++++----------- app/assets/stylesheets/pages/note_form.scss | 43 +++++ 2 files changed, 201 insertions(+), 94 deletions(-) (limited to 'app/assets') diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index b3a76fbb43e..988c484ed85 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -5,104 +5,154 @@ require('./preview_markdown'); window.DropzoneInput = (function() { function DropzoneInput(form) { - var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, uploads_path, showError, showSpinner, uploadFile, uploadProgress; + var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile; Dropzone.autoDiscover = false; - alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"; - alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""; - divHover = "
"; - divSpinner = "
"; - divAlert = "
"; - iconPaperclip = ""; - iconSpinner = ""; - uploadProgress = $("
"); - btnAlert = ""; - uploads_path = window.uploads_path || null; - max_file_size = gon.max_file_size || 10; - form_textarea = $(form).find(".js-gfm-input"); - form_textarea.wrap("
"); - form_textarea.on('paste', (function(_this) { + divHover = '
'; + iconPaperclip = ''; + $attachButton = form.find('.button-attach-file'); + $attachingFileMessage = form.find('.attaching-file-message'); + $cancelButton = form.find('.button-cancel-uploading-files'); + $retryLink = form.find('.retry-uploading-link'); + $uploadProgress = form.find('.uploading-progress'); + $uploadingErrorContainer = form.find('.uploading-error-container'); + $uploadingErrorMessage = form.find('.uploading-error-message'); + $uploadingProgressContainer = form.find('.uploading-progress-container'); + uploadsPath = window.uploads_path || null; + maxFileSize = gon.max_file_size || 10; + formTextarea = form.find('.js-gfm-input'); + formTextarea.wrap('
'); + formTextarea.on('paste', (function(_this) { return function(event) { return handlePaste(event); }; })(this)); - $mdArea = $(form_textarea).closest('.md-area'); - $(form).setupMarkdownPreview(); - form_dropzone = $(form).find('.div-dropzone'); - form_dropzone.parent().addClass("div-dropzone-wrapper"); - form_dropzone.append(divHover); - form_dropzone.find(".div-dropzone-hover").append(iconPaperclip); - form_dropzone.append(divSpinner); - form_dropzone.find(".div-dropzone-spinner").append(iconSpinner); - form_dropzone.find(".div-dropzone-spinner").append(uploadProgress); - form_dropzone.find(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); - if (!uploads_path) return; + // Add dropzone area to the form. + $mdArea = formTextarea.closest('.md-area'); + form.setupMarkdownPreview(); + $formDropzone = form.find('.div-dropzone'); + $formDropzone.parent().addClass('div-dropzone-wrapper'); + $formDropzone.append(divHover); + $formDropzone.find('.div-dropzone-hover').append(iconPaperclip); + + if (!uploadsPath) return; - dropzone = form_dropzone.dropzone({ - url: uploads_path, - dictDefaultMessage: "", + dropzone = $formDropzone.dropzone({ + url: uploadsPath, + dictDefaultMessage: '', clickable: true, - paramName: "file", - maxFilesize: max_file_size, + paramName: 'file', + maxFilesize: maxFileSize, uploadMultiple: false, headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, previewContainer: false, processing: function() { - return $(".div-dropzone-alert").alert("close"); + return $('.div-dropzone-alert').alert('close'); }, dragover: function() { $mdArea.addClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0.7); + form.find('.div-dropzone-hover').css('opacity', 0.7); }, dragleave: function() { $mdArea.removeClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0); + form.find('.div-dropzone-hover').css('opacity', 0); }, drop: function() { $mdArea.removeClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0); - form_textarea.focus(); + form.find('.div-dropzone-hover').css('opacity', 0); + formTextarea.focus(); }, success: function(header, response) { const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length; const shouldPad = processingFileCount >= 1; pasteText(response.link.markdown, shouldPad); + // Show 'Attach a file' link only when all files have been uploaded. + if (!processingFileCount) $attachButton.removeClass('hide'); }, - error: function(temp) { - var checkIfMsgExists, errorAlert; - errorAlert = $(form).find('.error-alert'); - checkIfMsgExists = errorAlert.children().length; - if (checkIfMsgExists === 0) { - errorAlert.append(divAlert); - $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed."); - } + error: function(file, errorMessage = 'Attaching the file failed.', xhr) { + // If 'error' event is fired by dropzone, the second parameter is error message. + // If the 'errorMessage' parameter is empty, the default error message is set. + // If the 'error' event is fired by backend (xhr) error response, the third parameter is + // xhr object (xhr.responseText is error message). + // On error we hide the 'Attach' and 'Cancel' buttons + // and show an error. + + // If there's xhr error message, let's show it instead of dropzone's one. + const message = xhr ? xhr.responseText : errorMessage; + + $uploadingErrorContainer.removeClass('hide'); + $uploadingErrorMessage.html(message); + $attachButton.addClass('hide'); + $cancelButton.addClass('hide'); }, totaluploadprogress: function(totalUploadProgress) { - uploadProgress.text(Math.round(totalUploadProgress) + "%"); + updateAttachingMessage(this.files, $attachingFileMessage); + $uploadProgress.text(Math.round(totalUploadProgress) + '%'); + }, + sending: function(file) { + // DOM elements already exist. + // Instead of dynamically generating them, + // we just either hide or show them. + $attachButton.addClass('hide'); + $uploadingErrorContainer.addClass('hide'); + $uploadingProgressContainer.removeClass('hide'); + $cancelButton.removeClass('hide'); }, - sending: function() { - form_dropzone.find(".div-dropzone-spinner").css({ - "opacity": 0.7, - "display": "inherit" - }); + removedfile: function() { + $attachButton.removeClass('hide'); + $cancelButton.addClass('hide'); + $uploadingProgressContainer.addClass('hide'); + $uploadingErrorContainer.addClass('hide'); }, queuecomplete: function() { - uploadProgress.text(""); - $(".dz-preview").remove(); - $(".markdown-area").trigger("input"); - $(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); + $('.dz-preview').remove(); + $('.markdown-area').trigger('input'); + + $uploadingProgressContainer.addClass('hide'); + $cancelButton.addClass('hide'); } }); - child = $(dropzone[0]).children("textarea"); + + child = $(dropzone[0]).children('textarea'); + + // removeAllFiles(true) stops uploading files (if any) + // and remove them from dropzone files queue. + $cancelButton.on('click', (e) => { + const target = e.target.closest('form').querySelector('.div-dropzone'); + + e.preventDefault(); + e.stopPropagation(); + Dropzone.forElement(target).removeAllFiles(true); + }); + + // If 'error' event is fired, we store a failed files, + // clear dropzone files queue, change status of failed files to undefined, + // and add that files to the dropzone files queue again. + // addFile() adds file to dropzone files queue and upload it. + $retryLink.on('click', (e) => { + const dropzoneInstance = Dropzone.forElement(e.target.closest('form').querySelector('.div-dropzone')); + const failedFiles = dropzoneInstance.files; + + e.preventDefault(); + + // 'true' parameter of removeAllFiles() cancels uploading of files that are being uploaded at the moment. + dropzoneInstance.removeAllFiles(true); + + failedFiles.map((failedFile, i) => { + const file = failedFile; + + if (file.status === Dropzone.ERROR) { + file.status = undefined; + file.accepted = undefined; + } + + return dropzoneInstance.addFile(file); + }); + }); + handlePaste = function(event) { var filename, image, pasteEvent, text; pasteEvent = event.originalEvent; @@ -110,25 +160,27 @@ window.DropzoneInput = (function() { image = isImage(pasteEvent); if (image) { event.preventDefault(); - filename = getFilename(pasteEvent) || "image.png"; - text = "{{" + filename + "}}"; + filename = getFilename(pasteEvent) || 'image.png'; + text = `{{${filename}}}`; pasteText(text); return uploadFile(image.getAsFile(), filename); } } }; + isImage = function(data) { var i, item; i = 0; while (i < data.clipboardData.items.length) { item = data.clipboardData.items[i]; - if (item.type.indexOf("image") !== -1) { + if (item.type.indexOf('image') !== -1) { return item; } i += 1; } return false; }; + pasteText = function(text, shouldPad) { var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; var formattedText = text; @@ -142,31 +194,33 @@ window.DropzoneInput = (function() { $(child).val(beforeSelection + formattedText + afterSelection); textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); textarea.style.height = `${textarea.scrollHeight}px`; - return form_textarea.trigger("input"); + return formTextarea.trigger('input'); }; + getFilename = function(e) { var value; if (window.clipboardData && window.clipboardData.getData) { - value = window.clipboardData.getData("Text"); + value = window.clipboardData.getData('Text'); } else if (e.clipboardData && e.clipboardData.getData) { - value = e.clipboardData.getData("text/plain"); + value = e.clipboardData.getData('text/plain'); } value = value.split("\r"); return value.first(); }; + uploadFile = function(item, filename) { var formData; formData = new FormData(); - formData.append("file", item, filename); + formData.append('file', item, filename); return $.ajax({ - url: uploads_path, - type: "POST", + url: uploadsPath, + type: 'POST', data: formData, - dataType: "json", + dataType: 'json', processData: false, contentType: false, headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, beforeSend: function() { showSpinner(); @@ -183,44 +237,54 @@ window.DropzoneInput = (function() { } }); }; + + updateAttachingMessage = (files, messageContainer) => { + let attachingMessage; + const filesCount = files.filter(function(file) { + return file.status === 'uploading' || + file.status === 'queued'; + }).length; + + // Dinamycally change uploading files text depending on files number in + // dropzone files queue. + if (filesCount > 1) { + attachingMessage = 'Attaching ' + filesCount + ' files -'; + } else { + attachingMessage = 'Attaching a file -'; + } + + messageContainer.text(attachingMessage); + }; + insertToTextArea = function(filename, url) { return $(child).val(function(index, val) { - return val.replace("{{" + filename + "}}", url); + return val.replace(`{{${filename}}}`, url); }); }; + appendToTextArea = function(url) { return $(child).val(function(index, val) { return val + url + "\n"; }); }; + showSpinner = function(e) { - return form.find(".div-dropzone-spinner").css({ - "opacity": 0.7, - "display": "inherit" - }); + return $uploadingProgressContainer.removeClass('hide'); }; + closeSpinner = function() { - return form.find(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); + return $uploadingProgressContainer.addClass('hide'); }; + showError = function(message) { - var checkIfMsgExists, errorAlert; - errorAlert = $(form).find('.error-alert'); - checkIfMsgExists = errorAlert.children().length; - if (checkIfMsgExists === 0) { - errorAlert.append(divAlert); - return $(".div-dropzone-alert").append(btnAlert + message); - } + $uploadingErrorContainer.removeClass('hide'); + $uploadingErrorMessage.html(message); }; - closeAlertMessage = function() { - return form.find(".div-dropzone-alert").alert("close"); - }; - form.find(".markdown-selector").click(function(e) { + + form.find('.markdown-selector').click(function(e) { e.preventDefault(); $(this).closest('.gfm-form').find('.div-dropzone').click(); - form_textarea.focus(); + formTextarea.focus(); }); } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 62f654ed343..9db26f99a75 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -277,6 +277,7 @@ .toolbar-text { font-size: 14px; line-height: 16px; + margin-top: 2px; @media (min-width: $screen-md-min) { float: left; @@ -402,3 +403,45 @@ } } } + +.uploading-container { + float: right; + + @media (max-width: $screen-xs-max) { + float: left; + margin-top: 5px; + } +} + +.uploading-error-icon, +.uploading-error-message { + color: $gl-text-red; +} + +.uploading-error-message { + @media (max-width: $screen-xs-max) { + &::after { + content: "\a"; + white-space: pre; + } + } +} + +.uploading-progress { + margin-right: 5px; +} + +.attach-new-file, +.button-attach-file, +.retry-uploading-link { + color: $gl-link-color; + padding: 0; + background: none; + border: 0; + font-size: 14px; + line-height: 16px; +} + +.markdown-selector { + color: $gl-link-color; +} -- cgit v1.2.1