You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
easy-markdown-editor/src/easymde.ts

242 lines
7.0 KiB
TypeScript

/* eslint-disable sort-keys,@typescript-eslint/member-ordering,max-classes-per-file */
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
// import { languages } from "@codemirror/language-data"; // Costs 800KB, probably should be a manual plugin
import {
HighlightStyle,
defaultHighlightStyle,
syntaxHighlighting,
// HighlightStyle,
// tags
} from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import { drawSelection, EditorView } from '@codemirror/view';
import { tags } from '@lezer/highlight';
import { marked } from 'marked';
import { InputOptions, Options } from './options';
import './styles.scss';
import { importDefaultToolbar, importToolbar } from '.';
class NotConstructedError extends Error {
public constructor() {
super(
'EasyMDE is not initialized, run the "construct()" method to do so.',
);
this.name = 'NotConstructedError';
}
}
class AlreadyConstructedError extends Error {
public constructor() {
super('EasyMDE is already initialized.');
this.name = 'AlreadyConstructedError';
}
}
export class EasyMDE {
private readonly element: HTMLTextAreaElement;
private _container?: HTMLDivElement;
private _codemirror?: EditorView;
// private rendered = false;
private readonly _options: Options;
private readonly plugins: IEasyMDEPlugin[] = [];
public constructor(options: InputOptions) {
this._options = {
...options,
blockStyles: {
bold: '**',
italic: '*',
strikethrough: '~~',
code: '`',
},
};
this.element = EasyMDE.verifyAndReturnElement(options.element);
marked.parse('# EasyMDE');
this.construct();
}
public get container(): HTMLDivElement {
if (!this._container) {
throw new NotConstructedError();
}
return this._container;
}
public get codemirror(): EditorView {
if (!this._codemirror) {
throw new NotConstructedError();
}
return this._codemirror;
}
public get options(): Readonly<Options> {
return Object.freeze(this._options);
}
private static verifyAndReturnElement(
element?: HTMLElement,
): HTMLTextAreaElement {
if (!element) {
throw new Error('EasyMDE: Parameter "element" is null.');
}
if (!(element instanceof HTMLTextAreaElement)) {
throw new TypeError(
'EasyMDE: Parameter "element" must be a TextArea.',
);
}
return element;
}
public get isRendered(): boolean {
return Boolean(this.container && this.codemirror);
}
public async construct(): Promise<void> {
if (this._container && this._codemirror) {
throw new AlreadyConstructedError();
}
// Customize the markdown highlight style.
const highlightStyle = HighlightStyle.define([
{
tag: tags.heading1,
fontSize: '200%',
lineHeight: '200%',
textDecoration: 'none',
},
{
tag: tags.heading2,
fontSize: '160%',
lineHeight: '160%',
textDecoration: 'none',
},
{
tag: tags.heading3,
fontSize: '125%',
lineHeight: '125%',
textDecoration: 'none',
},
{
tag: tags.heading4,
fontSize: '110%',
lineHeight: '110%',
textDecoration: 'none',
},
{
tag: tags.heading5,
fontSize: '105%',
lineHeight: '105%',
textDecoration: 'none',
},
{
tag: tags.heading6,
fontSize: '100%',
lineHeight: '100%',
textDecoration: 'none',
},
{
tag: tags.monospace,
fontFamily: 'monospace',
textDecoration: 'none',
background: 'rgba(0, 0, 0, 0.05)',
},
]);
this.element.hidden = true;
this._codemirror = new EditorView({
state: EditorState.create({
doc: this.element.value,
extensions: [
syntaxHighlighting(highlightStyle),
syntaxHighlighting(defaultHighlightStyle),
markdown({
base: markdownLanguage,
// codeLanguages: languages,
}),
drawSelection(),
],
selection: {
anchor: this.element.value.length,
},
}),
// parent: this.element.parentElement || document.body,
});
const easyMDEContainer = this.createContainer();
if (this.options.toolbar !== false) {
easyMDEContainer.append(await this.createToolbar());
}
easyMDEContainer.append(this.codemirror.dom);
if (this.options.statusbar !== false) {
easyMDEContainer.append(await this.createStatusBar());
}
this.element.insertAdjacentElement('afterend', easyMDEContainer);
this.codemirror.focus();
this._container = easyMDEContainer;
}
public destruct(): void {
this.element.value = this.codemirror.state.doc.toString();
for (const plugin of this.plugins) {
plugin.destroy();
}
this.codemirror.destroy();
this.container.remove();
this._container = undefined;
this._codemirror = undefined;
this.element.hidden = false;
}
public async addPlugin(plugin: IEasyMDEPlugin): Promise<IEasyMDEPlugin> {
this.plugins.push(plugin);
return plugin;
}
private async createToolbar(): Promise<HTMLDivElement> {
const [{ Toolbar }, { defaultToolbar }] = await Promise.all([
importToolbar(),
importDefaultToolbar(),
]);
const toolbar = new Toolbar(this, defaultToolbar);
await this.addPlugin(toolbar);
// await toolbar.build(defaultToolbar);
return toolbar.element;
}
private async createStatusBar(): Promise<HTMLDivElement> {
const { StatusBar } = await import('./status-bar/status-bar');
const statusBar = new StatusBar(this);
return statusBar.element;
}
private createContainer(): HTMLDivElement {
const container = document.createElement('div');
container.classList.add('easymde-container');
return container;
}
}
export type IEasyMDEPluginClass = new (easyMDE: EasyMDE) => IEasyMDEPlugin;
export interface IEasyMDEPlugin {
// new (editor: EasyMDE, ...args: any): IEasyMDEPlugin;
build(arguments_: any): Promise<void>;
destroy(): Promise<void>;
}