markdown-text-editor
Version:
A simple JavaScript Markdown editor plugin with real-time preview, and easy integration.
172 lines (141 loc) • 5.61 kB
JavaScript
// #markdown\editor.js
import './styles/main.css';
import { marked } from 'marked';
import Toolbar from './Toolbar/index.js';
import Preview from './preview.js';
marked.setOptions({
breaks: true
});
class MarkdownEditor {
constructor(selector, options = {}) {
this.usertextarea = typeof selector === 'string' ? document.querySelector(selector) : selector;
this.options = options;
this.preview = this.options.toolbar.includes('preview');
this.init();
}
init() {
this.createEditor();
if (this.options.toolbar) this.addToolbar();
}
createEditor() {
if (!this.isTextareaValid()) return;
this.applyDefaultAttributes();
this.buildEditorLayout();
this.addInputListener();
this.render();
}
isTextareaValid() {
return this.usertextarea.tagName === 'TEXTAREA';
}
applyDefaultAttributes() {
this.usertextarea.classList.add(
"focus:ring-0",
"focus:outline-0",
"border-0",
"p-1.5",
"max-w-full",
"size-full",
"bg-transparent",
"outline-0",
"appearance-none",
"prose",
"prose-sm",
"md:prose-base",
"dark:prose-invert",
"text-stone-700",
"dark:text-stone-200",
"overflow-y-auto",
"placeholder:text-stone-300",
"dark:placeholder:text-stone-700"
);
if (!this.usertextarea.hasAttribute('placeholder')) {
this.usertextarea.placeholder = this.options.placeholder || 'Write your markdown...';
}
}
buildEditorLayout() {
this.editorContainer = document.createElement('div');
this.editorContainer.className = `
markdown-editor-wrapper
border border-stone-200
dark:border-stone-700
rounded-md
overflow-hidden
`;
this.usertextarea.parentNode.insertBefore(this.editorContainer, this.usertextarea);
this.markdownEditorDiv = document.createElement('div');
this.markdownEditorDiv.className = `editor-layout`;
this.editorContainer.appendChild(this.markdownEditorDiv);
this.addTextareaWrapper();
if (this.preview) this.addPreviewWrapper();
}
addTextareaWrapper() {
const textareaContainer = document.createElement('div');
textareaContainer.className = `
textarea-wrapper
p-2
bg-white
dark:bg-stone-800
grid
after:px-3.5
after:py-2.5
after:text-inherit
[&>textarea]:resize-none
[&>textarea]:[grid-area:1/1/2/2]
after:[grid-area:1/1/2/2]
after:whitespace-pre-wrap
after:invisible
after:content-[attr(data-cloned-val)_'_']
after:border
`;
textareaContainer.appendChild(this.usertextarea);
this.markdownEditorDiv.appendChild(textareaContainer);
}
addPreviewWrapper() {
const preview = new Preview(this.markdownEditorDiv);
this.previewContent = preview.getPreviewContent();
}
addInputListener() {
this.usertextarea.addEventListener('input', () => this.render());
this.usertextarea.addEventListener('scroll', () => {
const textarea = this.usertextarea;
const previewPane = this.previewContent;
// Calculate the proportion of the textarea that has been scrolled
const textareaScrollRatio = textarea.scrollTop / (textarea.scrollHeight - textarea.clientHeight);
// Apply the same scroll ratio to the preview pane
previewPane.scrollTop = textareaScrollRatio * (previewPane.scrollHeight - previewPane.clientHeight);
});
}
addToolbar() {
new Toolbar(this, this.options.toolbar || ['bold', 'italic', 'strikethrough']);
}
insertText(text) {
const { selectionStart, selectionEnd } = this.usertextarea;
const value = this.usertextarea.value;
this.usertextarea.value = `${value.substring(0, selectionStart)}${text}${value.substring(selectionEnd)}`;
this.usertextarea.focus();
this.usertextarea.setSelectionRange(selectionStart, selectionStart + text.length);
// Scroll the textarea to the inserted text
this.scrollToView();
this.render();
}
scrollToView() {
const textarea = this.usertextarea;
// Calculate the position of the inserted text
const selectionStart = textarea.selectionStart;
// Get the line height (height of each row of text)
const lineHeight = parseInt(window.getComputedStyle(textarea).lineHeight);
// Get how many rows fit into the visible area of the textarea
const rowsInView = Math.floor(textarea.clientHeight / lineHeight);
// Calculate the current line number of the selectionStart
const currentLine = Math.floor(selectionStart / textarea.cols);
// Scroll to the line number that places the inserted text in the center
const targetScrollTop = (currentLine - Math.floor(rowsInView / 2)) * lineHeight;
// Adjust scrollTop to center the cursor's line in the view
textarea.scrollTop = targetScrollTop;
}
render() {
const html = marked(this.usertextarea.value);
this.previewContent.innerHTML = html;
}
}
export default MarkdownEditor;