@sertxudeveloper/markdown-editor
Version:
A customizable markdown editor for your projects
318 lines (243 loc) • 9.47 kB
text/typescript
import Bold from './plugins/bold/Bold';
import Italic from './plugins/italic/Italic';
import Quote from './plugins/quote/Quote';
import Plugin from './plugins/Plugin';
import Preview from './Preview';
import Link from './plugins/link/Link';
import Image from './plugins/image/Image';
import OrderedList from './plugins/ordered-list/OrderedList';
import UnorderedList from './plugins/unordered-list/UnorderedList';
import Strike from './plugins/strike/Strike';
import Underline from './plugins/underline/Underline';
import Mark from './plugins/mark/Mark';
import Mentions, { MentionFeed } from './plugins/mentions/Mentions';
export type EditorConfig = {
key?: string;
plugins?: any[];
placeholder?: string;
mentions?: MentionFeed[];
imageBrowserUrl?: string;
uploadAccept?: string;
hasAttachments?: boolean;
};
type EditorDefaultConfig = EditorConfig & {
builtinPlugins?: any[];
};
const defaultConfig: EditorDefaultConfig = {
key: 'editor',
placeholder: 'Start writing...',
builtinPlugins: [
Bold,
Italic,
Strike,
Underline,
Quote,
Link,
Image,
UnorderedList,
OrderedList,
Mark,
Mentions,
],
mentions: [],
plugins: [],
imageBrowserUrl: '',
uploadAccept:
'.gif,.jpeg,.jpg,.mov,.mp4,.png,.svg,.webm,.csv,.docx,.fodg,.fodp,.fods,.fodt,.gz,.log,.md,.odf,.odg,.odp,.ods,.odt,.pdf,.pptx,.tgz,.txt,.xls,.xlsx,.zip',
hasAttachments: false,
};
/**
* Markdown Editor class
*/
export default class Editor {
sourceElement?: HTMLElement | undefined;
config: EditorDefaultConfig = {};
private eventCallbacks: object[] = [];
private plugins: Plugin[] = [];
textarea?: HTMLTextAreaElement | undefined;
preview?: HTMLDivElement | undefined;
/** Get an HTMLElement or an element query selector */
constructor(element: string | HTMLElement, config: any = {}) {
if (typeof element === 'string') {
this.sourceElement = document.querySelector(element);
} else {
this.sourceElement = element;
}
if (!this.sourceElement) {
throw new Error('[MarkdownEditor]: No element found');
}
/** Merge config */
this.config = Object.assign(defaultConfig, config);
this.init();
this.changeMode('write');
}
/** Initialize plugins */
initPlugins() {
const plugins = [...this.config.builtinPlugins, ...this.config.plugins];
for (const plugin in plugins) {
this.plugins.push(new plugins[plugin](this));
}
}
/** Initialize the editor */
private init() {
this.sourceElement.classList.add('markdown-editor-container');
let initialValue = this.sourceElement.innerHTML
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/<br>/g, '\n');
this.sourceElement.innerHTML = '';
let editor = document.createElement('div');
editor.classList.add('markdown-editor');
if (this.config.hasAttachments) {
editor.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
const files = e.dataTransfer.files;
if (files.length === 0) return;
this.onAttachmentsChange(...files);
});
editor.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
}
let writeContainer = document.createElement('div');
writeContainer.classList.add('markdown-editor-write');
this.textarea = document.createElement('textarea');
this.textarea.name = this.config.key;
this.textarea.id = this.config.key;
this.textarea.value = initialValue;
this.textarea.placeholder = this.config.placeholder;
this.textarea.addEventListener('input', this.autoresize.bind(this));
this.textarea.addEventListener('input', this.updatePreview.bind(this));
this.textarea.addEventListener('input', this.onValueChange.bind(this));
if (this.config.hasAttachments) {
this.textarea.addEventListener('paste', this.onPaste.bind(this));
}
writeContainer.appendChild(this.textarea);
if (this.config.hasAttachments) {
let fileInputContainer = document.createElement('label');
fileInputContainer.classList.add('markdown-editor-file-input');
let labelContent = document.createElement('span');
labelContent.id = this.config.key + '-label-content';
labelContent.innerHTML =
'Attach files by dragging & dropping, selecting or pasting them.';
fileInputContainer.appendChild(labelContent);
let fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.multiple = true;
fileInput.accept = this.config.uploadAccept;
fileInput.addEventListener('change', (e: any) => {
const files = e.target.files;
if (files.length === 0) return;
this.onAttachmentsChange(...files);
});
fileInputContainer.appendChild(fileInput);
writeContainer.appendChild(fileInputContainer);
}
editor.appendChild(writeContainer);
this.preview = document.createElement('div');
this.preview.classList.add('markdown-editor-preview');
editor.appendChild(this.preview);
this.initPlugins();
let toolbar = this.initToolbar();
this.sourceElement.appendChild(toolbar);
this.sourceElement.appendChild(editor);
this.autoresize();
this.updatePreview();
}
/** Autorezise the textarea */
private autoresize() {
this.textarea.style.height = 'auto';
this.textarea.style.height = this.textarea.scrollHeight + 'px';
}
private updatePreview() {
this.preview.innerHTML = Preview.renderMarkdown(this.textarea.value);
}
/** Initialize the toolbar */
private initToolbar(): HTMLElement {
let toolbar = document.createElement('div');
toolbar.classList.add('toolbar');
// Add mode buttons container
let modeButtons = document.createElement('div');
modeButtons.classList.add('mode-buttons');
// Add write and preview buttons
for (const mode of ['write', 'preview']) {
let button = document.createElement('div');
button.setAttribute('role', 'button');
button.classList.add(`${mode}-mode-button`);
button.onclick = () => this.changeMode(mode);
button.innerText = mode;
modeButtons.appendChild(button);
}
toolbar.appendChild(modeButtons);
// Add style buttons container
let styleButtons = document.createElement('div');
styleButtons.classList.add('style-buttons');
// Add style buttons
for (const plugin of this.plugins) {
let icon = plugin.getIcon();
if (!icon) continue;
let button = document.createElement('div');
button.setAttribute('role', 'button');
button.addEventListener('click', plugin.execute.bind(plugin));
button.innerHTML = icon;
styleButtons.appendChild(button);
}
toolbar.appendChild(styleButtons);
return toolbar;
}
/** Change the editor mode */
private changeMode(mode: string) {
if (mode === 'write') {
this.sourceElement.classList.add('markdown-write-mode');
this.sourceElement.classList.remove('markdown-preview-mode');
} else {
this.sourceElement.classList.add('markdown-preview-mode');
this.sourceElement.classList.remove('markdown-write-mode');
}
}
/** Custom event listener */
on(command: string, callback: Function) {
this.eventCallbacks[command] = callback;
}
/** Execute a command */
execute(command: string, value: string = '') {
// Find the plugin with the given command
let plugin = this.plugins.find((plugin) => plugin.getKey() === command);
if (!plugin) return;
plugin.execute(value);
}
onValueChange() {
if (this.eventCallbacks['change'] && typeof this.eventCallbacks['change'] === 'function') {
this.eventCallbacks['change']();
}
}
onPaste(event: ClipboardEvent) {
const items = event.clipboardData.items;
if (items.length === 0) return;
for (const item of items) {
if (item.kind !== 'file') continue;
const file = item.getAsFile();
if (!file) continue;
this.onAttachmentsChange(file);
}
}
onAttachmentsChange(...files: File[]) {
if (
this.eventCallbacks['attachments'] &&
typeof this.eventCallbacks['attachments'] === 'function'
) {
this.eventCallbacks['attachments'](files);
}
}
getValue() {
return this.textarea?.value || '';
}
setValue(value: string) {
if (this.textarea) {
this.textarea.value = value;
this.textarea.dispatchEvent(new InputEvent('input'));
}
}
}