Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support markdown editor for issue template #24400

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions templates/repo/issue/fields/textarea.tmpl
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
<div class="field">
{{$useMarkdownEditor := not .item.Attributes.render}}
<div class="field {{if $useMarkdownEditor}}combo-editor-dropzone{{end}}">
{{template "repo/issue/fields/header" .}}
{{/* FIXME: preview markdown result */}}
{{/* FIXME: required validation for markdown editor */}}
<textarea name="form-field-{{.item.ID}}" placeholder="{{.item.Attributes.placeholder}}" {{if and .item.Validations.required .item.Attributes.render}}required{{end}}>{{.item.Attributes.value}}</textarea>

{{/* the real form element to provide the value */}}
<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>

{{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}}
<div class="gt-mt-4 form-field-dropzone gt-hidden">
{{template "repo/upload" .root}}
</div>
{{end}}
{{end}}
</div>
7 changes: 1 addition & 6 deletions templates/repo/issue/new_form.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
<div class="field">
{{template "repo/upload" .}}
</div>
{{end}}
Copy link
Contributor

@wxiaoguang wxiaoguang May 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only side effect is: if there is no textarea in template, then no uploader. IMO it's acceptable.

(it could also be easily fixed by: if no textarea, then add the "uploader" in the end, feel free to do so or not )

{{else}}
{{template "repo/issue/comment_tab" .}}
{{end}}
Expand Down
82 changes: 18 additions & 64 deletions web_src/js/features/comp/ComboMarkdownEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand All @@ -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 = {
Expand Down
8 changes: 8 additions & 0 deletions web_src/js/features/comp/ImagePaste.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,6 +42,7 @@ class TextareaEditor {
editor.selectionStart = startPos;
editor.selectionEnd = startPos + value.length;
editor.focus();
triggerEditorContentChanged(editor);
}

replacePlaceholder(oldVal, newVal) {
Expand All @@ -54,6 +59,7 @@ class TextareaEditor {
}
editor.selectionStart = editor.selectionEnd;
editor.focus();
triggerEditorContentChanged(editor);
}
}

Expand All @@ -70,6 +76,7 @@ class CodeMirrorEditor {
endPoint.ch = startPoint.ch + value.length;
editor.setSelection(startPoint, endPoint);
editor.focus();
triggerEditorContentChanged(editor.getTextArea());
}

replacePlaceholder(oldVal, newVal) {
Expand All @@ -84,6 +91,7 @@ class CodeMirrorEditor {
endPoint.ch += newVal.length;
editor.setSelection(endPoint, endPoint);
editor.focus();
triggerEditorContentChanged(editor.getTextArea());
}
}

Expand Down
4 changes: 3 additions & 1 deletion web_src/js/features/comp/QuickSubmit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions web_src/js/features/comp/TextExpander.js
Original file line number Diff line number Diff line change
@@ -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');
}
});
}
56 changes: 56 additions & 0 deletions web_src/js/features/repo-issue.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
silverwind marked this conversation as resolved.
Show resolved Hide resolved
}
22 changes: 8 additions & 14 deletions web_src/js/features/repo-legacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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
Expand Down