Skip to content

Commit b9bbd74

Browse files
committed
Merge remote-tracking branch 'giteaofficial/main'
* giteaofficial/main: Fix avatar radius problem on the new issue page (go-gitea#31506) Make toast support preventDuplicates (go-gitea#31501) Improve attachment upload methods (go-gitea#30513)
2 parents b71ae6f + d655ff1 commit b9bbd74

18 files changed

+258
-139
lines changed

templates/devtest/gitea-ui.tmpl

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -182,15 +182,6 @@
182182
</div>
183183
</div>
184184

185-
<div>
186-
<h1>Toast</h1>
187-
<div>
188-
<button class="ui button" id="info-toast">Show Info Toast</button>
189-
<button class="ui button" id="warning-toast">Show Warning Toast</button>
190-
<button class="ui button" id="error-toast">Show Error Toast</button>
191-
</div>
192-
</div>
193-
194185
<div>
195186
<h1>ComboMarkdownEditor</h1>
196187
<div>ps: no JS code attached, so just a layout</div>
@@ -201,7 +192,5 @@
201192
<div>
202193
<button class="{{if true}}tw-bg-red{{end}} tw-p-5 tw-border tw-rounded hover:tw-bg-blue active:tw-bg-yellow">Button</button>
203194
</div>
204-
205-
<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
206195
</div>
207196
{{template "base/footer" .}}

templates/devtest/toast.tmpl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{{template "base/head" .}}
2+
3+
<div>
4+
<h1>Toast</h1>
5+
<div>
6+
<button class="ui button toast-test-button" data-toast-level="info" data-toast-message="test info">Show Info Toast</button>
7+
<button class="ui button toast-test-button" data-toast-level="warning" data-toast-message="test warning">Show Warning Toast</button>
8+
<button class="ui button toast-test-button" data-toast-level="error" data-toast-message="test error">Show Error Toast</button>
9+
<button class="ui button toast-test-button" data-toast-level="error" data-toast-message="very looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message">Show Error Toast (long)</button>
10+
</div>
11+
</div>
12+
13+
<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
14+
15+
{{template "base/footer" .}}

web_src/css/modules/animations.css

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,22 @@ code.language-math.is-loading::after {
9292
}
9393
}
9494

95-
@keyframes pulse {
95+
/* 1p5 means 1-point-5. it can't use "pulse" here, otherwise the animation is not right (maybe due to some conflicts */
96+
@keyframes pulse-1p5 {
9697
0% {
9798
transform: scale(1);
9899
}
99100
50% {
100-
transform: scale(1.8);
101+
transform: scale(1.5);
101102
}
102103
100% {
103104
transform: scale(1);
104105
}
105106
}
106107

107-
.pulse {
108-
animation: pulse 2s linear;
108+
/* pulse animation for scale(1.5) in 200ms */
109+
.pulse-1p5-200 {
110+
animation: pulse-1p5 200ms linear;
109111
}
110112

111113
.ui.modal,

web_src/css/modules/toast.css

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,31 @@
2222
overflow-wrap: anywhere;
2323
}
2424

25-
.toast-close,
26-
.toast-icon {
27-
color: currentcolor;
25+
.toast-close {
2826
border-radius: var(--border-radius);
29-
background: transparent;
30-
border: none;
31-
display: flex;
3227
width: 30px;
3328
height: 30px;
3429
justify-content: center;
30+
}
31+
32+
.toast-icon {
33+
display: inline-flex;
34+
width: 30px;
35+
height: 30px;
3536
align-items: center;
37+
justify-content: center;
38+
}
39+
40+
.toast-duplicate-number::before {
41+
content: "(";
42+
}
43+
.toast-duplicate-number {
44+
display: inline-block;
45+
margin-right: 5px;
46+
user-select: none;
47+
}
48+
.toast-duplicate-number::after {
49+
content: ")";
3650
}
3751

3852
.toast-close:hover {

web_src/css/repo.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ td .commit-summary {
535535
min-width: 100px;
536536
}
537537

538-
#new-issue .avatar {
538+
#new-issue .comment .avatar {
539539
width: 3em;
540540
}
541541

web_src/js/features/comp/ComboMarkdownEditor.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@ import '@github/text-expander-element';
33
import $ from 'jquery';
44
import {attachTribute} from '../tribute.js';
55
import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.js';
6-
import {initEasyMDEPaste, initTextareaPaste} from './Paste.js';
6+
import {initEasyMDEPaste, initTextareaUpload} from './EditorUpload.js';
77
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
88
import {renderPreviewPanelContent} from '../repo-editor.js';
99
import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js';
1010
import {initTextExpander} from './TextExpander.js';
1111
import {showErrorToast} from '../../modules/toast.js';
1212
import {POST} from '../../modules/fetch.js';
1313
import {initTextareaMarkdown} from './EditorMarkdown.js';
14-
import {initDropzone} from '../dropzone.js';
14+
import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.js';
1515

1616
let elementIdCounter = 0;
1717

@@ -111,7 +111,7 @@ class ComboMarkdownEditor {
111111

112112
initTextareaMarkdown(this.textarea);
113113
if (this.dropzone) {
114-
initTextareaPaste(this.textarea, this.dropzone);
114+
initTextareaUpload(this.textarea, this.dropzone);
115115
}
116116
}
117117

@@ -130,13 +130,13 @@ class ComboMarkdownEditor {
130130

131131
dropzoneReloadFiles() {
132132
if (!this.dropzone) return;
133-
this.attachedDropzoneInst.emit('reload');
133+
this.attachedDropzoneInst.emit(DropzoneCustomEventReloadFiles);
134134
}
135135

136136
dropzoneSubmitReload() {
137137
if (!this.dropzone) return;
138138
this.attachedDropzoneInst.emit('submit');
139-
this.attachedDropzoneInst.emit('reload');
139+
this.attachedDropzoneInst.emit(DropzoneCustomEventReloadFiles);
140140
}
141141

142142
setupTab() {

web_src/js/features/comp/EditorMarkdown.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import {triggerEditorContentChanged} from './Paste.js';
1+
export function triggerEditorContentChanged(target) {
2+
target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true}));
3+
}
24

35
function handleIndentSelection(textarea, e) {
46
const selStart = textarea.selectionStart;

web_src/js/features/comp/Paste.js renamed to web_src/js/features/comp/EditorUpload.js

Lines changed: 72 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,29 @@
1-
import {htmlEscape} from 'escape-goat';
2-
import {POST} from '../../modules/fetch.js';
31
import {imageInfo} from '../../utils/image.js';
4-
import {getPastedContent, replaceTextareaSelection} from '../../utils/dom.js';
2+
import {replaceTextareaSelection} from '../../utils/dom.js';
53
import {isUrl} from '../../utils/url.js';
6-
7-
async function uploadFile(file, uploadUrl) {
8-
const formData = new FormData();
9-
formData.append('file', file, file.name);
10-
11-
const res = await POST(uploadUrl, {data: formData});
12-
return await res.json();
13-
}
14-
15-
export function triggerEditorContentChanged(target) {
16-
target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true}));
4+
import {triggerEditorContentChanged} from './EditorMarkdown.js';
5+
import {
6+
DropzoneCustomEventRemovedFile,
7+
DropzoneCustomEventUploadDone,
8+
generateMarkdownLinkForAttachment,
9+
} from '../dropzone.js';
10+
11+
let uploadIdCounter = 0;
12+
13+
function uploadFile(dropzoneEl, file) {
14+
return new Promise((resolve) => {
15+
const curUploadId = uploadIdCounter++;
16+
file._giteaUploadId = curUploadId;
17+
const dropzoneInst = dropzoneEl.dropzone;
18+
const onUploadDone = ({file}) => {
19+
if (file._giteaUploadId === curUploadId) {
20+
dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone);
21+
resolve();
22+
}
23+
};
24+
dropzoneInst.on(DropzoneCustomEventUploadDone, onUploadDone);
25+
dropzoneInst.handleFiles([file]);
26+
});
1727
}
1828

1929
class TextareaEditor {
@@ -82,48 +92,25 @@ class CodeMirrorEditor {
8292
}
8393
}
8494

85-
async function handleClipboardImages(editor, dropzone, images, e) {
86-
const uploadUrl = dropzone.getAttribute('data-upload-url');
87-
const filesContainer = dropzone.querySelector('.files');
88-
89-
if (!dropzone || !uploadUrl || !filesContainer || !images.length) return;
90-
95+
async function handleUploadFiles(editor, dropzoneEl, files, e) {
9196
e.preventDefault();
92-
e.stopPropagation();
93-
94-
for (const img of images) {
95-
const name = img.name.slice(0, img.name.lastIndexOf('.'));
97+
for (const file of files) {
98+
const name = file.name.slice(0, file.name.lastIndexOf('.'));
99+
const {width, dppx} = await imageInfo(file);
100+
const placeholder = `[${name}](uploading ...)`;
96101

97-
const placeholder = `![${name}](uploading ...)`;
98102
editor.insertPlaceholder(placeholder);
99-
100-
const {uuid} = await uploadFile(img, uploadUrl);
101-
const {width, dppx} = await imageInfo(img);
102-
103-
let text;
104-
if (width > 0 && dppx > 1) {
105-
// Scale down images from HiDPI monitors. This uses the <img> tag because it's the only
106-
// method to change image size in Markdown that is supported by all implementations.
107-
// Make the image link relative to the repo path, then the final URL is "/sub-path/owner/repo/attachments/{uuid}"
108-
const url = `attachments/${uuid}`;
109-
text = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(name)}" src="${htmlEscape(url)}">`;
110-
} else {
111-
// Markdown always renders the image with a relative path, so the final URL is "/sub-path/owner/repo/attachments/{uuid}"
112-
// TODO: it should also use relative path for consistency, because absolute is ambiguous for "/sub-path/attachments" or "/attachments"
113-
const url = `/attachments/${uuid}`;
114-
text = `![${name}](${url})`;
115-
}
116-
editor.replacePlaceholder(placeholder, text);
117-
118-
const input = document.createElement('input');
119-
input.setAttribute('name', 'files');
120-
input.setAttribute('type', 'hidden');
121-
input.setAttribute('id', uuid);
122-
input.value = uuid;
123-
filesContainer.append(input);
103+
await uploadFile(dropzoneEl, file); // the "file" will get its "uuid" during the upload
104+
editor.replacePlaceholder(placeholder, generateMarkdownLinkForAttachment(file, {width, dppx}));
124105
}
125106
}
126107

108+
export function removeAttachmentLinksFromMarkdown(text, fileUuid) {
109+
text = text.replace(new RegExp(`!?\\[([^\\]]+)\\]\\(/?attachments/${fileUuid}\\)`, 'g'), '');
110+
text = text.replace(new RegExp(`<img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), '');
111+
return text;
112+
}
113+
127114
function handleClipboardText(textarea, e, {text, isShiftDown}) {
128115
// pasting with "shift" means "paste as original content" in most applications
129116
if (isShiftDown) return; // let the browser handle it
@@ -139,16 +126,37 @@ function handleClipboardText(textarea, e, {text, isShiftDown}) {
139126
// else, let the browser handle it
140127
}
141128

142-
export function initEasyMDEPaste(easyMDE, dropzone) {
129+
// extract text and images from "paste" event
130+
function getPastedContent(e) {
131+
const images = [];
132+
for (const item of e.clipboardData?.items ?? []) {
133+
if (item.type?.startsWith('image/')) {
134+
images.push(item.getAsFile());
135+
}
136+
}
137+
const text = e.clipboardData?.getData?.('text') ?? '';
138+
return {text, images};
139+
}
140+
141+
export function initEasyMDEPaste(easyMDE, dropzoneEl) {
142+
const editor = new CodeMirrorEditor(easyMDE.codemirror);
143143
easyMDE.codemirror.on('paste', (_, e) => {
144144
const {images} = getPastedContent(e);
145-
if (images.length) {
146-
handleClipboardImages(new CodeMirrorEditor(easyMDE.codemirror), dropzone, images, e);
147-
}
145+
if (!images.length) return;
146+
handleUploadFiles(editor, dropzoneEl, images, e);
147+
});
148+
easyMDE.codemirror.on('drop', (_, e) => {
149+
if (!e.dataTransfer.files.length) return;
150+
handleUploadFiles(editor, dropzoneEl, e.dataTransfer.files, e);
151+
});
152+
dropzoneEl.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}) => {
153+
const oldText = easyMDE.codemirror.getValue();
154+
const newText = removeAttachmentLinksFromMarkdown(oldText, fileUuid);
155+
if (oldText !== newText) easyMDE.codemirror.setValue(newText);
148156
});
149157
}
150158

151-
export function initTextareaPaste(textarea, dropzone) {
159+
export function initTextareaUpload(textarea, dropzoneEl) {
152160
let isShiftDown = false;
153161
textarea.addEventListener('keydown', (e) => {
154162
if (e.shiftKey) isShiftDown = true;
@@ -159,9 +167,17 @@ export function initTextareaPaste(textarea, dropzone) {
159167
textarea.addEventListener('paste', (e) => {
160168
const {images, text} = getPastedContent(e);
161169
if (images.length) {
162-
handleClipboardImages(new TextareaEditor(textarea), dropzone, images, e);
170+
handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, images, e);
163171
} else if (text) {
164172
handleClipboardText(textarea, e, {text, isShiftDown});
165173
}
166174
});
175+
textarea.addEventListener('drop', (e) => {
176+
if (!e.dataTransfer.files.length) return;
177+
handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, e.dataTransfer.files, e);
178+
});
179+
dropzoneEl.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}) => {
180+
const newText = removeAttachmentLinksFromMarkdown(textarea.value, fileUuid);
181+
if (textarea.value !== newText) textarea.value = newText;
182+
});
167183
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {removeAttachmentLinksFromMarkdown} from './EditorUpload.js';
2+
3+
test('removeAttachmentLinksFromMarkdown', () => {
4+
expect(removeAttachmentLinksFromMarkdown('a foo b', 'foo')).toBe('a foo b');
5+
expect(removeAttachmentLinksFromMarkdown('a [x](attachments/foo) b', 'foo')).toBe('a b');
6+
expect(removeAttachmentLinksFromMarkdown('a ![x](attachments/foo) b', 'foo')).toBe('a b');
7+
expect(removeAttachmentLinksFromMarkdown('a [x](/attachments/foo) b', 'foo')).toBe('a b');
8+
expect(removeAttachmentLinksFromMarkdown('a ![x](/attachments/foo) b', 'foo')).toBe('a b');
9+
10+
expect(removeAttachmentLinksFromMarkdown('a <img src="attachments/foo"> b', 'foo')).toBe('a b');
11+
expect(removeAttachmentLinksFromMarkdown('a <img width="100" src="attachments/foo"> b', 'foo')).toBe('a b');
12+
expect(removeAttachmentLinksFromMarkdown('a <img src="/attachments/foo"> b', 'foo')).toBe('a b');
13+
expect(removeAttachmentLinksFromMarkdown('a <img src="/attachments/foo" width="100"/> b', 'foo')).toBe('a b');
14+
});

0 commit comments

Comments
 (0)