diff --git a/templates/repo/issue/fields/textarea.tmpl b/templates/repo/issue/fields/textarea.tmpl
index ad3c5efa04..49d51eb5a8 100644
--- a/templates/repo/issue/fields/textarea.tmpl
+++ b/templates/repo/issue/fields/textarea.tmpl
@@ -1,6 +1,25 @@
-
+{{$useMarkdownEditor := not .item.Attributes.render}}
+
{{template "repo/issue/fields/header" .}}
- {{/* FIXME: preview markdown result */}}
- {{/* FIXME: required validation for markdown editor */}}
-
+
+ {{/* the real form element to provide the value */}}
+
+
+ {{if $useMarkdownEditor}}
+ {{template "shared/combomarkdowneditor" (dict
+ "locale" .root.locale
+ "ContainerClasses" "gt-hidden"
+ "MarkdownPreviewUrl" (print .root.RepoLink "/markup")
+ "MarkdownPreviewContext" .root.RepoLink
+ "TextareaContent" .item.Attributes.value
+ "TextareaPlaceholder" .item.Attributes.placeholder
+ "DropzoneParentContainer" ".combo-editor-dropzone"
+ )}}
+
+ {{if .root.IsAttachmentEnabled}}
+
+ {{template "repo/upload" .root}}
+
+ {{end}}
+ {{end}}
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index 196e911061..c12b8149b0 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -24,18 +24,13 @@
{{else if eq .Type "markdown"}}
{{template "repo/issue/fields/markdown" dict "Context" $.Context "item" .}}
{{else if eq .Type "textarea"}}
- {{template "repo/issue/fields/textarea" dict "Context" $.Context "item" .}}
+ {{template "repo/issue/fields/textarea" dict "Context" $.Context "item" . "root" $}}
{{else if eq .Type "dropdown"}}
{{template "repo/issue/fields/dropdown" dict "Context" $.Context "item" .}}
{{else if eq .Type "checkboxes"}}
{{template "repo/issue/fields/checkboxes" dict "Context" $.Context "item" .}}
{{end}}
{{end}}
- {{if .IsAttachmentEnabled}}
-
- {{template "repo/upload" .}}
-
- {{end}}
{{else}}
{{template "repo/issue/comment_tab" .}}
{{end}}
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
index 90d1bcde5a..103e71daae 100644
--- a/web_src/js/features/comp/ComboMarkdownEditor.js
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -5,10 +5,9 @@ import {attachTribute} from '../tribute.js';
import {hideElem, showElem, autosize} from '../../utils/dom.js';
import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
-import {emojiString} from '../emoji.js';
import {renderPreviewPanelContent} from '../repo-editor.js';
-import {matchEmoji, matchMention} from '../../utils/match.js';
import {easyMDEToolbarActions} from './EasyMDEToolbarActions.js';
+import {initTextExpander} from './TextExpander.js';
let elementIdCounter = 0;
@@ -43,14 +42,12 @@ class ComboMarkdownEditor {
async init() {
this.prepareEasyMDEToolbarActions();
+ this.setupContainer();
this.setupTab();
this.setupDropzone();
this.setupTextarea();
- this.setupExpander();
- if (this.userPreferredEditor === 'easymde') {
- await this.switchToEasyMDE();
- }
+ await this.switchToUserPreference();
}
applyEditorHeights(el, heights) {
@@ -60,6 +57,11 @@ class ComboMarkdownEditor {
if (heights.maxHeight) el.style.maxHeight = heights.maxHeight;
}
+ setupContainer() {
+ initTextExpander(this.container.querySelector('text-expander'));
+ this.container.addEventListener('ce-editor-content-changed', (e) => this.options?.onContentChanged?.(this, e));
+ }
+
setupTextarea() {
this.textarea = this.container.querySelector('.markdown-text-editor');
this.textarea._giteaComboMarkdownEditor = this;
@@ -103,64 +105,6 @@ class ComboMarkdownEditor {
}
}
- setupExpander() {
- const expander = this.container.querySelector('text-expander');
- expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
- if (key === ':') {
- const matches = matchEmoji(text);
- if (!matches.length) return provide({matched: false});
-
- const ul = document.createElement('ul');
- ul.classList.add('suggestions');
- for (const name of matches) {
- const emoji = emojiString(name);
- const li = document.createElement('li');
- li.setAttribute('role', 'option');
- li.setAttribute('data-value', emoji);
- li.textContent = `${emoji} ${name}`;
- ul.append(li);
- }
-
- provide({matched: true, fragment: ul});
- } else if (key === '@') {
- const matches = matchMention(text);
- if (!matches.length) return provide({matched: false});
-
- const ul = document.createElement('ul');
- ul.classList.add('suggestions');
- for (const {value, name, fullname, avatar} of matches) {
- const li = document.createElement('li');
- li.setAttribute('role', 'option');
- li.setAttribute('data-value', `${key}${value}`);
-
- const img = document.createElement('img');
- img.src = avatar;
- li.append(img);
-
- const nameSpan = document.createElement('span');
- nameSpan.textContent = name;
- li.append(nameSpan);
-
- if (fullname && fullname.toLowerCase() !== name) {
- const fullnameSpan = document.createElement('span');
- fullnameSpan.classList.add('fullname');
- fullnameSpan.textContent = fullname;
- li.append(fullnameSpan);
- }
-
- ul.append(li);
- }
-
- provide({matched: true, fragment: ul});
- }
- });
- expander?.addEventListener('text-expander-value', ({detail}) => {
- if (detail?.item) {
- detail.value = detail.item.getAttribute('data-value');
- }
- });
- }
-
setupDropzone() {
const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container');
if (dropzoneParentContainer) {
@@ -224,7 +168,16 @@ class ComboMarkdownEditor {
return processed;
}
+ async switchToUserPreference() {
+ if (this.userPreferredEditor === 'easymde') {
+ await this.switchToEasyMDE();
+ } else {
+ this.switchToTextarea();
+ }
+ }
+
switchToTextarea() {
+ if (!this.easyMDE) return;
showElem(this.textareaMarkdownToolbar);
if (this.easyMDE) {
this.easyMDE.toTextArea();
@@ -233,6 +186,7 @@ class ComboMarkdownEditor {
}
async switchToEasyMDE() {
+ if (this.easyMDE) return;
// EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can not overwrite the default styles.
const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde');
const easyMDEOpt = {
diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js
index 9145b24062..dc335495a3 100644
--- a/web_src/js/features/comp/ImagePaste.js
+++ b/web_src/js/features/comp/ImagePaste.js
@@ -25,6 +25,10 @@ function clipboardPastedImages(e) {
return files;
}
+function triggerEditorContentChanged(target) {
+ target.dispatchEvent(new CustomEvent('ce-editor-content-changed', {bubbles: true}));
+}
+
class TextareaEditor {
constructor(editor) {
this.editor = editor;
@@ -38,6 +42,7 @@ class TextareaEditor {
editor.selectionStart = startPos;
editor.selectionEnd = startPos + value.length;
editor.focus();
+ triggerEditorContentChanged(editor);
}
replacePlaceholder(oldVal, newVal) {
@@ -54,6 +59,7 @@ class TextareaEditor {
}
editor.selectionStart = editor.selectionEnd;
editor.focus();
+ triggerEditorContentChanged(editor);
}
}
@@ -70,6 +76,7 @@ class CodeMirrorEditor {
endPoint.ch = startPoint.ch + value.length;
editor.setSelection(startPoint, endPoint);
editor.focus();
+ triggerEditorContentChanged(editor.getTextArea());
}
replacePlaceholder(oldVal, newVal) {
@@ -84,6 +91,7 @@ class CodeMirrorEditor {
endPoint.ch += newVal.length;
editor.setSelection(endPoint, endPoint);
editor.focus();
+ triggerEditorContentChanged(editor.getTextArea());
}
}
diff --git a/web_src/js/features/comp/QuickSubmit.js b/web_src/js/features/comp/QuickSubmit.js
index 43424a949f..d598a59655 100644
--- a/web_src/js/features/comp/QuickSubmit.js
+++ b/web_src/js/features/comp/QuickSubmit.js
@@ -6,7 +6,9 @@ export function handleGlobalEnterQuickSubmit(target) {
if ($form.length) {
// here use the event to trigger the submit event (instead of calling `submit()` method directly)
// otherwise the `areYouSure` handler won't be executed, then there will be an annoying "confirm to leave" dialog
- $form.trigger('submit');
+ if ($form[0].checkValidity()) {
+ $form.trigger('submit');
+ }
} else {
// 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.
// the 'ce-' prefix means this is a CustomEvent
diff --git a/web_src/js/features/comp/TextExpander.js b/web_src/js/features/comp/TextExpander.js
new file mode 100644
index 0000000000..e2840610df
--- /dev/null
+++ b/web_src/js/features/comp/TextExpander.js
@@ -0,0 +1,59 @@
+import {matchEmoji, matchMention} from '../../utils/match.js';
+import {emojiString} from '../emoji.js';
+
+export function initTextExpander(expander) {
+ expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => {
+ if (key === ':') {
+ const matches = matchEmoji(text);
+ if (!matches.length) return provide({matched: false});
+
+ const ul = document.createElement('ul');
+ ul.classList.add('suggestions');
+ for (const name of matches) {
+ const emoji = emojiString(name);
+ const li = document.createElement('li');
+ li.setAttribute('role', 'option');
+ li.setAttribute('data-value', emoji);
+ li.textContent = `${emoji} ${name}`;
+ ul.append(li);
+ }
+
+ provide({matched: true, fragment: ul});
+ } else if (key === '@') {
+ const matches = matchMention(text);
+ if (!matches.length) return provide({matched: false});
+
+ const ul = document.createElement('ul');
+ ul.classList.add('suggestions');
+ for (const {value, name, fullname, avatar} of matches) {
+ const li = document.createElement('li');
+ li.setAttribute('role', 'option');
+ li.setAttribute('data-value', `${key}${value}`);
+
+ const img = document.createElement('img');
+ img.src = avatar;
+ li.append(img);
+
+ const nameSpan = document.createElement('span');
+ nameSpan.textContent = name;
+ li.append(nameSpan);
+
+ if (fullname && fullname.toLowerCase() !== name) {
+ const fullnameSpan = document.createElement('span');
+ fullnameSpan.classList.add('fullname');
+ fullnameSpan.textContent = fullname;
+ li.append(fullnameSpan);
+ }
+
+ ul.append(li);
+ }
+
+ provide({matched: true, fragment: ul});
+ }
+ });
+ expander?.addEventListener('text-expander-value', ({detail}) => {
+ if (detail?.item) {
+ detail.value = detail.item.getAttribute('data-value');
+ }
+ });
+}
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 7e1249ed2f..8ecc7aa4ca 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -665,3 +665,59 @@ export function initRepoIssueGotoID() {
}
});
}
+
+export function initSingleCommentEditor($commentForm) {
+ // pages:
+ // * normal new issue/pr page, no status-button
+ // * issue/pr view page, with comment form, has status-button
+ const opts = {};
+ const $statusButton = $('#status-button');
+ if ($statusButton.length) {
+ $statusButton.on('click', (e) => {
+ e.preventDefault();
+ $('#status').val($statusButton.data('status-val'));
+ $('#comment-form').trigger('submit');
+ });
+ opts.onContentChanged = (editor) => {
+ $statusButton.text($statusButton.attr(editor.value().trim() ? 'data-status-and-comment' : 'data-status'));
+ };
+ }
+ initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), opts);
+}
+
+export function initIssueTemplateCommentEditors($commentForm) {
+ // pages:
+ // * new issue with issue template
+ const $comboFields = $commentForm.find('.combo-editor-dropzone');
+
+ const initCombo = async ($combo) => {
+ const $dropzoneContainer = $combo.find('.form-field-dropzone');
+ const $formField = $combo.find('.form-field-real');
+ const $markdownEditor = $combo.find('.combo-markdown-editor');
+
+ const editor = await initComboMarkdownEditor($markdownEditor, {
+ onContentChanged: (editor) => {
+ $formField.val(editor.value());
+ }
+ });
+
+ $formField.on('focus', async () => {
+ // deactivate all markdown editors
+ showElem($commentForm.find('.combo-editor-dropzone .form-field-real'));
+ hideElem($commentForm.find('.combo-editor-dropzone .combo-markdown-editor'));
+ hideElem($commentForm.find('.combo-editor-dropzone .form-field-dropzone'));
+
+ // activate this markdown editor
+ hideElem($formField);
+ showElem($markdownEditor);
+ showElem($dropzoneContainer);
+
+ await editor.switchToUserPreference();
+ editor.focus();
+ });
+ };
+
+ for (const el of $comboFields) {
+ initCombo($(el));
+ }
+}
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index c3bd0ccb76..2804844d81 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -3,7 +3,7 @@ import {
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
initRepoIssueTitleEdit, initRepoIssueWipToggle,
- initRepoPullRequestUpdate, updateIssuesMeta, handleReply
+ initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
} from './repo-issue.js';
import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
import {svg} from '../svg.js';
@@ -53,6 +53,13 @@ export function initRepoCommentForm() {
return;
}
+ if ($commentForm.find('.field.combo-editor-dropzone').length) {
+ // at the moment, if a form has multiple combo-markdown-editors, it must be a issue template form
+ initIssueTemplateCommentEditors($commentForm);
+ } else {
+ initSingleCommentEditor($commentForm);
+ }
+
function initBranchSelector() {
const $selectBranch = $('.ui.select-branch');
const $branchMenu = $selectBranch.find('.reference-list-menu');
@@ -82,19 +89,6 @@ export function initRepoCommentForm() {
});
}
- const $statusButton = $('#status-button');
- $statusButton.on('click', (e) => {
- e.preventDefault();
- $('#status').val($statusButton.data('status-val'));
- $('#comment-form').trigger('submit');
- });
-
- const _promise = initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), {
- onContentChanged(editor) {
- $statusButton.text($statusButton.attr(editor.value().trim() ? 'data-status-and-comment' : 'data-status'));
- },
- });
-
initBranchSelector();
// List submits