diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js
index a3aebc024625..98749f8445e4 100644
--- a/web_src/js/features/common-global.js
+++ b/web_src/js/features/common-global.js
@@ -5,7 +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';
import {initTooltip} from '../modules/tippy.js';
const {appUrl, csrfToken} = window.config;
@@ -163,17 +165,29 @@ export function initGlobalDropzone() {
thumbnailWidth: 480,
thumbnailHeight: 480,
init() {
+ this.on('addedfile', addUploadedFileToEditor);
this.on('success', (file, data) => {
file.uuid = data.uuid;
const input = $(``).val(data.uuid);
$dropzone.find('.files').append(input);
+ const name = file.name.slice(0, file.name.lastIndexOf('.'));
+ const placeholder = `![${name}](uploading ...)`;
+ const isImage = file.type.includes('image') ? '!' : '';
+ if (file.editor) {
+ file.editor.replacePlaceholder(placeholder, `${isImage}[${name}](/attachments/${data.uuid})`);
+ }
});
this.on('removedfile', (file) => {
$(`#${file.uuid}`).remove();
+ if (!file.editor && (file.editor = getAttachedEasyMDE(this.element.closest('div.comment').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);
});
}
});
diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js
index da41e7611a62..03f64fe0dbdf 100644
--- a/web_src/js/features/comp/ImagePaste.js
+++ b/web_src/js/features/comp/ImagePaste.js
@@ -1,30 +1,37 @@
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();
+import {getAttachedEasyMDE} from './EasyMDE.js';
+
+/**
+ * @param editor{EasyMDE}
+ * @param fileUuid
+ */
+export function removeUploadedFileFromEditor(editor, fileUuid) {
+ // the raw regexp is: /!\[[^\]]*]\(\/attachments\/{uuid}\)/ for remove file text in textarea
+ if (editor && editor.editor) {
+ const re = new RegExp(`(!|)\\[[^\\]]*]\\(/attachments/${fileUuid}\\)`);
+ if (editor.editor.setValue) {
+ editor.editor.setValue(editor.editor.getValue().replace(re, '')); // at the moment, we assume the editor is an EasyMDE
+ } else {
+ editor.editor.value = editor.editor.value.replace(re, '');
+ }
+ }
}
-function clipboardPastedImages(e) {
- if (!e.clipboardData) return [];
+function clipboardPastedFiles(e) {
+ const data = e.clipboardData || e.dataTransfer;
+ if (!data) return [];
const files = [];
- for (const item of e.clipboardData.items || []) {
- if (!item.type || !item.type.startsWith('image/')) continue;
- files.push(item.getAsFile());
+ 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;
}
+
class TextareaEditor {
constructor(editor) {
this.editor = editor;
@@ -87,15 +94,16 @@ class CodeMirrorEditor {
}
}
+export function initEasyMDEFilePaste(easyMDE, $dropzone) {
+ if ($dropzone.length !== 1) throw new Error('invalid dropzone binding for editor');
-export function initEasyMDEImagePaste(easyMDE, $dropzone) {
const uploadUrl = $dropzone.attr('data-upload-url');
const $files = $dropzone.find('.files');
if (!uploadUrl || !$files.length) return;
const uploadClipboardImage = async (editor, e) => {
- const pastedImages = clipboardPastedImages(e);
+ const pastedImages = clipboardPastedFiles(e);
if (!pastedImages || pastedImages.length === 0) {
return;
}
@@ -103,15 +111,8 @@ 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 = $(``).attr('id', data.uuid).val(data.uuid);
- $files.append($input);
+ img.editor = editor;
+ $dropzone[0].dropzone.addFile(img);
}
};
@@ -119,7 +120,32 @@ export function initEasyMDEImagePaste(easyMDE, $dropzone) {
return uploadClipboardImage(new CodeMirrorEditor(easyMDE.codemirror), e);
});
- $(easyMDE.element).on('paste', async (e) => {
+ easyMDE.codemirror.on('drop', async (_, e) => {
+ return uploadClipboardImage(new CodeMirrorEditor(easyMDE.codemirror), e);
+ });
+
+ $(easyMDE.element).on('paste drop', async (e) => {
return uploadClipboardImage(new TextareaEditor(easyMDE.element), e.originalEvent);
});
}
+
+export async function addUploadedFileToEditor(file) {
+ if (!file.editor) {
+ const form = file.previewElement.closest('div.comment');
+ if (form) {
+ const editor = getAttachedEasyMDE(form.querySelector('textarea'));
+ if (editor) {
+ if (editor.codemirror) {
+ file.editor = new CodeMirrorEditor(editor.codemirror);
+ } else {
+ file.editor = new TextareaEditor(editor);
+ }
+ }
+ if (file.editor) {
+ const name = file.name.slice(0, file.name.lastIndexOf('.'));
+ const placeholder = `![${name}](uploading ...)`;
+ file.editor.insertPlaceholder(placeholder);
+ }
+ }
+ }
+}
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 9dbe78edf51b..f65c2943eebe 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import {htmlEscape} from 'escape-goat';
import attachTribute from './tribute.js';
import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.js';
-import {initEasyMDEImagePaste} from './comp/ImagePaste.js';
+import {initEasyMDEFilePaste} from './comp/ImagePaste.js';
import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js';
import {initTooltip, showTemporaryTooltip} from '../modules/tippy.js';
@@ -475,9 +475,8 @@ 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'});
- initEasyMDEImagePaste(easyMDE, $reviewBox.find('.dropzone'));
+ const easyMDE = await createCommentEasyMDE($reviewBox.find('textarea'), {minHeight: '80px'});
+ initEasyMDEFilePaste(easyMDE, $reviewBox.find('.dropzone'));
})();
}
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 2c93ca03424b..722839066a04 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -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 {initEasyMDEFilePaste, addUploadedFileToEditor, removeUploadedFileFromEditor} from './comp/ImagePaste.js';
import {
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel,
initRepoIssueCommentDelete,
@@ -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;
}
@@ -74,7 +74,7 @@ export function initRepoCommentForm() {
continue;
}
const easyMDE = await createCommentEasyMDE(textarea);
- initEasyMDEImagePaste(easyMDE, $commentForm.find('.dropzone'));
+ initEasyMDEFilePaste(easyMDE, $commentForm.find('.dropzone'));
}
})();
@@ -289,7 +289,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},
@@ -306,19 +307,33 @@ async function onEditContent(event) {
thumbnailWidth: 480,
thumbnailHeight: 480,
init() {
+ this.on('addedfile', addUploadedFileToEditor);
this.on('success', (file, data) => {
file.uuid = data.uuid;
- fileUuidDict[file.uuid] = {submitted: false};
- const input = $(``).val(data.uuid);
+ const input = $(``).val(data.uuid);
$dropzone.find('.files').append(input);
+ fileUuidDict[file.uuid] = {submitted: false};
+ const name = file.name.slice(0, file.name.lastIndexOf('.'));
+ const placeholder = `![${name}](uploading ...)`;
+ const isImage = file.type.includes('image') ? '!' : '';
+ file.editor.replacePlaceholder(placeholder, `${isImage}[${name}](/attachments/${data.uuid})`);
});
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.closest('div.comment').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', () => {
@@ -328,8 +343,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);
@@ -359,7 +377,9 @@ async function onEditContent(event) {
easyMDE = await createCommentEasyMDE($textarea);
initCompMarkupContentPreviewTab($editContentForm);
- initEasyMDEImagePaste(easyMDE, $dropzone);
+ if ($dropzone.length) {
+ initEasyMDEFilePaste(easyMDE, $dropzone);
+ }
const $saveButton = $editContentZone.find('.save.button');
$textarea.on('ce-quick-submit', () => {
diff --git a/web_src/js/features/repo-release.js b/web_src/js/features/repo-release.js
index b68a7a6cd530..17a946a63bec 100644
--- a/web_src/js/features/repo-release.js
+++ b/web_src/js/features/repo-release.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import attachTribute from './tribute.js';
import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js';
-import {initEasyMDEImagePaste} from './comp/ImagePaste.js';
+import {initEasyMDEFilePaste} from './comp/ImagePaste.js';
import {createCommentEasyMDE} from './comp/EasyMDE.js';
export function initRepoRelease() {
@@ -26,6 +26,6 @@ export function initRepoReleaseEditor() {
const easyMDE = await createCommentEasyMDE($textarea);
initCompMarkupContentPreviewTab($editor);
const $dropzone = $editor.parent().find('.dropzone');
- initEasyMDEImagePaste(easyMDE, $dropzone);
+ initEasyMDEFilePaste(easyMDE, $dropzone);
})();
}