From 6e663804085c4b542673edb110be80afbc8f3a5e Mon Sep 17 00:00:00 2001 From: Beowulf Date: Fri, 2 May 2025 00:57:43 +0200 Subject: [PATCH] fix(ui): Add pasted images to dropzone This adds pasted images to the dropzone to provide the same experience as when using the dropzone. This gives the possibility to preview and delete the image. Additionally it provides a copy button to copy the markdown code for inserting the image. Removed the old implementation in `repo-legacy.js` and generalized everything in `common-global.js` as common implementation. Replaced old jquery code with plain JS. Fixes #4588 --- modules/structs/attachment.go | 6 + routers/web/repo/issue.go | 8 +- services/convert/attachment.go | 13 +- services/convert/attachment_test.go | 56 +++++++ tests/e2e/issue-comment-dropzone.test.e2e.ts | 94 ++++++++++++ tests/integration/issue_test.go | 7 +- web_src/js/features/common-global.js | 147 ++++++++++++++----- web_src/js/features/comp/Paste.js | 16 +- web_src/js/features/repo-issue.js | 4 +- web_src/js/features/repo-legacy.js | 110 ++------------ 10 files changed, 303 insertions(+), 158 deletions(-) create mode 100644 services/convert/attachment_test.go create mode 100644 tests/e2e/issue-comment-dropzone.test.e2e.ts diff --git a/modules/structs/attachment.go b/modules/structs/attachment.go index 0a3d4140c2..746f618cf0 100644 --- a/modules/structs/attachment.go +++ b/modules/structs/attachment.go @@ -22,6 +22,12 @@ type Attachment struct { Type string `json:"type"` } +// WebAttachment the generic attachment with mime type +type WebAttachment struct { + *Attachment + MimeType string `json:"mime_type"` +} + // EditAttachmentOptions options for editing attachments // swagger:model type EditAttachmentOptions struct { diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index a34e3b7c78..a4f6f97a05 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -3593,9 +3593,9 @@ func GetIssueAttachments(ctx *context.Context) { if ctx.Written() { return } - attachments := make([]*api.Attachment, len(issue.Attachments)) + attachments := make([]*api.WebAttachment, len(issue.Attachments)) for i := 0; i < len(issue.Attachments); i++ { - attachments[i] = convert.ToAttachment(ctx.Repo.Repository, issue.Attachments[i]) + attachments[i] = convert.ToWebAttachment(ctx.Repo.Repository, issue.Attachments[i]) } ctx.JSON(http.StatusOK, attachments) } @@ -3628,13 +3628,13 @@ func GetCommentAttachments(ctx *context.Context) { return } - attachments := make([]*api.Attachment, 0) if err := comment.LoadAttachments(ctx); err != nil { ctx.ServerError("LoadAttachments", err) return } + attachments := make([]*api.WebAttachment, len(comment.Attachments)) for i := 0; i < len(comment.Attachments); i++ { - attachments = append(attachments, convert.ToAttachment(ctx.Repo.Repository, comment.Attachments[i])) + attachments[i] = convert.ToWebAttachment(ctx.Repo.Repository, comment.Attachments[i]) } ctx.JSON(http.StatusOK, attachments) } diff --git a/services/convert/attachment.go b/services/convert/attachment.go index 6617aac906..74ae7c509c 100644 --- a/services/convert/attachment.go +++ b/services/convert/attachment.go @@ -4,6 +4,9 @@ package convert import ( + "mime" + "path/filepath" + repo_model "forgejo.org/models/repo" api "forgejo.org/modules/structs" ) @@ -20,9 +23,13 @@ func APIAssetDownloadURL(repo *repo_model.Repository, attach *repo_model.Attachm return attach.DownloadURL() } -// ToAttachment converts models.Attachment to api.Attachment for API usage -func ToAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.Attachment { - return toAttachment(repo, a, WebAssetDownloadURL) +// ToWebAttachment converts models.Attachment to api.WebAttachment for API usage +func ToWebAttachment(repo *repo_model.Repository, a *repo_model.Attachment) *api.WebAttachment { + attachment := toAttachment(repo, a, WebAssetDownloadURL) + return &api.WebAttachment{ + Attachment: attachment, + MimeType: mime.TypeByExtension(filepath.Ext(attachment.Name)), + } } // ToAPIAttachment converts models.Attachment to api.Attachment for API usage diff --git a/services/convert/attachment_test.go b/services/convert/attachment_test.go new file mode 100644 index 0000000000..d7bf0c1ee7 --- /dev/null +++ b/services/convert/attachment_test.go @@ -0,0 +1,56 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package convert + +import ( + "fmt" + "testing" + "time" + + repo_model "forgejo.org/models/repo" + "forgejo.org/models/unittest" + "forgejo.org/modules/setting" + api "forgejo.org/modules/structs" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestToWebAttachment(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + headRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + attachment := &repo_model.Attachment{ + ID: 10, + UUID: "uuidxxx", + RepoID: 1, + IssueID: 1, + ReleaseID: 0, + UploaderID: 0, + CommentID: 0, + Name: "test.png", + DownloadCount: 90, + Size: 30, + NoAutoTime: false, + CreatedUnix: 9342, + CustomDownloadURL: "", + ExternalURL: "", + } + + webAttachment := ToWebAttachment(headRepo, attachment) + + assert.NotNil(t, webAttachment) + assert.Equal(t, &api.WebAttachment{ + Attachment: &api.Attachment{ + ID: 10, + Name: "test.png", + Created: time.Unix(9342, 0), + DownloadCount: 90, + Size: 30, + UUID: "uuidxxx", + DownloadURL: fmt.Sprintf("%sattachments/uuidxxx", setting.AppURL), + Type: "attachment", + }, + MimeType: "image/png", + }, webAttachment) +} diff --git a/tests/e2e/issue-comment-dropzone.test.e2e.ts b/tests/e2e/issue-comment-dropzone.test.e2e.ts new file mode 100644 index 0000000000..33ea2c9403 --- /dev/null +++ b/tests/e2e/issue-comment-dropzone.test.e2e.ts @@ -0,0 +1,94 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +// @watch start +// web_src/js/features/common-global.js +// web_src/js/features/comp/Paste.js +// web_src/js/features/repo-issue.js +// web_src/js/features/repo-legacy.js +// @watch end + +import {expect, type Locator, type Page, type TestInfo} from '@playwright/test'; +import {test, save_visual, dynamic_id} from './utils_e2e.ts'; + +test.use({user: 'user2'}); + +async function pasteImage(el: Locator) { + await el.evaluate(async (el) => { + const base64 = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAAAHklEQVQoU2MUk1P7z0AGYBzViDvURgMHT4oaQoEDAFuJEu2fuGfhAAAAAElFTkSuQmCC`; + // eslint-disable-next-line no-restricted-syntax + const response = await fetch(base64); + const blob = await response.blob(); + + el.focus(); + + let pasteEvent = new Event('paste', {bubbles: true, cancelable: true}); + pasteEvent = Object.assign(pasteEvent, { + clipboardData: { + items: [ + { + kind: 'file', + type: 'image/png', + getAsFile() { + return new File([blob], 'foo.png', {type: blob.type}); + }, + }, + ], + }, + }); + + el.dispatchEvent(pasteEvent); + }); +} + +async function assertCopy(page: Page, workerInfo: TestInfo, startWith: string) { + const project = workerInfo.project.name; + if (project === 'webkit' || project === 'Mobile Safari') return; + + const dropzone = page.locator('.dropzone'); + const preview = dropzone.locator('.dz-preview'); + const copyLink = preview.locator('.octicon-copy').locator('..'); + await copyLink.click(); + + const clipboardContent = await page.evaluate(() => navigator.clipboard.readText()); + expect(clipboardContent).toContain(startWith); +} + +test('Paste image in new comment', async ({page}, workerInfo) => { + await page.goto('/user2/repo1/issues/new'); + + await pasteImage(page.locator('.markdown-text-editor')); + + const dropzone = page.locator('.dropzone'); + await expect(dropzone.locator('.files')).toHaveCount(1); + const preview = dropzone.locator('.dz-preview'); + await expect(preview).toHaveCount(1); + await expect(preview.locator('.dz-filename')).toHaveText('foo.png'); + await expect(preview.locator('.octicon-copy')).toBeVisible(); + await assertCopy(page, workerInfo, '![foo]('); + + await save_visual(page); +}); + +test('Re-add images to dropzone on edit', async ({page}, workerInfo) => { + await page.goto('/user2/repo1/issues/new'); + + const issueTitle = dynamic_id(); + await page.locator('#issue_title').fill(issueTitle); + await pasteImage(page.locator('.markdown-text-editor')); + await page.getByRole('button', {name: 'Create issue'}).click(); + + await expect(page).toHaveURL(/\/user2\/repo1\/issues\/\d+$/); + await page.click('.comment-container .context-menu'); + await page.click('.comment-container .menu > .edit-content'); + + const dropzone = page.locator('.dropzone'); + await expect(dropzone.locator('.files').first()).toHaveCount(1); + const preview = dropzone.locator('.dz-preview'); + await expect(preview).toHaveCount(1); + await expect(preview.locator('.dz-filename')).toHaveText('foo.png'); + await expect(preview.locator('.octicon-copy')).toBeVisible(); + await assertCopy(page, workerInfo, '![foo]('); + + await save_visual(page); +}); diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index 120fe6534e..2b50be48a4 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -629,7 +629,12 @@ func TestIssueCommentAttachment(t *testing.T) { assert.NotEqual(t, 0, id) req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user2", "repo1", id)) - session.MakeRequest(t, req, http.StatusOK) + resp = session.MakeRequest(t, req, http.StatusOK) + var attachments []*api.WebAttachment + DecodeJSON(t, resp, &attachments) + assert.Len(t, attachments, 1) + assert.Equal(t, attachments[0].UUID, uuid) + assert.Equal(t, "image/png", attachments[0].MimeType) // Using the ID of a comment that does not belong to the repository must fail req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/comments/%d/attachments", "user5", "repo4", id)) diff --git a/web_src/js/features/common-global.js b/web_src/js/features/common-global.js index 7d553f9692..2102e995d8 100644 --- a/web_src/js/features/common-global.js +++ b/web_src/js/features/common-global.js @@ -208,59 +208,128 @@ export function initGlobalDropzone() { } } -export function initDropzone(el) { - const $dropzone = $(el); - const _promise = createDropzone(el, { - url: $dropzone.data('upload-url'), +export async function initDropzone(dropzoneEl, zone = undefined) { + if (!dropzoneEl) return; + + 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 initFilePreview = (file, data, isReload = false) => { + file.uuid = data.uuid; + fileUuidDict[file.uuid] = {submitted: isReload}; + const input = document.createElement('input'); + input.id = data.uuid; + input.name = 'files'; + input.type = 'hidden'; + input.value = data.uuid; + dropzoneEl.querySelector('.files').append(input); + + // Create a "Copy Link" element, to conveniently copy the image + // or file link as Markdown to the clipboard + const copyLinkElement = document.createElement('div'); + copyLinkElement.className = 'tw-text-center'; + // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone + copyLinkElement.innerHTML = `${svg('octicon-copy', 14, 'copy link')} Copy link`; + copyLinkElement.addEventListener('click', async (e) => { + e.preventDefault(); + const name = file.name.slice(0, file.name.lastIndexOf('.')); + let fileMarkdown = `[${name}](/attachments/${file.uuid})`; + if (file.type.startsWith('image/')) { + fileMarkdown = `!${fileMarkdown}`; + } else if (file.type.startsWith('video/')) { + fileMarkdown = ``; + } + const success = await clippie(fileMarkdown); + showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error); + }); + file.previewTemplate.append(copyLinkElement); + }; + const updateDropzoneState = () => { + if (dropzoneEl.querySelector('.dz-preview')) { + dropzoneEl.classList.add('dz-started'); + } else { + dropzoneEl.classList.remove('dz-started'); + } + }; + + const dz = await createDropzone(dropzoneEl, { + url: dropzoneEl.getAttribute('data-upload-url'), headers: {'X-Csrf-Token': csrfToken}, - maxFiles: $dropzone.data('max-file'), - maxFilesize: $dropzone.data('max-size'), - acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'), + maxFiles: dropzoneEl.getAttribute('data-max-file'), + maxFilesize: dropzoneEl.getAttribute('data-max-size'), + acceptedFiles: (['*/*', ''].includes(dropzoneEl.getAttribute('data-accepts')) ? null : dropzoneEl.getAttribute('data-accepts')), addRemoveLinks: true, - dictDefaultMessage: $dropzone.data('default-message'), - dictInvalidFileType: $dropzone.data('invalid-input-type'), - dictFileTooBig: $dropzone.data('file-too-big'), - dictRemoveFile: $dropzone.data('remove-file'), + dictDefaultMessage: dropzoneEl.getAttribute('data-default-message'), + dictInvalidFileType: dropzoneEl.getAttribute('data-invalid-input-type'), + dictFileTooBig: dropzoneEl.getAttribute('data-file-too-big'), + dictRemoveFile: dropzoneEl.getAttribute('data-remove-file'), timeout: 0, thumbnailMethod: 'contain', thumbnailWidth: 480, thumbnailHeight: 480, init() { - this.on('success', (file, data) => { - file.uuid = data.uuid; - const $input = $(``).val(data.uuid); - $dropzone.find('.files').append($input); - // Create a "Copy Link" element, to conveniently copy the image - // or file link as Markdown to the clipboard - const copyLinkElement = document.createElement('div'); - copyLinkElement.className = 'tw-text-center'; - // The a element has a hardcoded cursor: pointer because the default is overridden by .dropzone - copyLinkElement.innerHTML = `${svg('octicon-copy', 14, 'copy link')} Copy link`; - copyLinkElement.addEventListener('click', async (e) => { - e.preventDefault(); - let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`; - if (file.type.startsWith('image/')) { - fileMarkdown = `!${fileMarkdown}`; - } else if (file.type.startsWith('video/')) { - fileMarkdown = ``; + this.on('success', initFilePreview); + this.on('removedfile', async (file) => { + document.getElementById(file.uuid)?.remove(); + if (disableRemovedfileEvent) return; + if (dropzoneEl.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) { + try { + await POST(dropzoneEl.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})}); + } catch (error) { + console.error(error); } - const success = await clippie(fileMarkdown); - showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error); - }); - file.previewTemplate.append(copyLinkElement); - }); - this.on('removedfile', (file) => { - $(`#${file.uuid}`).remove(); - if ($dropzone.data('remove-url')) { - POST($dropzone.data('remove-url'), { - data: new URLSearchParams({file: file.uuid}), - }); } + updateDropzoneState(); }); this.on('error', function (file, message) { showErrorToast(message); this.removeFile(file); }); + this.on('reload', async () => { + if (!zone || !dz.removeAllFiles) return; + try { + const response = await GET(zone.getAttribute('data-attachment-url')); + const data = await response.json(); + // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server + disableRemovedfileEvent = true; + dz.removeAllFiles(true); + dropzoneEl.querySelector('.files').innerHTML = ''; + for (const element of dropzoneEl.querySelectorAll('.dz-preview')) element.remove(); + fileUuidDict = {}; + disableRemovedfileEvent = false; + + for (const attachment of data) { + attachment.type = attachment.mime_type; + dz.emit('addedfile', attachment); + dz.emit('complete', attachment); + if (attachment.type.startsWith('image/')) { + const imgSrc = `${dropzoneEl.getAttribute('data-link-url')}/${attachment.uuid}`; + dz.emit('thumbnail', attachment, imgSrc); + } + initFilePreview(attachment, {uuid: attachment.uuid}, true); + fileUuidDict[attachment.uuid] = {submitted: true}; + } + } catch (error) { + console.error(error); + } + updateDropzoneState(); + }); + this.on('create-thumbnail', (attachment, file) => { + if (attachment.type && /image.*/.test(attachment.type)) { + // When a new issue is created, a thumbnail cannot be fetch, so we need to create it locally. + // The implementation is took from the dropzone library (`dropzone.js` > `_processThumbnailQueue()`) + dz.createThumbnail( + file, + dz.options.thumbnailWidth, + dz.options.thumbnailHeight, + dz.options.thumbnailMethod, + true, + (dataUrl) => { + dz.emit('thumbnail', attachment, dataUrl); + }, + ); + } + }); }, }); } diff --git a/web_src/js/features/comp/Paste.js b/web_src/js/features/comp/Paste.js index 7e4ecbbeda..0fb6cf4615 100644 --- a/web_src/js/features/comp/Paste.js +++ b/web_src/js/features/comp/Paste.js @@ -82,9 +82,8 @@ class CodeMirrorEditor { async function handleClipboardImages(editor, dropzone, images, e) { const uploadUrl = dropzone.getAttribute('data-upload-url'); - const filesContainer = dropzone.querySelector('.files'); - if (!dropzone || !uploadUrl || !filesContainer || !images.length) return; + if (!dropzone || !uploadUrl || !images.length) return; e.preventDefault(); e.stopPropagation(); @@ -92,7 +91,7 @@ async function handleClipboardImages(editor, dropzone, images, e) { for (const img of images) { const name = img.name.slice(0, img.name.lastIndexOf('.')); - const placeholder = `![${name}](uploading ...)`; + const placeholder = `![${name}](uploading...)`; editor.insertPlaceholder(placeholder); const {uuid} = await uploadFile(img, uploadUrl); @@ -101,12 +100,11 @@ async function handleClipboardImages(editor, dropzone, images, e) { const text = `![${name}](${url})`; editor.replacePlaceholder(placeholder, text); - const input = document.createElement('input'); - input.setAttribute('name', 'files'); - input.setAttribute('type', 'hidden'); - input.setAttribute('id', uuid); - input.value = uuid; - filesContainer.append(input); + const attachment = {uuid, name: img.name, browser_download_url: url, size: img.size, type: img.type}; + dropzone.dropzone.emit('addedfile', attachment); + dropzone.dropzone.emit('create-thumbnail', attachment, img); + dropzone.dropzone.emit('complete', attachment); + dropzone.dropzone.emit('success', attachment, {uuid}); } } diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js index bf76453428..d678d9195b 100644 --- a/web_src/js/features/repo-issue.js +++ b/web_src/js/features/repo-issue.js @@ -445,7 +445,7 @@ export async function handleReply($el) { // When the page is loaded, the dropzone is initialized by initGlobalDropzone, but the editor is not initialized. // When the form is submitted and partially reload, none of them is initialized. const dropzone = $form.find('.dropzone')[0]; - if (!dropzone.dropzone) initDropzone(dropzone); + if (!dropzone.dropzone) await initDropzone(dropzone); editor = await initComboMarkdownEditor($form.find('.combo-markdown-editor')); } editor.focus(); @@ -580,7 +580,7 @@ export function initRepoPullRequestReview() { $td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed'); $td.find("input[name='path']").val(path); - initDropzone($td.find('.dropzone')[0]); + await initDropzone($td.find('.dropzone')[0]); const editor = await initComboMarkdownEditor($td.find('.combo-markdown-editor')); editor.focus(); } catch (error) { diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js index 25ed181616..e6af4cbf04 100644 --- a/web_src/js/features/repo-legacy.js +++ b/web_src/js/features/repo-legacy.js @@ -16,7 +16,6 @@ import { import {initCitationFileCopyContent} from './citation.js'; import {initCompLabelEdit} from './comp/LabelEdit.js'; import {initRepoDiffConversationNav} from './repo-diff.js'; -import {createDropzone} from './dropzone.js'; import {showErrorToast} from '../modules/toast.js'; import {initCommentContent, initMarkupContent} from '../markup/content.js'; import {initCompReactionSelector} from './comp/ReactionSelector.js'; @@ -26,12 +25,10 @@ import {initRepoPullRequestCommitStatus} from './repo-issue-pr-status.js'; import {hideElem, showElem} from '../utils/dom.js'; import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js'; import {attachRefIssueContextPopup} from './contextpopup.js'; -import {POST, GET} from '../modules/fetch.js'; +import {POST} from '../modules/fetch.js'; import {MarkdownQuote} from '@github/quote-selection'; import {toAbsoluteUrl} from '../utils.js'; -import {initGlobalShowModal} from './common-global.js'; - -const {csrfToken} = window.config; +import {initDropzone, initGlobalShowModal} from './common-global.js'; export function initRepoCommentForm() { const $commentForm = $('.comment.form'); @@ -312,115 +309,27 @@ async function onEditContent(event) { let comboMarkdownEditor; - /** - * @param {HTMLElement} dropzone - */ - const setupDropzone = async (dropzone) => { - 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}, - maxFiles: dropzone.getAttribute('data-max-file'), - maxFilesize: dropzone.getAttribute('data-max-size'), - acceptedFiles: ['*/*', ''].includes(dropzone.getAttribute('data-accepts')) ? null : dropzone.getAttribute('data-accepts'), - addRemoveLinks: true, - dictDefaultMessage: dropzone.getAttribute('data-default-message'), - dictInvalidFileType: dropzone.getAttribute('data-invalid-input-type'), - dictFileTooBig: dropzone.getAttribute('data-file-too-big'), - dictRemoveFile: dropzone.getAttribute('data-remove-file'), - timeout: 0, - thumbnailMethod: 'contain', - thumbnailWidth: 480, - thumbnailHeight: 480, - 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'; - input.type = 'hidden'; - input.value = data.uuid; - dropzone.querySelector('.files').append(input); - }); - this.on('removedfile', async (file) => { - document.getElementById(file.uuid)?.remove(); - if (disableRemovedfileEvent) return; - if (dropzone.getAttribute('data-remove-url') && !fileUuidDict[file.uuid].submitted) { - try { - await POST(dropzone.getAttribute('data-remove-url'), {data: new URLSearchParams({file: file.uuid})}); - } 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')); - const data = await response.json(); - // do not trigger the "removedfile" event, otherwise the attachments would be deleted from server - disableRemovedfileEvent = true; - 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); - 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'; - input.type = 'hidden'; - input.value = attachment.uuid; - dropzone.querySelector('.files').append(input); - } - if (!dropzone.querySelector('.dz-preview')) { - dropzone.classList.remove('dz-started'); - } - } catch (error) { - console.error(error); - } - }); - }, - }); - dz.emit('reload'); - return dz; - }; - const cancelAndReset = (e) => { e.preventDefault(); showElem(renderContent); hideElem(editContentZone); comboMarkdownEditor.value(rawContent.textContent); - comboMarkdownEditor.attachedDropzoneInst?.emit('reload'); + editContentZone.querySelector('.dropzone')?.dropzone?.emit('reload'); }; const saveAndRefresh = async (e) => { e.preventDefault(); showElem(renderContent); hideElem(editContentZone); - const dropzoneInst = comboMarkdownEditor.attachedDropzoneInst; + const dropzone = editContentZone.querySelector('.dropzone')?.dropzone; + for (const element of dropzone?.element?.querySelectorAll('.dz-preview') ?? []) element.classList.remove('dz-success'); try { const params = new URLSearchParams({ content: comboMarkdownEditor.value(), context: editContentZone.getAttribute('data-context'), content_version: editContentZone.getAttribute('data-content-version'), }); - const files = dropzoneInst?.element?.querySelectorAll('.files [name=files]') ?? []; + const files = dropzone?.element?.querySelectorAll('.files [name=files]') ?? []; for (const fileInput of files) { params.append('files[]', fileInput.value); } @@ -451,8 +360,7 @@ async function onEditContent(event) { } else { content.querySelector('.dropzone-attachments').outerHTML = data.attachments; } - dropzoneInst?.emit('submit'); - dropzoneInst?.emit('reload'); + dropzone?.emit('submit'); initMarkupContent(); initCommentContent(); } catch (error) { @@ -463,8 +371,10 @@ async function onEditContent(event) { comboMarkdownEditor = getComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); if (!comboMarkdownEditor) { editContentZone.innerHTML = document.getElementById('issue-comment-editor-template').innerHTML; + const dropzone = editContentZone.querySelector('.dropzone'); + if (!dropzone.dropzone) await initDropzone(dropzone, editContentZone); comboMarkdownEditor = await initComboMarkdownEditor(editContentZone.querySelector('.combo-markdown-editor')); - comboMarkdownEditor.attachedDropzoneInst = await setupDropzone(editContentZone.querySelector('.dropzone')); + dropzone.dropzone.emit('reload'); editContentZone.addEventListener('ce-quick-submit', saveAndRefresh); editContentZone.querySelector('button[data-button-name="cancel-edit"]').addEventListener('click', cancelAndReset); editContentZone.querySelector('button[data-button-name="save-edit"]').addEventListener('click', saveAndRefresh);