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');
|
|
form.setupMarkdownPreview();
|
|
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;
|
|
}
|