Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor to use dropzone native methods upload files #20263

Closed
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
15aab4d
Refactor to use dropzone native methods upload files
tyroneyeh Jul 6, 2022
9926ecd
minor modification
tyroneyeh Jul 6, 2022
41c49c1
Adjust inline const
tyroneyeh Jul 14, 2022
30bc993
Update web_src/js/features/comp/ImagePaste.js
tyroneyeh Jul 18, 2022
8a137c3
Update web_src/js/features/comp/ImagePaste.js
tyroneyeh Jul 18, 2022
f31eb8e
Fix cannot edit on review
tyroneyeh Jul 18, 2022
ad444d3
Use closest find textarea
tyroneyeh Jul 19, 2022
6576760
Merge branch 'main' into 20130_add_paste_image_legend_new
tyroneyeh Jul 19, 2022
16085be
Edit comment cannot use closeset find to textarea
tyroneyeh Jul 27, 2022
349ee1f
Adjust pasting additional filename to textarea position below message
tyroneyeh Jul 28, 2022
7569d10
Fix lint build failed
tyroneyeh Jul 28, 2022
3af5672
Fix in edit upload file error
tyroneyeh Aug 1, 2022
ae14b46
Fix paste upload file in one line issue
tyroneyeh Aug 1, 2022
786411b
Merge branch 'main' into 20130_add_paste_image_legend_new
tyroneyeh Aug 4, 2022
c2a587c
Refactor again and add upload placeholder
tyroneyeh Aug 4, 2022
795b75d
Fix normal file link issue
tyroneyeh Aug 4, 2022
9a7496b
Merge branch 'main'
wxiaoguang Aug 5, 2022
fa958fe
Fix in repo upload file with js error
tyroneyeh Aug 8, 2022
5fde703
Fix repo upload file again
tyroneyeh Aug 8, 2022
649e2c6
Fix in repos upload file js error
tyroneyeh Sep 5, 2022
4c3d432
Merge branch 'main' into 20130_add_paste_image_legend_new
tyroneyeh Sep 5, 2022
ee64beb
Merge branch 'main' into 20130_add_paste_image_legend_new
tyroneyeh Sep 14, 2022
3aeb946
Fix lint check issue
tyroneyeh Sep 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions web_src/js/features/common-global.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import createDropzone from './dropzone.js';
import {initCompColorPicker} from './comp/ColorPicker.js';
import {showGlobalErrorMessage} from '../bootstrap.js';
import {attachDropdownAria} from './aria.js';
import {addUploadedFileToEditor, removeUploadedFileFromEditor} from './comp/ImagePaste.js';
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js';
import {getAttachedEasyMDE} from './comp/EasyMDE.js';

const {appUrl, csrfToken} = window.config;

Expand Down Expand Up @@ -53,20 +56,6 @@ export function initGlobalEnterQuickSubmit() {
});
}

export function handleGlobalEnterQuickSubmit(target) {
const $target = $(target);
const $form = $(target).closest('form');
if ($form.length) {
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
$form.trigger('submit');
} else {
// if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request.
// the 'ce-' prefix means this is a CustomEvent
$target.trigger('ce-quick-submit');
}
}

export function initGlobalButtonClickOnEnter() {
$(document).on('keypress', '.ui.button', (e) => {
if (e.keyCode === 13 || e.keyCode === 32) { // enter key or space bar
Expand Down Expand Up @@ -201,13 +190,19 @@ export function initGlobalDropzone() {
file.uuid = data.uuid;
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
$dropzone.find('.files').append(input);
addUploadedFileToEditor(file.editor, file);
});
this.on('removedfile', (file) => {
$(`#${file.uuid}`).remove();
if (!file.editor && (file.editor = getAttachedEasyMDE(this.element.closest('form').querySelector('textarea')))) {
file.editor = file.editor.codemirror;
}
if ($dropzone.data('remove-url')) {
$.post($dropzone.data('remove-url'), {
file: file.uuid,
_csrf: csrfToken,
}).always(() => {
removeUploadedFileFromEditor(file.editor, file.uuid);
});
}
});
Expand Down
2 changes: 1 addition & 1 deletion web_src/js/features/comp/EasyMDE.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import $ from 'jquery';
import attachTribute from '../tribute.js';
import {handleGlobalEnterQuickSubmit} from '../common-global.js';
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';

/**
* @returns {EasyMDE}
Expand Down
153 changes: 64 additions & 89 deletions web_src/js/features/comp/ImagePaste.js
Original file line number Diff line number Diff line change
@@ -1,94 +1,72 @@
import $ from 'jquery';

const {csrfToken} = window.config;

async function uploadFile(file, uploadUrl) {
const formData = new FormData();
formData.append('file', file, file.name);

const res = await fetch(uploadUrl, {
method: 'POST',
headers: {'X-Csrf-Token': csrfToken},
body: formData,
});
return await res.json();
}

function clipboardPastedImages(e) {
if (!e.clipboardData) return [];

const files = [];
for (const item of e.clipboardData.items || []) {
if (!item.type || !item.type.startsWith('image/')) continue;
files.push(item.getAsFile());
}
return files;
}

class TextareaEditor {
constructor(editor) {
this.editor = editor;
}

insertPlaceholder(value) {
const editor = this.editor;
const startPos = editor.selectionStart;
const endPos = editor.selectionEnd;
editor.value = editor.value.substring(0, startPos) + value + editor.value.substring(endPos);
editor.selectionStart = startPos;
editor.selectionEnd = startPos + value.length;
editor.focus();
import {getAttachedEasyMDE} from './EasyMDE.js';

/**
*
* @param {*} editor
* @param {*} file
*/
export function addUploadedFileToEditor(editor, file) {
if (!editor && file.previewElement) {
editor = file.previewElement.closest('form') ? getAttachedEasyMDE(file.previewElement.closest('form').querySelector('textarea')) : getAttachedEasyMDE(file.previewElement.parentElement.parentElement.parentElement.querySelector('textarea'));
tyroneyeh marked this conversation as resolved.
Show resolved Hide resolved
editor = editor.codemirror;
}

replacePlaceholder(oldVal, newVal) {
const editor = this.editor;
const startPos = editor.selectionStart;
const endPos = editor.selectionEnd;
if (editor.value.substring(startPos, endPos) === oldVal) {
editor.value = editor.value.substring(0, startPos) + newVal + editor.value.substring(endPos);
editor.selectionEnd = startPos + newVal.length;
const startPos = editor.selectionStart || editor.getCursor && editor.getCursor('start');
const endPos = editor.selectionEnd || editor.getCursor && editor.getCursor('end');
const isimage = file.type.startsWith('image/') ? '!' : '';
const fileName = (isimage ? file.name.replace(/\.[^/.]+$/, '') : file.name);
if (startPos) {
if (editor.setSelection) {
if (startPos.line !== undefined) {
editor.setSelection(startPos, endPos);
editor.replaceSelection(`${isimage}[${fileName}](/attachments/${file.uuid})\n`);
} else {
const val = editor.getValue();
editor.setValue(`${val}\n${isimage}[${fileName}](/attachments/${file.uuid})`);
}
} else if (typeof startPos === 'number' && startPos > 0) {
editor.value = `${editor.value.substring(0, startPos)}\n${isimage}[${fileName}](/attachments/${file.uuid})\n${editor.value.substring(endPos)}`;
} else {
editor.value = editor.value.replace(oldVal, newVal);
editor.selectionEnd -= oldVal.length;
editor.selectionEnd += newVal.length;
editor.value += `\n${isimage}[${fileName}](/attachments/${file.uuid})`;
}
editor.selectionStart = editor.selectionEnd;
editor.focus();
} else if (editor.setSelection) {
editor.value(`${editor.value()}\n${isimage}[${fileName}](/attachments/${file.uuid})\n`);
} else {
editor.value += `${editor.value}\n${isimage}[${fileName}](/attachments/${file.uuid})\n`;
wxiaoguang marked this conversation as resolved.
Show resolved Hide resolved
}
}

class CodeMirrorEditor {
constructor(editor) {
this.editor = editor;
/**
* @param editor{EasyMDE}
* @param fileUuid
*/
export function removeUploadedFileFromEditor(editor, fileUuid) {
// the raw regexp is: /!\[[^\]]*]\(\/attachments\/{uuid}\)/
const re = new RegExp(`(!|)\\[[^\\]]*]\\(/attachments/${fileUuid}\\)`);
tyroneyeh marked this conversation as resolved.
Show resolved Hide resolved
if (editor.setValue) {
editor.setValue(editor.getValue().replace(re, '')); // at the moment, we assume the editor is an EasyMDE
} else {
editor.value = editor.value.replace(re, '');
}
}

insertPlaceholder(value) {
const editor = this.editor;
const startPoint = editor.getCursor('start');
const endPoint = editor.getCursor('end');
editor.replaceSelection(value);
endPoint.ch = startPoint.ch + value.length;
editor.setSelection(startPoint, endPoint);
editor.focus();
}
function clipboardPastedImages(e) {
const data = e.clipboardData || e.dataTransfer;
if (!data) return [];

replacePlaceholder(oldVal, newVal) {
const editor = this.editor;
const endPoint = editor.getCursor('end');
if (editor.getSelection() === oldVal) {
editor.replaceSelection(newVal);
} else {
editor.setValue(editor.getValue().replace(oldVal, newVal));
}
endPoint.ch -= oldVal.length;
endPoint.ch += newVal.length;
editor.setSelection(endPoint, endPoint);
editor.focus();
const files = [];
const datafiles = e.clipboardData?.items || e.dataTransfer?.files;
for (const item of datafiles || []) {
const file = e.clipboardData ? item.getAsFile() : item;
if (file === null || !item.type) continue;
files.push(file);
}
return files;
}


export function initEasyMDEImagePaste(easyMDE, $dropzone) {
if ($dropzone.length !== 1) throw new Error('invalid dropzone binding for editor');

const uploadUrl = $dropzone.attr('data-upload-url');
const $files = $dropzone.find('.files');

Expand All @@ -103,23 +81,20 @@ export function initEasyMDEImagePaste(easyMDE, $dropzone) {
e.stopPropagation();

for (const img of pastedImages) {
const name = img.name.slice(0, img.name.lastIndexOf('.'));

const placeholder = `![${name}](uploading ...)`;
editor.insertPlaceholder(placeholder);
const data = await uploadFile(img, uploadUrl);
editor.replacePlaceholder(placeholder, `![${name}](/attachments/${data.uuid})`);

const $input = $(`<input name="files" type="hidden">`).attr('id', data.uuid).val(data.uuid);
$files.append($input);
img.editor = editor;
$dropzone[0].dropzone.addFile(img);
}
};

easyMDE.codemirror.on('paste', async (_, e) => {
return uploadClipboardImage(new CodeMirrorEditor(easyMDE.codemirror), e);
return uploadClipboardImage(easyMDE.codemirror, e);
});

easyMDE.codemirror.on('drop', async (_, e) => {
return uploadClipboardImage(easyMDE.codemirror, e);
});

$(easyMDE.element).on('paste', async (e) => {
return uploadClipboardImage(new TextareaEditor(easyMDE.element), e.originalEvent);
$(easyMDE.element).on('paste drop', async (e) => {
return uploadClipboardImage(easyMDE.element, e.originalEvent);
});
}
15 changes: 15 additions & 0 deletions web_src/js/features/comp/QuickSubmit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import $ from 'jquery';

export function handleGlobalEnterQuickSubmit(target) {
const $target = $(target);
const $form = $(target).closest('form');
if ($form.length) {
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
$form.trigger('submit');
} else {
// if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request.
// the 'ce-' prefix means this is a CustomEvent
$target.trigger('ce-quick-submit');
}
}
3 changes: 1 addition & 2 deletions web_src/js/features/repo-issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -480,8 +480,7 @@ export function initRepoPullRequestReview() {
// the editor's height is too large in some cases, and the panel cannot be scrolled with page now because there is `.repository .diff-detail-box.sticky { position: sticky; }`
// the temporary solution is to make the editor's height smaller (about 4 lines). GitHub also only show 4 lines for default. We can improve the UI (including Dropzone area) in future
// EasyMDE's options can not handle minHeight & maxHeight together correctly, we have to set max-height for .CodeMirror-scroll in CSS.
const $reviewTextarea = $reviewBox.find('textarea');
const easyMDE = await createCommentEasyMDE($reviewTextarea, {minHeight: '80px'});
const easyMDE = await createCommentEasyMDE($reviewBox.find('textarea'), {minHeight: '80px'});
initEasyMDEImagePaste(easyMDE, $reviewBox.find('.dropzone'));
})();
}
Expand Down
33 changes: 24 additions & 9 deletions web_src/js/features/repo-legacy.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import $ from 'jquery';
import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.js';
import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js';
import {initEasyMDEImagePaste} from './comp/ImagePaste.js';
import {initEasyMDEImagePaste, addUploadedFileToEditor, removeUploadedFileFromEditor} from './comp/ImagePaste.js';
import {
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel,
initRepoIssueCommentDelete,
Expand Down Expand Up @@ -33,7 +33,7 @@ import initRepoPullRequestMergeForm from './repo-issue-pr-form.js';
const {csrfToken} = window.config;

export function initRepoCommentForm() {
const $commentForm = $('.comment.form');
const $commentForm = $('#comment-form, #new-issue'); // for issues and PRs
if ($commentForm.length === 0) {
return;
}
Expand Down Expand Up @@ -68,8 +68,7 @@ export function initRepoCommentForm() {
}

(async () => {
const $textarea = $commentForm.find('textarea:not(.review-textarea)');
const easyMDE = await createCommentEasyMDE($textarea);
const easyMDE = await createCommentEasyMDE($commentForm.find('textarea:not(.review-textarea)'));
initEasyMDEImagePaste(easyMDE, $commentForm.find('.dropzone'));
})();

Expand Down Expand Up @@ -284,7 +283,8 @@ async function onEditContent(event) {
if ($dropzone.length === 1) {
$dropzone.data('saved', false);

const fileUuidDict = {};
let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the removedfile event
let fileUuidDict = {}; // if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
dz = await createDropzone($dropzone[0], {
url: $dropzone.data('upload-url'),
headers: {'X-Csrf-Token': csrfToken},
Expand All @@ -303,17 +303,27 @@ async function onEditContent(event) {
init() {
this.on('success', (file, data) => {
file.uuid = data.uuid;
fileUuidDict[file.uuid] = {submitted: false};
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
const input = $(`<input id="${file.uuid}" name="files" type="hidden">`).val(data.uuid);
$dropzone.find('.files').append(input);
fileUuidDict[file.uuid] = {submitted: false};
addUploadedFileToEditor(file.editor, file);
});
this.on('removedfile', (file) => {
if (disableRemovedfileEvent) return;
$(`#${file.uuid}`).remove();
if ($dropzone.data('remove-url') && !fileUuidDict[file.uuid].submitted) {
if (!file.editor && (file.editor = getAttachedEasyMDE(this.element.parentElement.parentElement.querySelector('textarea')))) {
file.editor = file.editor.codemirror;
}
if ($dropzone.data('remove-url') && !fileUuidDict[file.uuid]?.submitted) {
$.post($dropzone.data('remove-url'), {
file: file.uuid,
_csrf: csrfToken,
}).always(() => {
removeUploadedFileFromEditor(file.editor, file.uuid);
});
} else {
// for saved comment's attachment's removal, only remove the link in the editor
removeUploadedFileFromEditor(file.editor, file.uuid);
}
});
this.on('submit', () => {
Expand All @@ -323,8 +333,11 @@ async function onEditContent(event) {
});
this.on('reload', () => {
$.getJSON($editContentZone.data('attachment-url'), (data) => {
disableRemovedfileEvent = true;
dz.removeAllFiles(true);
disableRemovedfileEvent = false;
$dropzone.find('.files').empty();
fileUuidDict = {};
$.each(data, function () {
const imgSrc = `${$dropzone.data('link-url')}/${this.uuid}`;
dz.emit('addedfile', this);
Expand Down Expand Up @@ -354,7 +367,9 @@ async function onEditContent(event) {
easyMDE = await createCommentEasyMDE($textarea);

initCompMarkupContentPreviewTab($editContentForm);
initEasyMDEImagePaste(easyMDE, $dropzone);
if ($dropzone.length) {
initEasyMDEImagePaste(easyMDE, $dropzone);
}

const $saveButton = $editContentZone.find('.save.button');
$textarea.on('ce-quick-submit', () => {
Expand Down