How to get Obsidian Editor inside Modal

At present i had the following code which i was using to render any raw text i have with Markdown format (Read-Only) :

	// Reference to the HTML element where markdown will be rendered
	const previewContainerRef = useRef<HTMLDivElement>(null);
	useEffect(() => {
		if (previewContainerRef.current) {
			// Clear previous content before rendering new markdown
			previewContainerRef.current.innerHTML = '';

			// Use the MarkdownRenderer.render() method
			MarkdownRenderer.render(
				app,
				formatedContent,
				previewContainerRef.current,
				task.filePath,
				container
			);
		}
	}, [newTaskContent]);

I wanted to know is there a similar service like MarkdownRenderer, which will basically render the Obsidian Editor View inside the previewContainerRef div element. In which i can send a text from previos session as well as the user can edit the old content, just like a normal Obisidian Editor View which opens a file. But in this case it will render the raw text, and the user can do all the basic functionalities, such as pasting links, making text bold and pasting images, etc.

I have found one approach which uses CodeMirror. I have created a file to give me the Code Editor as follows :

// /src/components/MarkdownEditor.tsx

import { EditorView, basicSetup } from 'codemirror';
import React, { useEffect, useRef, useState } from 'react';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';

import { indentWithTab } from '@codemirror/commands';
import { keymap } from '@codemirror/view';
import { oneDark } from '@codemirror/theme-one-dark';

class MarkdownEditor {
	private editorView: EditorView | null = null;
	private onChangeCallback: (value: string) => void;

	constructor(onChangeCallback: (value: string) => void) {
		this.onChangeCallback = onChangeCallback;
	}

	initializeEditor(editorContainer: HTMLDivElement, initialValue: string) {
		if (this.editorView) {
			this.editorView.destroy();
		}

		this.editorView = new EditorView({
			doc: initialValue,
			extensions: [
				basicSetup,
				markdown({ base: markdownLanguage }),
				oneDark,
				keymap.of([indentWithTab]), // Correctly wrap keybindings with `keymap.of()`
				EditorView.updateListener.of((update) => {
					if (update.docChanged) {
						const currentValue = this.editorView?.state.doc.toString() || '';
						this.onChangeCallback(currentValue);
					}
				}),
			],
			parent: editorContainer,
		});
	}

	destroyEditor() {
		if (this.editorView) {
			this.editorView.destroy();
			this.editorView = null;
		}
	}

	getEditorValue(): string {
		return this.editorView?.state.doc.toString() || '';
	}

	static extractIndentedLines(content: string): string[] {
		return content
			.split('\n')
			.filter((line) => /^\s+[^- \[]/.test(line)); // lines with indentation not starting with `- [ ]`
	}
}

export default function CodeMirrorEditor({ initialContent, onChange }: { initialContent: string, onChange: (bodyContent: string[]) => void }) {
	const editorRef = useRef<HTMLDivElement>(null);
	const [editorInstance, setEditorInstance] = useState<MarkdownEditor | null>(null);

	useEffect(() => {
		if (editorRef.current) {
			const editor = new MarkdownEditor((value: string) => {
				const indentedLines = MarkdownEditor.extractIndentedLines(value);
				onChange(indentedLines);
			});
			editor.initializeEditor(editorRef.current, initialContent);
			setEditorInstance(editor);

			return () => {
				editor.destroyEditor();
			};
		}
	}, [initialContent, onChange]);

	return <div ref={editorRef} className="markdown-editor"></div>;
}

And i am using this in one of my Modal using following manner :

const EditBodyContent: React.FC<{ container: any, app: App; onSave: (updatedTask: any) => void; onClose: () => void }> = ({ container, app, onSave, onClose}) => {
    const [bodyContent, setBodyContent] = useState();

	let formatedContent = '';
	const newBodyContent: taskItem = {
		...file,
		body: [
			...bodyContent.split('\n'),
		],
	};

	const previewContainerRef = useRef<HTMLDivElement>(null);
	useEffect(() => {
		formatedContent = contentFormatter(newBodyContent);
		}
	}, [newBodyContent]);

	return (
		<div>
			<div>Edit Body</div>
			<div>
				<CodeMirrorEditor
					initialContent={formatedContent}
					onChange={(bodyContent: string[]) => setBodyContent(bodyContent.join('\n'))}
				/>
			<div>
		<div>
     );
};

// Class component extending Modal for Obsidian
export class EditContentModal extends Modal {
	task: any;
	onSave: (updatedContent: any) => void;

	constructor(app: App, onSave: (updatedContent: any) => void) {
		super(app);
		this.app = app;
		this.task = task;
		this.onSave = onSave;
	}

	onOpen() {
		const { contentEl } = this;
		contentEl.empty();

		const container = document.createElement("div");
		contentEl.appendChild(container);

		const root = ReactDOM.createRoot(this.contentEl);

		root.render(<EditBodyContent
			container={container}
			app={this.app}
			onSave={this.onSave}
			onClose={() => this.close()}
		/>);
	}

	onClose() {
		const { contentEl } = this;
		contentEl.empty();
	}
}

Error : But after making the above changes, thats mainly when i am putting the CodeMirrorEditor component in my Modal HTML. My plugin fails to load or build.

Getting the live-preview markdown editor requires some work to grab the correct prototype and set up everything.

Fevol has a typescript implementation based on mgmeyers’ React implementation

1 Like