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); })(); }