forked from forgejo/forgejo
Introduce GitHub markdown editor, keep EasyMDE as fallback (#23876)
The first step of the plan * #23290 Thanks to @silverwind for the first try in #15394 . Close #10729 and a lot of related issues. The EasyMDE is not removed, now it works as a fallback, users can switch between these two editors. Editor list: * Issue / PR comment * Issue / PR comment edit * Issue / PR comment quote reply * PR diff view, inline comment * PR diff view, inline comment edit * PR diff view, inline comment quote reply * Release editor * Wiki editor Some editors have attached dropzone Screenshots: <details>     </details> --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
parent
d67e40684f
commit
5cc0801de9
46 changed files with 763 additions and 833 deletions
|
@ -1,11 +1,8 @@
|
|||
import $ from 'jquery';
|
||||
import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.js';
|
||||
import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js';
|
||||
import {initEasyMDEImagePaste} from './comp/ImagePaste.js';
|
||||
import {
|
||||
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
|
||||
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
|
||||
initRepoIssueStatusButton, initRepoIssueTitleEdit, initRepoIssueWipToggle,
|
||||
initRepoIssueTitleEdit, initRepoIssueWipToggle,
|
||||
initRepoPullRequestUpdate, updateIssuesMeta, handleReply
|
||||
} from './repo-issue.js';
|
||||
import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
|
||||
|
@ -19,27 +16,27 @@ import {
|
|||
import {initCitationFileCopyContent} from './citation.js';
|
||||
import {initCompLabelEdit} from './comp/LabelEdit.js';
|
||||
import {initRepoDiffConversationNav} from './repo-diff.js';
|
||||
import {attachTribute} from './tribute.js';
|
||||
import {createDropzone} from './dropzone.js';
|
||||
import {initCommentContent, initMarkupContent} from '../markup/content.js';
|
||||
import {initCompReactionSelector} from './comp/ReactionSelector.js';
|
||||
import {initRepoSettingBranches} from './repo-settings.js';
|
||||
import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
|
||||
import {hideElem, showElem} from '../utils/dom.js';
|
||||
import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
|
||||
import {attachRefIssueContextPopup} from './contextpopup.js';
|
||||
|
||||
const {csrfToken} = window.config;
|
||||
|
||||
// if there are draft comments (more than 20 chars), confirm before reloading, to avoid losing comments
|
||||
// if there are draft comments, confirm before reloading, to avoid losing comments
|
||||
function reloadConfirmDraftComment() {
|
||||
const commentTextareas = [
|
||||
document.querySelector('.edit-content-zone:not(.gt-hidden) textarea'),
|
||||
document.querySelector('.edit_area'),
|
||||
document.querySelector('#comment-form textarea'),
|
||||
];
|
||||
for (const textarea of commentTextareas) {
|
||||
// Most users won't feel too sad if they lose a comment with 10 or 20 chars, they can re-type these in seconds.
|
||||
// Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds.
|
||||
// But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
|
||||
if (textarea && textarea.value.trim().length > 20) {
|
||||
if (textarea && textarea.value.trim().length > 10) {
|
||||
textarea.parentElement.scrollIntoView();
|
||||
if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
|
||||
return;
|
||||
|
@ -85,25 +82,20 @@ export function initRepoCommentForm() {
|
|||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const $statusButton = $('#status-button');
|
||||
for (const textarea of $commentForm.find('textarea:not(.review-textarea, .no-easymde)')) {
|
||||
// Don't initialize EasyMDE for the dormant #edit-content-form
|
||||
if (textarea.closest('#edit-content-form')) {
|
||||
continue;
|
||||
}
|
||||
const easyMDE = await createCommentEasyMDE(textarea, {
|
||||
'onChange': () => {
|
||||
const value = easyMDE?.value().trim();
|
||||
$statusButton.text($statusButton.attr(value.length === 0 ? 'data-status' : 'data-status-and-comment'));
|
||||
},
|
||||
});
|
||||
initEasyMDEImagePaste(easyMDE, $commentForm.find('.dropzone'));
|
||||
}
|
||||
})();
|
||||
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();
|
||||
initCompMarkupContentPreviewTab($commentForm);
|
||||
|
||||
// List submits
|
||||
function initListSubmits(selector, outerSelector) {
|
||||
|
@ -275,7 +267,7 @@ export function initRepoCommentForm() {
|
|||
} else if (input_id === '#project_id') {
|
||||
icon = svg('octicon-project', 18, 'gt-mr-3');
|
||||
} else if (input_id === '#assignee_id') {
|
||||
icon = `<img class="ui avatar image gt-mr-3" src=${$(this).data('avatar')}>`;
|
||||
icon = `<img class="ui avatar image gt-mr-3" alt="avatar" src=${$(this).data('avatar')}>`;
|
||||
}
|
||||
|
||||
$list.find('.selected').html(`
|
||||
|
@ -322,162 +314,148 @@ async function onEditContent(event) {
|
|||
const $editContentZone = $segment.find('.edit-content-zone');
|
||||
const $renderContent = $segment.find('.render-content');
|
||||
const $rawContent = $segment.find('.raw-content');
|
||||
let $textarea;
|
||||
let easyMDE;
|
||||
|
||||
// Setup new form
|
||||
if ($editContentZone.html().length === 0) {
|
||||
$editContentZone.html($('#edit-content-form').html());
|
||||
$textarea = $editContentZone.find('textarea');
|
||||
await attachTribute($textarea.get(), {mentions: true, emoji: true});
|
||||
let comboMarkdownEditor;
|
||||
|
||||
let dz;
|
||||
const $dropzone = $editContentZone.find('.dropzone');
|
||||
if ($dropzone.length === 1) {
|
||||
$dropzone.data('saved', false);
|
||||
const setupDropzone = async ($dropzone) => {
|
||||
if ($dropzone.length === 0) return null;
|
||||
$dropzone.data('saved', false);
|
||||
|
||||
const fileUuidDict = {};
|
||||
dz = await createDropzone($dropzone[0], {
|
||||
url: $dropzone.data('upload-url'),
|
||||
headers: {'X-Csrf-Token': csrfToken},
|
||||
maxFiles: $dropzone.data('max-file'),
|
||||
maxFilesize: $dropzone.data('max-size'),
|
||||
acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
|
||||
addRemoveLinks: true,
|
||||
dictDefaultMessage: $dropzone.data('default-message'),
|
||||
dictInvalidFileType: $dropzone.data('invalid-input-type'),
|
||||
dictFileTooBig: $dropzone.data('file-too-big'),
|
||||
dictRemoveFile: $dropzone.data('remove-file'),
|
||||
timeout: 0,
|
||||
thumbnailMethod: 'contain',
|
||||
thumbnailWidth: 480,
|
||||
thumbnailHeight: 480,
|
||||
init() {
|
||||
this.on('success', (file, data) => {
|
||||
file.uuid = data.uuid;
|
||||
fileUuidDict[file.uuid] = {submitted: false};
|
||||
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
|
||||
$dropzone.find('.files').append(input);
|
||||
const fileUuidDict = {};
|
||||
const dz = await createDropzone($dropzone[0], {
|
||||
url: $dropzone.data('upload-url'),
|
||||
headers: {'X-Csrf-Token': csrfToken},
|
||||
maxFiles: $dropzone.data('max-file'),
|
||||
maxFilesize: $dropzone.data('max-size'),
|
||||
acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
|
||||
addRemoveLinks: true,
|
||||
dictDefaultMessage: $dropzone.data('default-message'),
|
||||
dictInvalidFileType: $dropzone.data('invalid-input-type'),
|
||||
dictFileTooBig: $dropzone.data('file-too-big'),
|
||||
dictRemoveFile: $dropzone.data('remove-file'),
|
||||
timeout: 0,
|
||||
thumbnailMethod: 'contain',
|
||||
thumbnailWidth: 480,
|
||||
thumbnailHeight: 480,
|
||||
init() {
|
||||
this.on('success', (file, data) => {
|
||||
file.uuid = data.uuid;
|
||||
fileUuidDict[file.uuid] = {submitted: false};
|
||||
const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
|
||||
$dropzone.find('.files').append(input);
|
||||
});
|
||||
this.on('removedfile', (file) => {
|
||||
$(`#${file.uuid}`).remove();
|
||||
if ($dropzone.data('remove-url') && !fileUuidDict[file.uuid].submitted) {
|
||||
$.post($dropzone.data('remove-url'), {
|
||||
file: file.uuid,
|
||||
_csrf: csrfToken,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.on('submit', () => {
|
||||
$.each(fileUuidDict, (fileUuid) => {
|
||||
fileUuidDict[fileUuid].submitted = true;
|
||||
});
|
||||
this.on('removedfile', (file) => {
|
||||
$(`#${file.uuid}`).remove();
|
||||
if ($dropzone.data('remove-url') && !fileUuidDict[file.uuid].submitted) {
|
||||
$.post($dropzone.data('remove-url'), {
|
||||
file: file.uuid,
|
||||
_csrf: csrfToken,
|
||||
});
|
||||
}
|
||||
});
|
||||
this.on('submit', () => {
|
||||
$.each(fileUuidDict, (fileUuid) => {
|
||||
fileUuidDict[fileUuid].submitted = true;
|
||||
});
|
||||
this.on('reload', () => {
|
||||
$.getJSON($editContentZone.data('attachment-url'), (data) => {
|
||||
dz.removeAllFiles(true);
|
||||
$dropzone.find('.files').empty();
|
||||
$.each(data, function () {
|
||||
const imgSrc = `${$dropzone.data('link-url')}/${this.uuid}`;
|
||||
dz.emit('addedfile', this);
|
||||
dz.emit('thumbnail', this, imgSrc);
|
||||
dz.emit('complete', this);
|
||||
dz.files.push(this);
|
||||
fileUuidDict[this.uuid] = {submitted: true};
|
||||
$dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
|
||||
const input = $(`<input id="${this.uuid}" name="files" type="hidden">`).val(this.uuid);
|
||||
$dropzone.find('.files').append(input);
|
||||
});
|
||||
});
|
||||
this.on('reload', () => {
|
||||
$.getJSON($editContentZone.data('attachment-url'), (data) => {
|
||||
dz.removeAllFiles(true);
|
||||
$dropzone.find('.files').empty();
|
||||
$.each(data, function () {
|
||||
const imgSrc = `${$dropzone.data('link-url')}/${this.uuid}`;
|
||||
dz.emit('addedfile', this);
|
||||
dz.emit('thumbnail', this, imgSrc);
|
||||
dz.emit('complete', this);
|
||||
dz.files.push(this);
|
||||
fileUuidDict[this.uuid] = {submitted: true};
|
||||
$dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
|
||||
const input = $(`<input id="${this.uuid}" name="files" type="hidden">`).val(this.uuid);
|
||||
$dropzone.find('.files').append(input);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
dz.emit('reload');
|
||||
return dz;
|
||||
};
|
||||
|
||||
const cancelAndReset = (dz) => {
|
||||
showElem($renderContent);
|
||||
hideElem($editContentZone);
|
||||
if (dz) {
|
||||
dz.emit('reload');
|
||||
}
|
||||
// Give new write/preview data-tab name to distinguish from others
|
||||
const $editContentForm = $editContentZone.find('.ui.comment.form');
|
||||
const $tabMenu = $editContentForm.find('.tabular.menu');
|
||||
$tabMenu.attr('data-write', $editContentZone.data('write'));
|
||||
$tabMenu.attr('data-preview', $editContentZone.data('preview'));
|
||||
$tabMenu.find('.write.item').attr('data-tab', $editContentZone.data('write'));
|
||||
$tabMenu.find('.preview.item').attr('data-tab', $editContentZone.data('preview'));
|
||||
$editContentForm.find('.write').attr('data-tab', $editContentZone.data('write'));
|
||||
$editContentForm.find('.preview').attr('data-tab', $editContentZone.data('preview'));
|
||||
easyMDE = await createCommentEasyMDE($textarea);
|
||||
};
|
||||
|
||||
initCompMarkupContentPreviewTab($editContentForm);
|
||||
initEasyMDEImagePaste(easyMDE, $dropzone);
|
||||
const saveAndRefresh = (dz, $dropzone) => {
|
||||
showElem($renderContent);
|
||||
hideElem($editContentZone);
|
||||
const $attachments = $dropzone.find('.files').find('[name=files]').map(function () {
|
||||
return $(this).val();
|
||||
}).get();
|
||||
$.post($editContentZone.data('update-url'), {
|
||||
_csrf: csrfToken,
|
||||
content: comboMarkdownEditor.value(),
|
||||
context: $editContentZone.data('context'),
|
||||
files: $attachments,
|
||||
}, (data) => {
|
||||
if (!data.content) {
|
||||
$renderContent.html($('#no-content').html());
|
||||
$rawContent.text('');
|
||||
} else {
|
||||
$renderContent.html(data.content);
|
||||
$rawContent.text(comboMarkdownEditor.value());
|
||||
|
||||
const $saveButton = $editContentZone.find('.save.button');
|
||||
$textarea.on('ce-quick-submit', () => {
|
||||
$saveButton.trigger('click');
|
||||
});
|
||||
|
||||
$editContentZone.find('.cancel.button').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
showElem($renderContent);
|
||||
hideElem($editContentZone);
|
||||
if (dz) {
|
||||
dz.emit('reload');
|
||||
const refIssues = $renderContent.find('p .ref-issue');
|
||||
attachRefIssueContextPopup(refIssues);
|
||||
}
|
||||
});
|
||||
|
||||
$saveButton.on('click', () => {
|
||||
showElem($renderContent);
|
||||
hideElem($editContentZone);
|
||||
const $attachments = $dropzone.find('.files').find('[name=files]').map(function () {
|
||||
return $(this).val();
|
||||
}).get();
|
||||
$.post($editContentZone.data('update-url'), {
|
||||
_csrf: csrfToken,
|
||||
content: $textarea.val(),
|
||||
context: $editContentZone.data('context'),
|
||||
files: $attachments,
|
||||
}, (data) => {
|
||||
if (data.length === 0 || data.content.length === 0) {
|
||||
$renderContent.html($('#no-content').html());
|
||||
$rawContent.text('');
|
||||
} else {
|
||||
$renderContent.html(data.content);
|
||||
$rawContent.text($textarea.val());
|
||||
const refIssues = $renderContent.find('p .ref-issue');
|
||||
attachRefIssueContextPopup(refIssues);
|
||||
}
|
||||
const $content = $segment;
|
||||
if (!$content.find('.dropzone-attachments').length) {
|
||||
if (data.attachments !== '') {
|
||||
$content.append(`<div class="dropzone-attachments"></div>`);
|
||||
$content.find('.dropzone-attachments').replaceWith(data.attachments);
|
||||
}
|
||||
} else if (data.attachments === '') {
|
||||
$content.find('.dropzone-attachments').remove();
|
||||
} else {
|
||||
const $content = $segment;
|
||||
if (!$content.find('.dropzone-attachments').length) {
|
||||
if (data.attachments !== '') {
|
||||
$content.append(`<div class="dropzone-attachments"></div>`);
|
||||
$content.find('.dropzone-attachments').replaceWith(data.attachments);
|
||||
}
|
||||
if (dz) {
|
||||
dz.emit('submit');
|
||||
dz.emit('reload');
|
||||
}
|
||||
initMarkupContent();
|
||||
initCommentContent();
|
||||
});
|
||||
} else if (data.attachments === '') {
|
||||
$content.find('.dropzone-attachments').remove();
|
||||
} else {
|
||||
$content.find('.dropzone-attachments').replaceWith(data.attachments);
|
||||
}
|
||||
if (dz) {
|
||||
dz.emit('submit');
|
||||
dz.emit('reload');
|
||||
}
|
||||
initMarkupContent();
|
||||
initCommentContent();
|
||||
});
|
||||
} else { // use existing form
|
||||
$textarea = $segment.find('textarea');
|
||||
easyMDE = getAttachedEasyMDE($textarea);
|
||||
};
|
||||
|
||||
if (!$editContentZone.html()) {
|
||||
$editContentZone.html($('#issue-comment-editor-template').html());
|
||||
comboMarkdownEditor = await initComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
|
||||
|
||||
const $dropzone = $editContentZone.find('.dropzone');
|
||||
const dz = await setupDropzone($dropzone);
|
||||
$editContentZone.find('.cancel.button').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
cancelAndReset(dz);
|
||||
});
|
||||
$editContentZone.find('.save.button').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
saveAndRefresh(dz, $dropzone);
|
||||
});
|
||||
} else {
|
||||
comboMarkdownEditor = getComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
|
||||
}
|
||||
|
||||
// Show write/preview tab and copy raw content as needed
|
||||
showElem($editContentZone);
|
||||
hideElem($renderContent);
|
||||
if ($textarea.val().length === 0) {
|
||||
$textarea.val($rawContent.text());
|
||||
easyMDE.value($rawContent.text());
|
||||
if (!comboMarkdownEditor.value()) {
|
||||
comboMarkdownEditor.value($rawContent.text());
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
$textarea.focus();
|
||||
easyMDE.codemirror.focus();
|
||||
});
|
||||
comboMarkdownEditor.focus();
|
||||
}
|
||||
|
||||
export function initRepository() {
|
||||
|
@ -575,7 +553,6 @@ export function initRepository() {
|
|||
initRepoIssueCommentDelete();
|
||||
initRepoIssueDependencyDelete();
|
||||
initRepoIssueCodeCommentCancel();
|
||||
initRepoIssueStatusButton();
|
||||
initRepoPullRequestUpdate();
|
||||
initCompReactionSelector();
|
||||
|
||||
|
@ -592,12 +569,6 @@ export function initRepository() {
|
|||
|
||||
const $form = $repoComparePull.find('.pullrequest-form');
|
||||
showElem($form);
|
||||
$form.find('textarea.edit_area').each(function() {
|
||||
const easyMDE = getAttachedEasyMDE($(this));
|
||||
if (easyMDE) {
|
||||
easyMDE.codemirror.refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -614,24 +585,22 @@ function initRepoIssueCommentEdit() {
|
|||
const target = $(this).data('target');
|
||||
const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
|
||||
const content = `> ${quote}\n\n`;
|
||||
let easyMDE;
|
||||
let editor;
|
||||
if ($(this).hasClass('quote-reply-diff')) {
|
||||
const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
|
||||
easyMDE = await handleReply($replyBtn);
|
||||
editor = await handleReply($replyBtn);
|
||||
} else {
|
||||
// for normal issue/comment page
|
||||
easyMDE = getAttachedEasyMDE($('#comment-form .edit_area'));
|
||||
editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
|
||||
}
|
||||
if (easyMDE) {
|
||||
if (easyMDE.value() !== '') {
|
||||
easyMDE.value(`${easyMDE.value()}\n\n${content}`);
|
||||
if (editor) {
|
||||
if (editor.value()) {
|
||||
editor.value(`${editor.value()}\n\n${content}`);
|
||||
} else {
|
||||
easyMDE.value(`${content}`);
|
||||
editor.value(content);
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
easyMDE.codemirror.focus();
|
||||
easyMDE.codemirror.setCursor(easyMDE.codemirror.lineCount(), 0);
|
||||
});
|
||||
editor.focus();
|
||||
editor.moveCursorToEnd();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue