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:
wxiaoguang 2024-12-04 10:11:34 +08:00 committed by GitHub
parent 2f43536c3e
commit c9e582c6b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 147 additions and 116 deletions

View file

@ -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');
}