From ce5745233c3491e95b51a586e110df2a82771130 Mon Sep 17 00:00:00 2001 From: Tyrone Yeh Date: Tue, 16 Apr 2024 11:28:30 +0800 Subject: [PATCH] Improve attachment upload methods --- web_src/js/features/common-global.js | 10 ++-- .../js/features/comp/ComboMarkdownEditor.js | 11 ++-- web_src/js/features/comp/Paste.js | 59 ++++++++++++------- web_src/js/features/repo-issue-edit.js | 24 +++----- web_src/js/features/repo-issue.js | 4 +- web_src/js/utils/dom.js | 21 +++++-- 6 files changed, 77 insertions(+), 52 deletions(-) diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index e7db9b2336..43b0329dba 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -5,12 +5,13 @@ import {createDropzone} from './dropzone.js'; import {showGlobalErrorMessage} from '../bootstrap.js'; import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.js'; import {svg} from '../svg.js'; -import {hideElem, showElem, toggleElem, initSubmitEventPolyfill, submitEventSubmitter} from '../utils/dom.js'; +import {hideElem, showElem, toggleElem, initSubmitEventPolyfill, submitEventSubmitter, getComboMarkdownEditor} from '../utils/dom.js'; import {htmlEscape} from 'escape-goat'; import {showTemporaryTooltip} from '../modules/tippy.js'; import {confirmModal} from './comp/ConfirmModal.js'; import {showErrorToast} from '../modules/toast.js'; import {request, POST, GET} from '../modules/fetch.js'; +import {removeLinksInTextarea} from './comp/ComboMarkdownEditor.js'; import '../htmx.js'; const {appUrl, appSubUrl, csrfToken, i18n} = window.config; @@ -249,12 +250,13 @@ export function initDropzone(el) { }); file.previewTemplate.append(copyLinkElement); }); - this.on('removedfile', (file) => { - $(`#${file.uuid}`).remove(); + this.on('removedfile', async (file) => { + document.getElementById(file.uuid)?.remove(); if ($dropzone.data('remove-url')) { - POST($dropzone.data('remove-url'), { + await POST($dropzone.data('remove-url'), { data: new URLSearchParams({file: file.uuid}), }); + removeLinksInTextarea(getComboMarkdownEditor(el.closest('form').querySelector('.combo-markdown-editor')), file); } }); this.on('error', function (file, message) { diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js index d3fab375a9..b336bf0f78 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.js +++ b/web_src/js/features/comp/ComboMarkdownEditor.js @@ -296,11 +296,6 @@ class ComboMarkdownEditor { } } -export function getComboMarkdownEditor(el) { - if (el instanceof $) el = el[0]; - return el?._giteaComboMarkdownEditor; -} - export async function initComboMarkdownEditor(container, options = {}) { if (container instanceof $) { if (container.length !== 1) { @@ -315,3 +310,9 @@ export async function initComboMarkdownEditor(container, options = {}) { await editor.init(); return editor; } + +export function removeLinksInTextarea(editor, file) { + const fileName = file.name.slice(0, file.name.lastIndexOf('.')); + const fileText = `\\[${fileName}\\]\\(/attachments/${file.uuid}\\)`; + editor.value(editor.value().replace(new RegExp(`${fileName}`, 'g'), '').replace(new RegExp(`\\!${fileText}`, 'g'), '').replace(new RegExp(fileText, 'g'), '')); +} diff --git a/web_src/js/features/comp/Paste.js b/web_src/js/features/comp/Paste.js index b26296d1fc..6a5c63af19 100644 --- a/web_src/js/features/comp/Paste.js +++ b/web_src/js/features/comp/Paste.js @@ -82,35 +82,48 @@ class CodeMirrorEditor { } } -async function handleClipboardImages(editor, dropzone, images, e) { +async function handleClipboardFiles(editor, dropzone, files, e) { const uploadUrl = dropzone.getAttribute('data-upload-url'); const filesContainer = dropzone.querySelector('.files'); - if (!dropzone || !uploadUrl || !filesContainer || !images.length) return; + if (!dropzone || !uploadUrl || !filesContainer || !files.length) return; e.preventDefault(); e.stopPropagation(); - for (const img of images) { - const name = img.name.slice(0, img.name.lastIndexOf('.')); + for (const file of files) { + if (!file) continue; + const name = file.name.slice(0, file.name.lastIndexOf('.')); const placeholder = `![${name}](uploading ...)`; editor.insertPlaceholder(placeholder); - const {uuid} = await uploadFile(img, uploadUrl); - const {width, dppx} = await imageInfo(img); + const {uuid} = await uploadFile(file, uploadUrl); + const {width, dppx} = await imageInfo(file); const url = `/attachments/${uuid}`; let text; - if (width > 0 && dppx > 1) { - // Scale down images from HiDPI monitors. This uses the tag because it's the only - // method to change image size in Markdown that is supported by all implementations. - text = `${htmlEscape(name)}`; + if (file.type?.startsWith('image/')) { + if (width > 0 && dppx > 1) { + // Scale down images from HiDPI monitors. This uses the tag because it's the only + // method to change image size in Markdown that is supported by all implementations. + text = `${htmlEscape(name)}`; + } else { + text = `![${name}](${url})`; + } } else { - text = `![${name}](${url})`; + text = `[${name}](${url})`; } editor.replacePlaceholder(placeholder, text); + file.uuid = uuid; + dropzone.dropzone.emit('addedfile', file); + if (/\.(jpg|jpeg|png|gif|bmp)$/i.test(file.name)) { + const imgSrc = `/attachments/${file.uuid}`; + dropzone.dropzone.emit('thumbnail', file, imgSrc); + dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%'; + } + dropzone.dropzone.emit('complete', file); const input = document.createElement('input'); input.setAttribute('name', 'files'); input.setAttribute('type', 'hidden'); @@ -134,21 +147,25 @@ function handleClipboardText(textarea, text, e) { } export function initEasyMDEPaste(easyMDE, dropzone) { - easyMDE.codemirror.on('paste', (_, e) => { - const {images} = getPastedContent(e); - if (images.length) { - handleClipboardImages(new CodeMirrorEditor(easyMDE.codemirror), dropzone, images, e); + const pasteFunc = (e) => { + const {files} = getPastedContent(e); + if (files.length) { + handleClipboardFiles(new CodeMirrorEditor(easyMDE.codemirror), dropzone, files, e); } - }); + }; + easyMDE.codemirror.on('paste', (_, e) => pasteFunc(e)); + easyMDE.codemirror.on('drop', (_, e) => pasteFunc(e)); } export function initTextareaPaste(textarea, dropzone) { - textarea.addEventListener('paste', (e) => { - const {images, text} = getPastedContent(e); - if (images.length) { - handleClipboardImages(new TextareaEditor(textarea), dropzone, images, e); + const pasteFunc = (e) => { + const {files, text} = getPastedContent(e); + if (files.length) { + handleClipboardFiles(new TextareaEditor(textarea), dropzone, files, e); } else if (text) { handleClipboardText(textarea, text, e); } - }); + }; + textarea.addEventListener('paste', (e) => pasteFunc(e)); + textarea.addEventListener('drop', (e) => pasteFunc(e)); } diff --git a/web_src/js/features/repo-issue-edit.js b/web_src/js/features/repo-issue-edit.js index 4c03325c7a..6be928ac86 100644 --- a/web_src/js/features/repo-issue-edit.js +++ b/web_src/js/features/repo-issue-edit.js @@ -1,9 +1,9 @@ import $ from 'jquery'; import {handleReply} from './repo-issue.js'; -import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; +import {initComboMarkdownEditor, removeLinksInTextarea} from './comp/ComboMarkdownEditor.js'; import {createDropzone} from './dropzone.js'; import {GET, POST} from '../modules/fetch.js'; -import {hideElem, showElem} from '../utils/dom.js'; +import {hideElem, showElem, getComboMarkdownEditor} from '../utils/dom.js'; import {attachRefIssueContextPopup} from './contextpopup.js'; import {initCommentContent, initMarkupContent} from '../markup/content.js'; @@ -26,7 +26,6 @@ async function onEditContent(event) { if (!dropzone) return null; let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event - let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone const dz = await createDropzone(dropzone, { url: dropzone.getAttribute('data-upload-url'), headers: {'X-Csrf-Token': csrfToken}, @@ -45,7 +44,6 @@ async function onEditContent(event) { init() { this.on('success', (file, data) => { file.uuid = data.uuid; - fileUuidDict[file.uuid] = {submitted: false}; const input = document.createElement('input'); input.id = data.uuid; input.name = 'files'; @@ -56,19 +54,15 @@ async function onEditContent(event) { this.on('removedfile', async (file) => { document.getElementById(file.uuid)?.remove(); if (disableRemovedfileEvent) return; - if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) { + if (dropzone.getAttribute('data-remove-url')) { try { await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})}); + removeLinksInTextarea(getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')), file); } catch (error) { console.error(error); } } }); - this.on('submit', () => { - for (const fileUuid of Object.keys(fileUuidDict)) { - fileUuidDict[fileUuid].submitted = true; - } - }); this.on('reload', async () => { try { const response = await GET(editContentZone.getAttribute('data-attachment-url')); @@ -78,16 +72,16 @@ async function onEditContent(event) { dz.removeAllFiles(true); dropzone.querySelector('.files').innerHTML = ''; for (const el of dropzone.querySelectorAll('.dz-preview')) el.remove(); - fileUuidDict = {}; disableRemovedfileEvent = false; for (const attachment of data) { - const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`; dz.emit('addedfile', attachment); - dz.emit('thumbnail', attachment, imgSrc); + if (/\.(jpg|jpeg|png|gif|bmp)$/i.test(attachment.name)) { + const imgSrc = `${dropzone.getAttribute('data-link-url')}/${attachment.uuid}`; + dz.emit('thumbnail', attachment, imgSrc); + dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%'; + } dz.emit('complete', attachment); - fileUuidDict[attachment.uuid] = {submitted: true}; - dropzone.querySelector(`img[src='${imgSrc}']`).style.maxWidth = '100%'; const input = document.createElement('input'); input.id = attachment.uuid; input.name = 'files'; diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js index 2b2eed58bb..a98c1a73fc 100644 --- a/web_src/js/features/repo-issue.js +++ b/web_src/js/features/repo-issue.js @@ -1,9 +1,9 @@ import $ from 'jquery'; import {htmlEscape} from 'escape-goat'; import {showTemporaryTooltip, createTippy} from '../modules/tippy.js'; -import {hideElem, showElem, toggleElem} from '../utils/dom.js'; +import {hideElem, showElem, toggleElem, getComboMarkdownEditor} from '../utils/dom.js'; import {setFileFolding} from './file-fold.js'; -import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; +import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; import {toAbsoluteUrl} from '../utils.js'; import {initDropzone} from './common-global.js'; import {POST, GET} from '../modules/fetch.js'; diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js index fb23a71725..d9fe774580 100644 --- a/web_src/js/utils/dom.js +++ b/web_src/js/utils/dom.js @@ -258,16 +258,27 @@ export function isElemVisible(element) { return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length); } +export function getComboMarkdownEditor(el) { + if (el.jquery) el = el[0]; + return el?._giteaComboMarkdownEditor; +} + // extract text and images from "paste" event export function getPastedContent(e) { - const images = []; - for (const item of e.clipboardData?.items ?? []) { - if (item.type?.startsWith('image/')) { - images.push(item.getAsFile()); + const acceptedFiles = getComboMarkdownEditor(e.currentTarget).dropzone.getAttribute('data-accepts'); + const files = []; + const data = e.clipboardData?.items || e.dataTransfer?.items; + for (const item of data ?? []) { + if (!item.type?.startsWith('text/')) { + const file = item.getAsFile(); + const extName = file.name.slice(file.name.lastIndexOf('.'), file.name.length); + if (acceptedFiles.includes(extName)) { + files.push(file); + } } } const text = e.clipboardData?.getData?.('text') ?? ''; - return {text, images}; + return {text, files}; } // replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this