293 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			293 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| import Dropzone from 'dropzone';
 | |
| import $ from 'jquery';
 | |
| import { escape } from 'lodash';
 | |
| import './behaviors/preview_markdown';
 | |
| import { spriteIcon } from '~/lib/utils/common_utils';
 | |
| import { getFilename } from '~/lib/utils/file_upload';
 | |
| import { truncate } from '~/lib/utils/text_utility';
 | |
| import { n__, __ } from '~/locale';
 | |
| import PasteMarkdownTable from './behaviors/markdown/paste_markdown_table';
 | |
| import axios from './lib/utils/axios_utils';
 | |
| import csrf from './lib/utils/csrf';
 | |
| 
 | |
| Dropzone.autoDiscover = false;
 | |
| 
 | |
| /**
 | |
|  * Return the error message string from the given response.
 | |
|  *
 | |
|  * @param {String|Object} res
 | |
|  */
 | |
| function getErrorMessage(res) {
 | |
|   if (!res || typeof res === 'string') {
 | |
|     return res;
 | |
|   }
 | |
| 
 | |
|   return res.message;
 | |
| }
 | |
| 
 | |
| export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
 | |
|   const divHover = '<div class="div-dropzone-hover"></div>';
 | |
|   const iconPaperclip = spriteIcon('paperclip', 'div-dropzone-icon s24');
 | |
|   const $attachButton = form.find('.button-attach-file');
 | |
|   const $attachingFileMessage = form.find('.attaching-file-message');
 | |
|   const $cancelButton = form.find('.button-cancel-uploading-files');
 | |
|   const $retryLink = form.find('.retry-uploading-link');
 | |
|   const $uploadProgress = form.find('.uploading-progress');
 | |
|   const $uploadingErrorContainer = form.find('.uploading-error-container');
 | |
|   const $uploadingErrorMessage = form.find('.uploading-error-message');
 | |
|   const $uploadingProgressContainer = form.find('.uploading-progress-container');
 | |
|   const uploadsPath = form.data('uploads-path') || window.uploads_path || null;
 | |
|   const maxFileSize = gon.max_file_size || 10;
 | |
|   const formTextarea = form.find('.js-gfm-input');
 | |
|   let handlePaste;
 | |
|   let pasteText;
 | |
|   let addFileToForm;
 | |
|   let updateAttachingMessage;
 | |
|   let uploadFile;
 | |
|   let hasPlainText;
 | |
| 
 | |
|   formTextarea.wrap('<div class="div-dropzone"></div>');
 | |
|   formTextarea.on('paste', (event) => handlePaste(event));
 | |
| 
 | |
|   // Add dropzone area to the form.
 | |
|   const $mdArea = formTextarea.closest('.md-area');
 | |
|   const $formDropzone = form.find('.div-dropzone');
 | |
|   $formDropzone.parent().addClass('div-dropzone-wrapper');
 | |
|   $formDropzone.append(divHover);
 | |
|   $formDropzone.find('.div-dropzone-hover').append(iconPaperclip);
 | |
| 
 | |
|   if (!uploadsPath) {
 | |
|     $formDropzone.addClass('js-invalid-dropzone');
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const dropzone = $formDropzone.dropzone({
 | |
|     url: uploadsPath,
 | |
|     dictDefaultMessage: '',
 | |
|     clickable: true,
 | |
|     paramName: 'file',
 | |
|     maxFilesize: maxFileSize,
 | |
|     uploadMultiple: false,
 | |
|     headers: csrf.headers,
 | |
|     previewContainer: false,
 | |
|     ...config,
 | |
|     dragover: () => {
 | |
|       $mdArea.addClass('is-dropzone-hover');
 | |
|       form.find('.div-dropzone-hover').css('opacity', 0.7);
 | |
|     },
 | |
|     dragleave: () => {
 | |
|       $mdArea.removeClass('is-dropzone-hover');
 | |
|       form.find('.div-dropzone-hover').css('opacity', 0);
 | |
|     },
 | |
|     drop: () => {
 | |
|       $mdArea.removeClass('is-dropzone-hover');
 | |
|       form.find('.div-dropzone-hover').css('opacity', 0);
 | |
|       formTextarea.focus();
 | |
|     },
 | |
|     success(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');
 | |
|       addFileToForm(response.link.url);
 | |
|     },
 | |
|     error: (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.
 | |
|       const message = getErrorMessage(errorMessage || xhr.responseText);
 | |
| 
 | |
|       $uploadingErrorContainer.removeClass('hide');
 | |
|       $uploadingErrorMessage.html(message);
 | |
|       $attachButton.addClass('hide');
 | |
|       $cancelButton.addClass('hide');
 | |
|     },
 | |
|     totaluploadprogress(totalUploadProgress) {
 | |
|       updateAttachingMessage(this.files, $attachingFileMessage);
 | |
|       $uploadProgress.text(`${Math.round(totalUploadProgress)}%`);
 | |
|     },
 | |
|     sending: () => {
 | |
|       // 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');
 | |
|     },
 | |
|     removedfile: () => {
 | |
|       $attachButton.removeClass('hide');
 | |
|       $cancelButton.addClass('hide');
 | |
|       $uploadingProgressContainer.addClass('hide');
 | |
|       $uploadingErrorContainer.addClass('hide');
 | |
|     },
 | |
|     queuecomplete: () => {
 | |
|       $('.dz-preview').remove();
 | |
|       $('.markdown-area').trigger('input');
 | |
| 
 | |
|       $uploadingProgressContainer.addClass('hide');
 | |
|       $cancelButton.addClass('hide');
 | |
|     },
 | |
|   });
 | |
| 
 | |
|   const child = $(dropzone[0]).children('textarea');
 | |
| 
 | |
|   // removeAllFiles(true) stops uploading files (if any)
 | |
|   // and remove them from dropzone files queue.
 | |
|   $cancelButton.on('click', (e) => {
 | |
|     e.preventDefault();
 | |
|     e.stopPropagation();
 | |
|     Dropzone.forElement($formDropzone.get(0)).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('.js-main-target-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) => {
 | |
|       const file = failedFile;
 | |
| 
 | |
|       if (file.status === Dropzone.ERROR) {
 | |
|         file.status = undefined;
 | |
|         file.accepted = undefined;
 | |
|       }
 | |
| 
 | |
|       return dropzoneInstance.addFile(file);
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   handlePaste = (event) => {
 | |
|     const pasteEvent = event.originalEvent;
 | |
|     const { clipboardData } = pasteEvent;
 | |
|     if (clipboardData && clipboardData.items) {
 | |
|       const converter = new PasteMarkdownTable(clipboardData);
 | |
|       // Apple Numbers copies a table as an image, HTML, and text, so
 | |
|       // we need to check for the presence of a table first.
 | |
|       if (converter.isTable()) {
 | |
|         event.preventDefault();
 | |
|         const text = converter.convertToTableMarkdown();
 | |
|         pasteText(text);
 | |
|       } else if (!hasPlainText(pasteEvent)) {
 | |
|         const fileList = [...clipboardData.files];
 | |
|         fileList.forEach((file) => {
 | |
|           if (file.type.indexOf('image') !== -1) {
 | |
|             event.preventDefault();
 | |
|             const MAX_FILE_NAME_LENGTH = 246;
 | |
| 
 | |
|             const filename = getFilename(file) || 'image.png';
 | |
|             const truncateFilename = truncate(filename, MAX_FILE_NAME_LENGTH);
 | |
|             const text = `{{${truncateFilename}}}`;
 | |
|             pasteText(text);
 | |
| 
 | |
|             uploadFile(file, truncateFilename);
 | |
|           }
 | |
|         });
 | |
|       }
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   hasPlainText = (data) => {
 | |
|     const clipboardDataList = [...data.clipboardData.items];
 | |
|     return clipboardDataList.some((item) => item.type === 'text/plain');
 | |
|   };
 | |
| 
 | |
|   pasteText = (text, shouldPad) => {
 | |
|     let formattedText = text;
 | |
|     if (shouldPad) {
 | |
|       formattedText += '\n\n';
 | |
|     }
 | |
|     const textarea = child.get(0);
 | |
|     const caretStart = textarea.selectionStart;
 | |
|     const caretEnd = textarea.selectionEnd;
 | |
|     const textEnd = $(child).val().length;
 | |
|     const beforeSelection = $(child).val().substring(0, caretStart);
 | |
|     const afterSelection = $(child).val().substring(caretEnd, textEnd);
 | |
|     $(child).val(beforeSelection + formattedText + afterSelection);
 | |
|     textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
 | |
|     textarea.style.height = `${textarea.scrollHeight}px`;
 | |
|     formTextarea.get(0).dispatchEvent(new Event('input'));
 | |
|     return formTextarea.trigger('input');
 | |
|   };
 | |
| 
 | |
|   addFileToForm = (path) => {
 | |
|     $(form).append(`<input type="hidden" name="files[]" value="${escape(path)}">`);
 | |
|   };
 | |
| 
 | |
|   const showSpinner = () => $uploadingProgressContainer.removeClass('hide');
 | |
| 
 | |
|   const closeSpinner = () => $uploadingProgressContainer.addClass('hide');
 | |
| 
 | |
|   const showError = (message) => {
 | |
|     $uploadingErrorContainer.removeClass('hide');
 | |
|     $uploadingErrorMessage.html(message);
 | |
|   };
 | |
| 
 | |
|   const insertToTextArea = (filename, url) => {
 | |
|     const $child = $(child);
 | |
|     const textarea = $child.get(0);
 | |
|     const caretStart = textarea.selectionStart;
 | |
|     const caretEnd = textarea.selectionEnd;
 | |
|     const formattedText = `{{${filename}}}`;
 | |
|     $child.val((index, val) => val.replace(formattedText, url));
 | |
|     textarea.setSelectionRange(
 | |
|       caretStart - formattedText.length + url.length,
 | |
|       caretEnd - formattedText.length + url.length,
 | |
|     );
 | |
|     $child.trigger('change');
 | |
|   };
 | |
| 
 | |
|   uploadFile = (item, filename) => {
 | |
|     const formData = new FormData();
 | |
|     formData.append('file', item, filename);
 | |
| 
 | |
|     showSpinner();
 | |
| 
 | |
|     axios
 | |
|       .post(uploadsPath, formData)
 | |
|       .then(({ data }) => {
 | |
|         const md = data.link.markdown;
 | |
| 
 | |
|         insertToTextArea(filename, md);
 | |
|         closeSpinner();
 | |
|       })
 | |
|       .catch((e) => {
 | |
|         showError(e.response.data.message);
 | |
|         closeSpinner();
 | |
|       });
 | |
|   };
 | |
| 
 | |
|   updateAttachingMessage = (files, messageContainer) => {
 | |
|     const filesCount = files.filter(
 | |
|       (file) => file.status === 'uploading' || file.status === 'queued',
 | |
|     ).length;
 | |
|     const attachingMessage = n__('Attaching a file', 'Attaching %d files', filesCount);
 | |
| 
 | |
|     messageContainer.text(`${attachingMessage} -`);
 | |
|   };
 | |
| 
 | |
|   form.find('.markdown-selector').click(function onMarkdownClick(e) {
 | |
|     e.preventDefault();
 | |
|     $(this).closest('.gfm-form').find('.div-dropzone').click();
 | |
|     formTextarea.focus();
 | |
|   });
 | |
| 
 | |
|   return $formDropzone.get(0) ? Dropzone.forElement($formDropzone.get(0)) : null;
 | |
| }
 |