Skip to content

Commit 26e8c8c

Browse files
committed
merge duplicate code
1 parent 1e730ea commit 26e8c8c

File tree

6 files changed

+64
-59
lines changed

6 files changed

+64
-59
lines changed

web_src/js/features/comp/EditorUpload.js

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
1-
import {htmlEscape} from 'escape-goat';
21
import {imageInfo} from '../../utils/image.js';
32
import {replaceTextareaSelection} from '../../utils/dom.js';
43
import {isUrl} from '../../utils/url.js';
5-
import {isWellKnownImageFilename} from '../../utils.js';
64
import {triggerEditorContentChanged} from './EditorMarkdown.js';
7-
import {DropzoneCustomEventRemovedFile} from '../dropzone.js';
5+
import {
6+
DropzoneCustomEventRemovedFile,
7+
DropzoneCustomEventUploadDone,
8+
generateMarkdownLinkForAttachment,
9+
} from '../dropzone.js';
810

911
let uploadIdCounter = 0;
1012

1113
function uploadFile(dropzoneEl, file) {
1214
return new Promise((resolve, _) => {
13-
file._giteaUploadId = uploadIdCounter++;
15+
const curUploadId = uploadIdCounter++;
16+
file._giteaUploadId = curUploadId;
1417
const dropzoneInst = dropzoneEl.dropzone;
15-
const onSuccess = (successFile, successResp) => {
16-
if (successFile._giteaUploadId === file._giteaUploadId) {
17-
resolve({uuid: successResp.uuid});
18+
const onUploadDone = ({file}) => {
19+
if (file._giteaUploadId === curUploadId) {
20+
dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone);
21+
resolve();
1822
}
19-
dropzoneInst.off('success', onSuccess);
2023
};
21-
// TODO: handle errors (or maybe not needed at the moment)
22-
dropzoneInst.on('success', onSuccess);
24+
dropzoneInst.on(DropzoneCustomEventUploadDone, onUploadDone);
2325
dropzoneInst.handleFiles([file]);
2426
});
2527
}
@@ -90,42 +92,16 @@ class CodeMirrorEditor {
9092
}
9193
}
9294

93-
function isImageFile(file) {
94-
return file.type?.startsWith('image/') || isWellKnownImageFilename(file.name);
95-
}
96-
9795
async function handleUploadFiles(editor, dropzoneEl, files, e) {
9896
e.preventDefault();
9997
for (const file of files) {
10098
const name = file.name.slice(0, file.name.lastIndexOf('.'));
101-
const isImage = isImageFile(file);
102-
103-
let placeholder = `[${name}](uploading ...)`;
104-
if (isImage) placeholder = `!${placeholder}`;
99+
const {width, dppx} = await imageInfo(file);
100+
const placeholder = `[${name}](uploading ...)`;
105101

106102
editor.insertPlaceholder(placeholder);
107-
const {uuid} = await uploadFile(dropzoneEl, file);
108-
109-
let fileMarkdownLink;
110-
if (isImage) {
111-
const {width, dppx} = await imageInfo(file);
112-
if (width > 0 && dppx > 1) {
113-
// Scale down images from HiDPI monitors. This uses the <img> tag because it's the only
114-
// method to change image size in Markdown that is supported by all implementations.
115-
// Make the image link relative to the repo path, then the final URL is "/sub-path/owner/repo/attachments/{uuid}"
116-
const url = `attachments/${uuid}`;
117-
fileMarkdownLink = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(name)}" src="${htmlEscape(url)}">`;
118-
} else {
119-
// Markdown always renders the image with a relative path, so the final URL is "/sub-path/owner/repo/attachments/{uuid}"
120-
// TODO: it should also use relative path for consistency, because absolute is ambiguous for "/sub-path/attachments" or "/attachments"
121-
const url = `/attachments/${uuid}`;
122-
fileMarkdownLink = `![${name}](${url})`;
123-
}
124-
} else {
125-
const url = `/attachments/${uuid}`;
126-
fileMarkdownLink = `[${name}](${url})`;
127-
}
128-
editor.replacePlaceholder(placeholder, fileMarkdownLink);
103+
await uploadFile(dropzoneEl, file); // the "file" will get its "uuid" during the upload
104+
editor.replacePlaceholder(placeholder, generateMarkdownLinkForAttachment(file, {width, dppx}));
129105
}
130106
}
131107

web_src/js/features/dropzone.js

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import {showTemporaryTooltip} from '../modules/tippy.js';
55
import {GET, POST} from '../modules/fetch.js';
66
import {showErrorToast} from '../modules/toast.js';
77
import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.js';
8+
import {isImageFile, isVideoFile} from '../utils.js';
89

910
const {csrfToken, i18n} = window.config;
1011

1112
// dropzone has its owner event dispatcher (emitter)
1213
export const DropzoneCustomEventReloadFiles = 'dropzone-custom-reload-files';
1314
export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file';
15+
export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done';
1416

1517
async function createDropzone(el, opts) {
1618
const [{Dropzone}] = await Promise.all([
@@ -20,6 +22,26 @@ async function createDropzone(el, opts) {
2022
return new Dropzone(el, opts);
2123
}
2224

25+
export function generateMarkdownLinkForAttachment(file, {width, dppx} = {}) {
26+
let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
27+
if (isImageFile(file)) {
28+
fileMarkdown = `!${fileMarkdown}`;
29+
if (width > 0 && dppx > 1) {
30+
// Scale down images from HiDPI monitors. This uses the <img> tag because it's the only
31+
// method to change image size in Markdown that is supported by all implementations.
32+
// Make the image link relative to the repo path, then the final URL is "/sub-path/owner/repo/attachments/{uuid}"
33+
fileMarkdown = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(file.name)}" src="attachments/${htmlEscape(file.uuid)}">`;
34+
} else {
35+
// Markdown always renders the image with a relative path, so the final URL is "/sub-path/owner/repo/attachments/{uuid}"
36+
// TODO: it should also use relative path for consistency, because absolute is ambiguous for "/sub-path/attachments" or "/attachments"
37+
fileMarkdown = `![${file.name}](/attachments/${file.uuid})`;
38+
}
39+
} else if (isVideoFile(file)) {
40+
fileMarkdown = `<video src="attachments/${htmlEscape(file.uuid)}" title="${htmlEscape(file.name)}" controls></video>`;
41+
}
42+
return fileMarkdown;
43+
}
44+
2345
function addCopyLink(file) {
2446
// Create a "Copy Link" element, to conveniently copy the image or file link as Markdown to the clipboard
2547
// The "<a>" element has a hardcoded cursor: pointer because the default is overridden by .dropzone
@@ -29,13 +51,7 @@ function addCopyLink(file) {
2951
</div>`);
3052
copyLinkEl.addEventListener('click', async (e) => {
3153
e.preventDefault();
32-
let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
33-
if (file.type?.startsWith('image/')) {
34-
fileMarkdown = `!${fileMarkdown}`;
35-
} else if (file.type?.startsWith('video/')) {
36-
fileMarkdown = `<video src="/attachments/${htmlEscape(file.uuid)}" title="${htmlEscape(file.name)}" controls></video>`;
37-
}
38-
const success = await clippie(fileMarkdown);
54+
const success = await clippie(generateMarkdownLinkForAttachment(file));
3955
showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error);
4056
});
4157
file.previewTemplate.append(copyLinkEl);
@@ -78,6 +94,7 @@ export async function initDropzone(dropzoneEl) {
7894
const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${resp.uuid}`, value: resp.uuid});
7995
dropzoneEl.querySelector('.files').append(input);
8096
addCopyLink(file);
97+
dzInst.emit(DropzoneCustomEventUploadDone, {file});
8198
});
8299

83100
dzInst.on('removedfile', async (file) => {
@@ -114,7 +131,7 @@ export async function initDropzone(dropzoneEl) {
114131
dzInst.emit('addedfile', attachment);
115132
dzInst.emit('thumbnail', attachment, imgSrc);
116133
dzInst.emit('complete', attachment);
117-
addCopyLink(attachment);
134+
addCopyLink(attachment); // it is from server response, so no "type"
118135
fileUuidDict[attachment.uuid] = {submitted: true};
119136
const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${attachment.uuid}`, value: attachment.uuid});
120137
dropzoneEl.querySelector('.files').append(input);

web_src/js/utils.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ export function serializeXml(node) {
145145

146146
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
147147

148-
export function isWellKnownImageFilename(fn) {
149-
return /\.(jpe?g|png|gif|webp|svg|heic)$/i.test(fn);
148+
export function isImageFile({name, type}) {
149+
return /\.(jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/');
150+
}
151+
152+
export function isVideoFile({name, type}) {
153+
return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/');
150154
}

web_src/js/utils.test.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
basename, extname, isObject, stripTags, parseIssueHref,
33
parseUrl, translateMonth, translateDay, blobToDataURI,
4-
toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isWellKnownImageFilename,
4+
toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile,
55
} from './utils.js';
66

77
test('basename', () => {
@@ -114,11 +114,17 @@ test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => {
114114
expect(Array.from(decodeURLEncodedBase64('YQ=='))).toEqual(Array.from(uint8array('a')));
115115
});
116116

117-
test('isWellKnownImageFilename', () => {
118-
for (const filename of ['a.jpg', '/a.jpeg', '.file.png', '.webp', 'file.svg']) {
119-
expect(isWellKnownImageFilename(filename)).toBeTruthy();
117+
test('file detection', () => {
118+
for (const name of ['a.jpg', '/a.jpeg', '.file.png', '.webp', 'file.svg']) {
119+
expect(isImageFile({name})).toBeTruthy();
120120
}
121-
for (const filename of ['', 'a.jpg.x', '/path.png/x', 'webp']) {
122-
expect(isWellKnownImageFilename(filename)).toBeFalsy();
121+
for (const name of ['', 'a.jpg.x', '/path.png/x', 'webp']) {
122+
expect(isImageFile({name})).toBeFalsy();
123+
}
124+
for (const name of ['a.mpg', '/a.mpeg', '.file.mp4', '.webm', 'file.mkv']) {
125+
expect(isVideoFile({name})).toBeTruthy();
126+
}
127+
for (const name of ['', 'a.mpg.x', '/path.mp4/x', 'webm']) {
128+
expect(isVideoFile({name})).toBeFalsy();
123129
}
124130
});

web_src/js/utils/image.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,10 @@ export async function pngChunks(blob) {
1919
return chunks;
2020
}
2121

22-
// decode a image and try to obtain width and dppx. If will never throw but instead
22+
// decode a image and try to obtain width and dppx. It will never throw but instead
2323
// return default values.
2424
export async function imageInfo(blob) {
25-
let width = 0; // 0 means no width could be determined
26-
let dppx = 1; // 1 dot per pixel for non-HiDPI screens
25+
let width = 0, dppx = 1; // dppx: 1 dot per pixel for non-HiDPI screens
2726

2827
if (blob.type === 'image/png') { // only png is supported currently
2928
try {
@@ -41,6 +40,8 @@ export async function imageInfo(blob) {
4140
}
4241
}
4342
} catch {}
43+
} else {
44+
return {}; // no image info for non-image files
4445
}
4546

4647
return {width, dppx};

web_src/js/utils/image.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ test('imageInfo', async () => {
2626
expect(await imageInfo(await dataUriToBlob(pngNoPhys))).toEqual({width: 1, dppx: 1});
2727
expect(await imageInfo(await dataUriToBlob(pngPhys))).toEqual({width: 2, dppx: 2});
2828
expect(await imageInfo(await dataUriToBlob(pngEmpty))).toEqual({width: 0, dppx: 1});
29+
expect(await imageInfo(await dataUriToBlob(`data:image/gif;base64,`))).toEqual({});
2930
});

0 commit comments

Comments
 (0)