-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
24f4ebb
commit a5848f9
Showing
7 changed files
with
164 additions
and
183 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,80 +1,131 @@ | ||
import $ from 'jquery'; | ||
import {svg} from '../svg.js'; | ||
import {htmlEscape} from 'escape-goat'; | ||
import {clippie} from 'clippie'; | ||
import {showTemporaryTooltip} from '../modules/tippy.js'; | ||
import {POST} from '../modules/fetch.js'; | ||
import {GET, POST} from '../modules/fetch.js'; | ||
import {showErrorToast} from '../modules/toast.js'; | ||
import {createElementFromHTML, createElementFromObject} from '../utils/dom.js'; | ||
|
||
const {csrfToken, i18n} = window.config; | ||
|
||
export async function createDropzone(el, opts) { | ||
async function createDropzone(el, opts) { | ||
const [{Dropzone}] = await Promise.all([ | ||
import(/* webpackChunkName: "dropzone" */'dropzone'), | ||
import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'), | ||
]); | ||
return new Dropzone(el, opts); | ||
} | ||
|
||
export function initGlobalDropzone() { | ||
for (const el of document.querySelectorAll('.dropzone')) { | ||
initDropzone(el); | ||
} | ||
function addCopyLink(file) { | ||
// Create a "Copy Link" element, to conveniently copy the image or file link as Markdown to the clipboard | ||
// The "<a>" element has a hardcoded cursor: pointer because the default is overridden by .dropzone | ||
const copyLinkEl = createElementFromHTML(` | ||
<div class="tw-text-center"> | ||
<a href="#" class="tw-cursor-pointer">${svg('octicon-copy', 14)} Copy link</a> | ||
</div>`); | ||
copyLinkEl.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 = `<video src="/attachments/${htmlEscape(file.uuid)}" title="${htmlEscape(file.name)}" controls></video>`; | ||
} | ||
const success = await clippie(fileMarkdown); | ||
showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error); | ||
}); | ||
file.previewTemplate.append(copyLinkEl); | ||
} | ||
|
||
export function initDropzone(el) { | ||
const $dropzone = $(el); | ||
const _promise = createDropzone(el, { | ||
url: $dropzone.data('upload-url'), | ||
/** | ||
* @param {HTMLElement} dropzoneEl | ||
*/ | ||
export async function initDropzone(dropzoneEl) { | ||
if (!dropzoneEl) return null; | ||
|
||
const listAttachmentsUrl = dropzoneEl.closest('[data-attachment-url]')?.getAttribute('data-attachment-url'); | ||
const removeAttachmentUrl = dropzoneEl.getAttribute('data-remove-url'); | ||
const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url'); | ||
|
||
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 dzInst = 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 = $(`<input id="${data.uuid}" name="files" type="hidden">`).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 = `<a href="#" style="cursor: pointer;">${svg('octicon-copy', 14, 'copy link')} Copy link</a>`; | ||
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 = `<video src="/attachments/${file.uuid}" title="${htmlEscape(file.name)}" controls></video>`; | ||
} | ||
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}), | ||
}); | ||
} | ||
}); | ||
this.on('error', function (file, message) { | ||
showErrorToast(message); | ||
this.removeFile(file); | ||
}); | ||
}, | ||
}); | ||
|
||
dzInst.on('success', (file, data) => { | ||
file.uuid = data.uuid; | ||
fileUuidDict[file.uuid] = {submitted: false}; | ||
const input = createElementFromObject('input', {name: 'files', type: 'hidden', id: `dropzone-file-${data.uuid}`, value: data.uuid}); | ||
dropzoneEl.querySelector('.files').append(input); | ||
addCopyLink(file); | ||
}); | ||
|
||
dzInst.on('removedfile', async (file) => { | ||
if (disableRemovedfileEvent) return; | ||
document.querySelector(`#dropzone-file-${file.uuid}`)?.remove(); | ||
if (removeAttachmentUrl && !fileUuidDict[file.uuid].submitted) { | ||
await POST(removeAttachmentUrl, {data: new URLSearchParams({file: file.uuid})}); | ||
} | ||
}); | ||
|
||
dzInst.on('submit', () => { | ||
for (const fileUuid of Object.keys(fileUuidDict)) { | ||
fileUuidDict[fileUuid].submitted = true; | ||
} | ||
}); | ||
|
||
dzInst.on('reload', async () => { | ||
try { | ||
const resp = await GET(listAttachmentsUrl); | ||
const respData = await resp.json(); | ||
// do not trigger the "removedfile" event, otherwise the attachments would be deleted from server | ||
disableRemovedfileEvent = true; | ||
dzInst.removeAllFiles(true); | ||
disableRemovedfileEvent = false; | ||
|
||
dropzoneEl.querySelector('.files').innerHTML = ''; | ||
for (const el of dropzoneEl.querySelectorAll('.dz-preview')) el.remove(); | ||
fileUuidDict = {}; | ||
for (const attachment of respData) { | ||
const imgSrc = `${attachmentBaseLinkUrl}/${attachment.uuid}`; | ||
dzInst.emit('addedfile', attachment); | ||
dzInst.emit('thumbnail', attachment, imgSrc); | ||
dzInst.emit('complete', attachment); | ||
addCopyLink(attachment); | ||
fileUuidDict[attachment.uuid] = {submitted: true}; | ||
const input = createElementFromObject('input', {name: 'files', type: 'hidden', id: `dropzone-file-${attachment.uuid}`, value: attachment.uuid}); | ||
dropzoneEl.querySelector('.files').append(input); | ||
} | ||
if (!dropzoneEl.querySelector('.dz-preview')) { | ||
dropzoneEl.classList.remove('dz-started'); | ||
} | ||
} catch (error) { | ||
// TODO: if listing the existing attachments failed, it should stop from operating the content or attachments, | ||
// otherwise the attachments might be lost. | ||
showErrorToast(`Failed to load attachments: ${error}`); | ||
console.error(error); | ||
} | ||
}); | ||
|
||
dzInst.on('error', (file, message) => { | ||
showErrorToast(`Dropzone upload error: ${message}`); | ||
dzInst.removeFile(file); | ||
}); | ||
|
||
if (listAttachmentsUrl) dzInst.emit('reload'); | ||
return dzInst; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.