Skip to content

Commit c4303ef

Browse files
yp05327wxiaoguangsilverwind
authored
Support markdown editor for issue template (#24400)
Fixes #24398 Task: - [x] Reusing "textarea" like GitHub seems more friendly to users. - [x] ^V image pasting and file uploading handling. <details><summary>screenshots</summary> ![image](https://user-images.githubusercontent.com/18380374/235418877-00090552-ebda-411c-8e39-b47246bc8746.png) ![image](https://user-images.githubusercontent.com/18380374/235419073-dc33cad7-7626-4bce-9161-eb205c7384b5.png) Display only one markdown editor: ![image](https://user-images.githubusercontent.com/18380374/235419098-ee21386d-2b2d-432e-bdb2-18646cc031e7.png) Support file upload and ^V image pasting ![image](https://user-images.githubusercontent.com/18380374/235419364-7b390fa4-da56-437d-b55e-3847fbc049e7.png) </details> --------- Co-authored-by: wxiaoguang <[email protected]> Co-authored-by: silverwind <[email protected]>
1 parent 9ad5b59 commit c4303ef

File tree

8 files changed

+176
-89
lines changed

8 files changed

+176
-89
lines changed
Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
1-
<div class="field">
1+
{{$useMarkdownEditor := not .item.Attributes.render}}
2+
<div class="field {{if $useMarkdownEditor}}combo-editor-dropzone{{end}}">
23
{{template "repo/issue/fields/header" .}}
3-
{{/* FIXME: preview markdown result */}}
4-
{{/* FIXME: required validation for markdown editor */}}
5-
<textarea name="form-field-{{.item.ID}}" placeholder="{{.item.Attributes.placeholder}}" {{if and .item.Validations.required .item.Attributes.render}}required{{end}}>{{.item.Attributes.value}}</textarea>
4+
5+
{{/* the real form element to provide the value */}}
6+
<textarea class="form-field-real" name="form-field-{{.item.ID}}" placeholder="{{.item.Attributes.placeholder}}" {{if and .item.Validations.required}}required{{end}}>{{.item.Attributes.value}}</textarea>
7+
8+
{{if $useMarkdownEditor}}
9+
{{template "shared/combomarkdowneditor" (dict
10+
"locale" .root.locale
11+
"ContainerClasses" "gt-hidden"
12+
"MarkdownPreviewUrl" (print .root.RepoLink "/markup")
13+
"MarkdownPreviewContext" .root.RepoLink
14+
"TextareaContent" .item.Attributes.value
15+
"TextareaPlaceholder" .item.Attributes.placeholder
16+
"DropzoneParentContainer" ".combo-editor-dropzone"
17+
)}}
18+
19+
{{if .root.IsAttachmentEnabled}}
20+
<div class="gt-mt-4 form-field-dropzone gt-hidden">
21+
{{template "repo/upload" .root}}
22+
</div>
23+
{{end}}
24+
{{end}}
625
</div>

templates/repo/issue/new_form.tmpl

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,13 @@
2424
{{else if eq .Type "markdown"}}
2525
{{template "repo/issue/fields/markdown" dict "Context" $.Context "item" .}}
2626
{{else if eq .Type "textarea"}}
27-
{{template "repo/issue/fields/textarea" dict "Context" $.Context "item" .}}
27+
{{template "repo/issue/fields/textarea" dict "Context" $.Context "item" . "root" $}}
2828
{{else if eq .Type "dropdown"}}
2929
{{template "repo/issue/fields/dropdown" dict "Context" $.Context "item" .}}
3030
{{else if eq .Type "checkboxes"}}
3131
{{template "repo/issue/fields/checkboxes" dict "Context" $.Context "item" .}}
3232
{{end}}
3333
{{end}}
34-
{{if .IsAttachmentEnabled}}
35-
<div class="field">
36-
{{template "repo/upload" .}}
37-
</div>
38-
{{end}}
3934
{{else}}
4035
{{template "repo/issue/comment_tab" .}}
4136
{{end}}

web_src/js/features/comp/ComboMarkdownEditor.js

Lines changed: 18 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@ import {attachTribute} from '../tribute.js';
55
import {hideElem, showElem, autosize} from '../../utils/dom.js';
66
import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
77
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
8-
import {emojiString} from '../emoji.js';
98
import {renderPreviewPanelContent} from '../repo-editor.js';
10-
import {matchEmoji, matchMention} from '../../utils/match.js';
119
import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js';
10+
import {initTextExpander} from './TextExpander.js';
1211

1312
let elementIdCounter = 0;
1413

@@ -43,14 +42,12 @@ class ComboMarkdownEditor {
4342

4443
async init() {
4544
this.prepareEasyMDEToolbarActions();
45+
this.setupContainer();
4646
this.setupTab();
4747
this.setupDropzone();
4848
this.setupTextarea();
49-
this.setupExpander();
5049

51-
if (this.userPreferredEditor === 'easymde') {
52-
await this.switchToEasyMDE();
53-
}
50+
await this.switchToUserPreference();
5451
}
5552

5653
applyEditorHeights(el, heights) {
@@ -60,6 +57,11 @@ class ComboMarkdownEditor {
6057
if (heights.maxHeight) el.style.maxHeight = heights.maxHeight;
6158
}
6259

60+
setupContainer() {
61+
initTextExpander(this.container.querySelector('text-expander'));
62+
this.container.addEventListener('ce-editor-content-changed', (e) => this.options?.onContentChanged?.(this, e));
63+
}
64+
6365
setupTextarea() {
6466
this.textarea = this.container.querySelector('.markdown-text-editor');
6567
this.textarea._giteaComboMarkdownEditor = this;
@@ -103,64 +105,6 @@ class ComboMarkdownEditor {
103105
}
104106
}
105107

106-
setupExpander() {
107-
const expander = this.container.querySelector('text-expander');
108-
expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
109-
if (key === ':') {
110-
const matches = matchEmoji(text);
111-
if (!matches.length) return provide({matched: false});
112-
113-
const ul = document.createElement('ul');
114-
ul.classList.add('suggestions');
115-
for (const name of matches) {
116-
const emoji = emojiString(name);
117-
const li = document.createElement('li');
118-
li.setAttribute('role', 'option');
119-
li.setAttribute('data-value', emoji);
120-
li.textContent = `${emoji} ${name}`;
121-
ul.append(li);
122-
}
123-
124-
provide({matched: true, fragment: ul});
125-
} else if (key === '@') {
126-
const matches = matchMention(text);
127-
if (!matches.length) return provide({matched: false});
128-
129-
const ul = document.createElement('ul');
130-
ul.classList.add('suggestions');
131-
for (const {value, name, fullname, avatar} of matches) {
132-
const li = document.createElement('li');
133-
li.setAttribute('role', 'option');
134-
li.setAttribute('data-value', `${key}${value}`);
135-
136-
const img = document.createElement('img');
137-
img.src = avatar;
138-
li.append(img);
139-
140-
const nameSpan = document.createElement('span');
141-
nameSpan.textContent = name;
142-
li.append(nameSpan);
143-
144-
if (fullname && fullname.toLowerCase() !== name) {
145-
const fullnameSpan = document.createElement('span');
146-
fullnameSpan.classList.add('fullname');
147-
fullnameSpan.textContent = fullname;
148-
li.append(fullnameSpan);
149-
}
150-
151-
ul.append(li);
152-
}
153-
154-
provide({matched: true, fragment: ul});
155-
}
156-
});
157-
expander?.addEventListener('text-expander-value', ({detail}) => {
158-
if (detail?.item) {
159-
detail.value = detail.item.getAttribute('data-value');
160-
}
161-
});
162-
}
163-
164108
setupDropzone() {
165109
const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container');
166110
if (dropzoneParentContainer) {
@@ -224,7 +168,16 @@ class ComboMarkdownEditor {
224168
return processed;
225169
}
226170

171+
async switchToUserPreference() {
172+
if (this.userPreferredEditor === 'easymde') {
173+
await this.switchToEasyMDE();
174+
} else {
175+
this.switchToTextarea();
176+
}
177+
}
178+
227179
switchToTextarea() {
180+
if (!this.easyMDE) return;
228181
showElem(this.textareaMarkdownToolbar);
229182
if (this.easyMDE) {
230183
this.easyMDE.toTextArea();
@@ -233,6 +186,7 @@ class ComboMarkdownEditor {
233186
}
234187

235188
async switchToEasyMDE() {
189+
if (this.easyMDE) return;
236190
// EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can not overwrite the default styles.
237191
const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde');
238192
const easyMDEOpt = {

web_src/js/features/comp/ImagePaste.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ function clipboardPastedImages(e) {
2525
return files;
2626
}
2727

28+
function triggerEditorContentChanged(target) {
29+
target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true}));
30+
}
31+
2832
class TextareaEditor {
2933
constructor(editor) {
3034
this.editor = editor;
@@ -38,6 +42,7 @@ class TextareaEditor {
3842
editor.selectionStart = startPos;
3943
editor.selectionEnd = startPos + value.length;
4044
editor.focus();
45+
triggerEditorContentChanged(editor);
4146
}
4247

4348
replacePlaceholder(oldVal, newVal) {
@@ -54,6 +59,7 @@ class TextareaEditor {
5459
}
5560
editor.selectionStart = editor.selectionEnd;
5661
editor.focus();
62+
triggerEditorContentChanged(editor);
5763
}
5864
}
5965

@@ -70,6 +76,7 @@ class CodeMirrorEditor {
7076
endPoint.ch = startPoint.ch + value.length;
7177
editor.setSelection(startPoint, endPoint);
7278
editor.focus();
79+
triggerEditorContentChanged(editor.getTextArea());
7380
}
7481

7582
replacePlaceholder(oldVal, newVal) {
@@ -84,6 +91,7 @@ class CodeMirrorEditor {
8491
endPoint.ch += newVal.length;
8592
editor.setSelection(endPoint, endPoint);
8693
editor.focus();
94+
triggerEditorContentChanged(editor.getTextArea());
8795
}
8896
}
8997

web_src/js/features/comp/QuickSubmit.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ export function handleGlobalEnterQuickSubmit(target) {
66
if ($form.length) {
77
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
88
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
9-
$form.trigger('submit');
9+
if ($form[0].checkValidity()) {
10+
$form.trigger('submit');
11+
}
1012
} else {
1113
// if no form, then the editor is for an AJAX request, dispatch an event to the target, let the target's event handler to do the AJAX request.
1214
// the 'ce-' prefix means this is a CustomEvent
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {matchEmoji, matchMention} from '../../utils/match.js';
2+
import {emojiString} from '../emoji.js';
3+
4+
export function initTextExpander(expander) {
5+
expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
6+
if (key === ':') {
7+
const matches = matchEmoji(text);
8+
if (!matches.length) return provide({matched: false});
9+
10+
const ul = document.createElement('ul');
11+
ul.classList.add('suggestions');
12+
for (const name of matches) {
13+
const emoji = emojiString(name);
14+
const li = document.createElement('li');
15+
li.setAttribute('role', 'option');
16+
li.setAttribute('data-value', emoji);
17+
li.textContent = `${emoji} ${name}`;
18+
ul.append(li);
19+
}
20+
21+
provide({matched: true, fragment: ul});
22+
} else if (key === '@') {
23+
const matches = matchMention(text);
24+
if (!matches.length) return provide({matched: false});
25+
26+
const ul = document.createElement('ul');
27+
ul.classList.add('suggestions');
28+
for (const {value, name, fullname, avatar} of matches) {
29+
const li = document.createElement('li');
30+
li.setAttribute('role', 'option');
31+
li.setAttribute('data-value', `${key}${value}`);
32+
33+
const img = document.createElement('img');
34+
img.src = avatar;
35+
li.append(img);
36+
37+
const nameSpan = document.createElement('span');
38+
nameSpan.textContent = name;
39+
li.append(nameSpan);
40+
41+
if (fullname && fullname.toLowerCase() !== name) {
42+
const fullnameSpan = document.createElement('span');
43+
fullnameSpan.classList.add('fullname');
44+
fullnameSpan.textContent = fullname;
45+
li.append(fullnameSpan);
46+
}
47+
48+
ul.append(li);
49+
}
50+
51+
provide({matched: true, fragment: ul});
52+
}
53+
});
54+
expander?.addEventListener('text-expander-value', ({detail}) => {
55+
if (detail?.item) {
56+
detail.value = detail.item.getAttribute('data-value');
57+
}
58+
});
59+
}

web_src/js/features/repo-issue.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,3 +665,59 @@ export function initRepoIssueGotoID() {
665665
}
666666
});
667667
}
668+
669+
export function initSingleCommentEditor($commentForm) {
670+
// pages:
671+
// * normal new issue/pr page, no status-button
672+
// * issue/pr view page, with comment form, has status-button
673+
const opts = {};
674+
const $statusButton = $('#status-button');
675+
if ($statusButton.length) {
676+
$statusButton.on('click', (e) => {
677+
e.preventDefault();
678+
$('#status').val($statusButton.data('status-val'));
679+
$('#comment-form').trigger('submit');
680+
});
681+
opts.onContentChanged = (editor) => {
682+
$statusButton.text($statusButton.attr(editor.value().trim() ? 'data-status-and-comment' : 'data-status'));
683+
};
684+
}
685+
initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts);
686+
}
687+
688+
export function initIssueTemplateCommentEditors($commentForm) {
689+
// pages:
690+
// * new issue with issue template
691+
const $comboFields = $commentForm.find('.combo-editor-dropzone');
692+
693+
const initCombo = async ($combo) => {
694+
const $dropzoneContainer = $combo.find('.form-field-dropzone');
695+
const $formField = $combo.find('.form-field-real');
696+
const $markdownEditor = $combo.find('.combo-markdown-editor');
697+
698+
const editor = await initComboMarkdownEditor($markdownEditor, {
699+
onContentChanged: (editor) => {
700+
$formField.val(editor.value());
701+
}
702+
});
703+
704+
$formField.on('focus', async () => {
705+
// deactivate all markdown editors
706+
showElem($commentForm.find('.combo-editor-dropzone .form-field-real'));
707+
hideElem($commentForm.find('.combo-editor-dropzone .combo-markdown-editor'));
708+
hideElem($commentForm.find('.combo-editor-dropzone .form-field-dropzone'));
709+
710+
// activate this markdown editor
711+
hideElem($formField);
712+
showElem($markdownEditor);
713+
showElem($dropzoneContainer);
714+
715+
await editor.switchToUserPreference();
716+
editor.focus();
717+
});
718+
};
719+
720+
for (const el of $comboFields) {
721+
initCombo($(el));
722+
}
723+
}

web_src/js/features/repo-legacy.js

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
44
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
55
initRepoIssueTitleEdit, initRepoIssueWipToggle,
6-
initRepoPullRequestUpdate, updateIssuesMeta, handleReply
6+
initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
77
} from './repo-issue.js';
88
import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
99
import {svg} from '../svg.js';
@@ -53,6 +53,13 @@ export function initRepoCommentForm() {
5353
return;
5454
}
5555

56+
if ($commentForm.find('.field.combo-editor-dropzone').length) {
57+
// at the moment, if a form has multiple combo-markdown-editors, it must be a issue template form
58+
initIssueTemplateCommentEditors($commentForm);
59+
} else {
60+
initSingleCommentEditor($commentForm);
61+
}
62+
5663
function initBranchSelector() {
5764
const $selectBranch = $('.ui.select-branch');
5865
const $branchMenu = $selectBranch.find('.reference-list-menu');
@@ -82,19 +89,6 @@ export function initRepoCommentForm() {
8289
});
8390
}
8491

85-
const $statusButton = $('#status-button');
86-
$statusButton.on('click', (e) => {
87-
e.preventDefault();
88-
$('#status').val($statusButton.data('status-val'));
89-
$('#comment-form').trigger('submit');
90-
});
91-
92-
const _promise = initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), {
93-
onContentChanged(editor) {
94-
$statusButton.text($statusButton.attr(editor.value().trim() ? 'data-status-and-comment' : 'data-status'));
95-
},
96-
});
97-
9892
initBranchSelector();
9993

10094
// List submits

0 commit comments

Comments
 (0)