Refactor markdown editor and use it for milestone description editor (#32688)
Refactor markdown editor to clarify its "preview" behavior and remove jQuery code. Close #15045 --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
parent
2f43536c3e
commit
c9e582c6b6
29 changed files with 147 additions and 116 deletions
|
@ -1,5 +1,7 @@
|
|||
import {applyAreYouSure, initAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.ts';
|
||||
import {queryElems} from '../utils/dom.ts';
|
||||
import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
||||
|
||||
export function initGlobalFormDirtyLeaveConfirm() {
|
||||
initAreYouSure(window.jQuery);
|
||||
|
@ -11,7 +13,7 @@ export function initGlobalFormDirtyLeaveConfirm() {
|
|||
}
|
||||
|
||||
export function initGlobalEnterQuickSubmit() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
document.addEventListener('keydown', (e: KeyboardEvent & {target: HTMLElement}) => {
|
||||
if (e.key !== 'Enter') return;
|
||||
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
|
||||
if (hasCtrlOrMeta && e.target.matches('textarea')) {
|
||||
|
@ -27,3 +29,7 @@ export function initGlobalEnterQuickSubmit() {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function initGlobalComboMarkdownEditor() {
|
||||
queryElems<HTMLElement>(document, '.combo-markdown-editor:not(.custom-init)', (el) => initComboMarkdownEditor(el));
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import '@github/markdown-toolbar-element';
|
||||
import '@github/text-expander-element';
|
||||
import $ from 'jquery';
|
||||
import {attachTribute} from '../tribute.ts';
|
||||
import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.ts';
|
||||
import {
|
||||
|
@ -23,6 +22,8 @@ import {
|
|||
} from './EditorMarkdown.ts';
|
||||
import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts';
|
||||
import {createTippy} from '../../modules/tippy.ts';
|
||||
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||
import type EasyMDE from 'easymde';
|
||||
|
||||
let elementIdCounter = 0;
|
||||
|
||||
|
@ -48,18 +49,23 @@ export function validateTextareaNonEmpty(textarea) {
|
|||
return true;
|
||||
}
|
||||
|
||||
type ComboMarkdownEditorOptions = {
|
||||
editorHeights?: {minHeight?: string, height?: string, maxHeight?: string},
|
||||
easyMDEOptions?: EasyMDE.Options,
|
||||
};
|
||||
|
||||
export class ComboMarkdownEditor {
|
||||
static EventEditorContentChanged = EventEditorContentChanged;
|
||||
static EventUploadStateChanged = EventUploadStateChanged;
|
||||
|
||||
public container : HTMLElement;
|
||||
|
||||
// TODO: use correct types to replace these "any" types
|
||||
options: any;
|
||||
options: ComboMarkdownEditorOptions;
|
||||
|
||||
tabEditor: HTMLElement;
|
||||
tabPreviewer: HTMLElement;
|
||||
|
||||
supportEasyMDE: boolean;
|
||||
easyMDE: any;
|
||||
easyMDEToolbarActions: any;
|
||||
easyMDEToolbarDefault: any;
|
||||
|
@ -71,11 +77,12 @@ export class ComboMarkdownEditor {
|
|||
dropzone: HTMLElement;
|
||||
attachedDropzoneInst: any;
|
||||
|
||||
previewMode: string;
|
||||
previewUrl: string;
|
||||
previewContext: string;
|
||||
previewMode: string;
|
||||
|
||||
constructor(container, options = {}) {
|
||||
constructor(container, options:ComboMarkdownEditorOptions = {}) {
|
||||
if (container._giteaComboMarkdownEditor) throw new Error('ComboMarkdownEditor already initialized');
|
||||
container._giteaComboMarkdownEditor = this;
|
||||
this.options = options;
|
||||
this.container = container;
|
||||
|
@ -99,6 +106,10 @@ export class ComboMarkdownEditor {
|
|||
}
|
||||
|
||||
setupContainer() {
|
||||
this.supportEasyMDE = this.container.getAttribute('data-support-easy-mde') === 'true';
|
||||
this.previewMode = this.container.getAttribute('data-content-mode');
|
||||
this.previewUrl = this.container.getAttribute('data-preview-url');
|
||||
this.previewContext = this.container.getAttribute('data-preview-context');
|
||||
initTextExpander(this.container.querySelector('text-expander'));
|
||||
}
|
||||
|
||||
|
@ -137,12 +148,14 @@ export class ComboMarkdownEditor {
|
|||
monospaceButton.setAttribute('aria-checked', String(enabled));
|
||||
});
|
||||
|
||||
const easymdeButton = this.container.querySelector('.markdown-switch-easymde');
|
||||
easymdeButton.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
this.userPreferredEditor = 'easymde';
|
||||
await this.switchToEasyMDE();
|
||||
});
|
||||
if (this.supportEasyMDE) {
|
||||
const easymdeButton = this.container.querySelector('.markdown-switch-easymde');
|
||||
easymdeButton.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
this.userPreferredEditor = 'easymde';
|
||||
await this.switchToEasyMDE();
|
||||
});
|
||||
}
|
||||
|
||||
this.initMarkdownButtonTableAdd();
|
||||
|
||||
|
@ -187,6 +200,7 @@ export class ComboMarkdownEditor {
|
|||
|
||||
setupTab() {
|
||||
const tabs = this.container.querySelectorAll<HTMLElement>('.tabular.menu > .item');
|
||||
if (!tabs.length) return;
|
||||
|
||||
// Fomantic Tab requires the "data-tab" to be globally unique.
|
||||
// So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
|
||||
|
@ -207,11 +221,8 @@ export class ComboMarkdownEditor {
|
|||
});
|
||||
});
|
||||
|
||||
$(tabs).tab();
|
||||
fomanticQuery(tabs).tab();
|
||||
|
||||
this.previewUrl = this.tabPreviewer.getAttribute('data-preview-url');
|
||||
this.previewContext = this.tabPreviewer.getAttribute('data-preview-context');
|
||||
this.previewMode = this.options.previewMode ?? 'comment';
|
||||
this.tabPreviewer.addEventListener('click', async () => {
|
||||
const formData = new FormData();
|
||||
formData.append('mode', this.previewMode);
|
||||
|
@ -219,7 +230,7 @@ export class ComboMarkdownEditor {
|
|||
formData.append('text', this.value());
|
||||
const response = await POST(this.previewUrl, {data: formData});
|
||||
const data = await response.text();
|
||||
renderPreviewPanelContent($(panelPreviewer), data);
|
||||
renderPreviewPanelContent(panelPreviewer, data);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -284,7 +295,7 @@ export class ComboMarkdownEditor {
|
|||
}
|
||||
|
||||
async switchToUserPreference() {
|
||||
if (this.userPreferredEditor === 'easymde') {
|
||||
if (this.userPreferredEditor === 'easymde' && this.supportEasyMDE) {
|
||||
await this.switchToEasyMDE();
|
||||
} else {
|
||||
this.switchToTextarea();
|
||||
|
@ -304,7 +315,7 @@ export class ComboMarkdownEditor {
|
|||
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 = {
|
||||
const easyMDEOpt: EasyMDE.Options = {
|
||||
autoDownloadFontAwesome: false,
|
||||
element: this.textarea,
|
||||
forceSync: true,
|
||||
|
@ -384,19 +395,20 @@ export class ComboMarkdownEditor {
|
|||
}
|
||||
|
||||
get userPreferredEditor() {
|
||||
return window.localStorage.getItem(`markdown-editor-${this.options.useScene ?? 'default'}`);
|
||||
return window.localStorage.getItem(`markdown-editor-${this.previewMode ?? 'default'}`);
|
||||
}
|
||||
set userPreferredEditor(s) {
|
||||
window.localStorage.setItem(`markdown-editor-${this.options.useScene ?? 'default'}`, s);
|
||||
window.localStorage.setItem(`markdown-editor-${this.previewMode ?? 'default'}`, s);
|
||||
}
|
||||
}
|
||||
|
||||
export function getComboMarkdownEditor(el) {
|
||||
if (el instanceof $) el = el[0];
|
||||
return el?._giteaComboMarkdownEditor;
|
||||
if (!el) return null;
|
||||
if (el.length) el = el[0];
|
||||
return el._giteaComboMarkdownEditor;
|
||||
}
|
||||
|
||||
export async function initComboMarkdownEditor(container: HTMLElement, options = {}) {
|
||||
export async function initComboMarkdownEditor(container: HTMLElement, options:ComboMarkdownEditorOptions = {}) {
|
||||
if (!container) {
|
||||
throw new Error('initComboMarkdownEditor: container is null');
|
||||
}
|
||||
|
|
|
@ -201,10 +201,8 @@ export function initRepoEditor() {
|
|||
})();
|
||||
}
|
||||
|
||||
export function renderPreviewPanelContent($previewPanel, data) {
|
||||
$previewPanel.html(data);
|
||||
export function renderPreviewPanelContent(previewPanel: Element, content: string) {
|
||||
previewPanel.innerHTML = content;
|
||||
initMarkupContent();
|
||||
|
||||
const $refIssues = $previewPanel.find('p .ref-issue');
|
||||
attachRefIssueContextPopup($refIssues);
|
||||
attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue'));
|
||||
}
|
||||
|
|
|
@ -414,11 +414,6 @@ export function initRepoPullRequestReview() {
|
|||
await handleReply(this);
|
||||
});
|
||||
|
||||
const elReviewBox = document.querySelector('.review-box-panel');
|
||||
if (elReviewBox) {
|
||||
initComboMarkdownEditor(elReviewBox.querySelector('.combo-markdown-editor'));
|
||||
}
|
||||
|
||||
// The following part is only for diff views
|
||||
if (!$('.repository.pull.diff').length) return;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {hideElem, showElem} from '../utils/dom.ts';
|
||||
import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
||||
|
||||
export function initRepoRelease() {
|
||||
document.addEventListener('click', (e) => {
|
||||
|
@ -16,7 +15,6 @@ export function initRepoReleaseNew() {
|
|||
if (!document.querySelector('.repository.new.release')) return;
|
||||
|
||||
initTagNameEditor();
|
||||
initRepoReleaseEditor();
|
||||
}
|
||||
|
||||
function initTagNameEditor() {
|
||||
|
@ -48,11 +46,3 @@ function initTagNameEditor() {
|
|||
hideTargetInput(e.target);
|
||||
});
|
||||
}
|
||||
|
||||
function initRepoReleaseEditor() {
|
||||
const editor = document.querySelector<HTMLElement>('.repository.new.release .combo-markdown-editor');
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
initComboMarkdownEditor(editor);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import {initMarkupContent} from '../markup/content.ts';
|
|||
import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
||||
import {fomanticMobileScreen} from '../modules/fomantic.ts';
|
||||
import {POST} from '../modules/fetch.ts';
|
||||
import type {ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
|
||||
|
||||
async function initRepoWikiFormEditor() {
|
||||
const editArea = document.querySelector<HTMLTextAreaElement>('.repository.wiki .combo-markdown-editor textarea');
|
||||
|
@ -9,7 +10,7 @@ async function initRepoWikiFormEditor() {
|
|||
|
||||
const form = document.querySelector('.repository.wiki.new .ui.form');
|
||||
const editorContainer = form.querySelector<HTMLElement>('.combo-markdown-editor');
|
||||
let editor;
|
||||
let editor: ComboMarkdownEditor;
|
||||
|
||||
let renderRequesting = false;
|
||||
let lastContent;
|
||||
|
@ -45,12 +46,10 @@ async function initRepoWikiFormEditor() {
|
|||
renderEasyMDEPreview();
|
||||
|
||||
editor = await initComboMarkdownEditor(editorContainer, {
|
||||
useScene: 'wiki',
|
||||
// EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it.
|
||||
// And another benefit is that we only need to write the style once for both editors.
|
||||
// TODO: Move height style to CSS after EasyMDE removal.
|
||||
editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'},
|
||||
previewMode: 'wiki',
|
||||
easyMDEOptions: {
|
||||
previewRender: (_content, previewTarget) => previewTarget.innerHTML, // disable builtin preview render
|
||||
toolbar: ['bold', 'italic', 'strikethrough', '|',
|
||||
|
@ -59,7 +58,7 @@ async function initRepoWikiFormEditor() {
|
|||
'unordered-list', 'ordered-list', '|',
|
||||
'link', 'image', 'table', 'horizontal-rule', '|',
|
||||
'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea',
|
||||
],
|
||||
] as any, // to use custom toolbar buttons
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -83,7 +83,11 @@ import {
|
|||
initGlobalButtons,
|
||||
initGlobalDeleteButton,
|
||||
} from './features/common-button.ts';
|
||||
import {initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts';
|
||||
import {
|
||||
initGlobalComboMarkdownEditor,
|
||||
initGlobalEnterQuickSubmit,
|
||||
initGlobalFormDirtyLeaveConfirm,
|
||||
} from './features/common-form.ts';
|
||||
|
||||
initGiteaFomantic();
|
||||
initDirAuto();
|
||||
|
@ -127,6 +131,7 @@ onDomReady(() => {
|
|||
initGlobalCopyToClipboardListener,
|
||||
initGlobalEnterQuickSubmit,
|
||||
initGlobalFormDirtyLeaveConfirm,
|
||||
initGlobalComboMarkdownEditor,
|
||||
initGlobalDeleteButton,
|
||||
|
||||
initCommonOrganization,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue