From: Dan Brown Date: Tue, 22 Jul 2025 09:34:29 +0000 (+0100) Subject: MD Editor: Added plaintext/cm switching X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/d55db06c01302c8a2e597c91620fbfb8ddfc6982?ds=inline MD Editor: Added plaintext/cm switching Also aligned the construction of the inputs where possible. --- diff --git a/resources/js/markdown/codemirror.ts b/resources/js/markdown/codemirror.ts index 82aeb1141..1ae018477 100644 --- a/resources/js/markdown/codemirror.ts +++ b/resources/js/markdown/codemirror.ts @@ -1,25 +1,48 @@ -import {provideKeyBindings} from './shortcuts'; -import {EditorView, ViewUpdate} from "@codemirror/view"; -import {MarkdownEditor} from "./index.mjs"; +import {EditorView, KeyBinding, ViewUpdate} from "@codemirror/view"; import {CodeModule} from "../global"; import {MarkdownEditorEventMap} from "./dom-handlers"; +import {MarkdownEditorShortcutMap} from "./shortcuts"; + +/** + * Convert editor shortcuts to CodeMirror keybinding format. + */ +export function shortcutsToKeyBindings(shortcuts: MarkdownEditorShortcutMap): KeyBinding[] { + const keyBindings = []; + + const wrapAction = (action: () => void) => () => { + action(); + return true; + }; + + for (const [shortcut, action] of Object.entries(shortcuts)) { + keyBindings.push({key: shortcut, run: wrapAction(action), preventDefault: true}); + } + + return keyBindings; +} /** * Initiate the codemirror instance for the Markdown editor. */ -export function init(editor: MarkdownEditor, Code: CodeModule, domEventHandlers: MarkdownEditorEventMap): EditorView { +export async function init( + input: HTMLTextAreaElement, + shortcuts: MarkdownEditorShortcutMap, + domEventHandlers: MarkdownEditorEventMap, + onChange: () => void +): Promise { + const Code = await window.importVersioned('code') as CodeModule; + function onViewUpdate(v: ViewUpdate) { if (v.docChanged) { - editor.actions.updateAndRender(); + onChange(); } } - const cm = Code.markdownEditor( - editor.config.inputEl, + input, onViewUpdate, domEventHandlers, - provideKeyBindings(editor), + shortcutsToKeyBindings(shortcuts), ); // Add editor view to the window for easy access/debugging. diff --git a/resources/js/markdown/index.mts b/resources/js/markdown/index.mts index 7edf80d4f..4cd89c077 100644 --- a/resources/js/markdown/index.mts +++ b/resources/js/markdown/index.mts @@ -4,7 +4,6 @@ import {Actions} from './actions'; import {Settings} from './settings'; import {listenToCommonEvents} from './common-events'; import {init as initCodemirror} from './codemirror'; -import {CodeModule} from "../global"; import {MarkdownEditorInput} from "./inputs/interface"; import {CodemirrorInput} from "./inputs/codemirror"; import {TextareaInput} from "./inputs/textarea"; @@ -34,8 +33,6 @@ export interface MarkdownEditor { * Initiate a new Markdown editor instance. */ export async function init(config: MarkdownEditorConfig): Promise { - // const Code = await window.importVersioned('code') as CodeModule; - const editor: MarkdownEditor = { config, markdown: new Markdown(), @@ -46,15 +43,25 @@ export async function init(config: MarkdownEditorConfig): Promise editor.actions.updateAndRender(); + + const initCodemirrorInput: () => Promise = async () => { + const codeMirror = await initCodemirror(config.inputEl, shortcuts, eventHandlers, onInputChange); + return new CodemirrorInput(codeMirror); + }; + const initTextAreaInput: () => Promise = async () => { + return new TextareaInput(config.inputEl, shortcuts, eventHandlers, onInputChange); + }; + const isPlainEditor = Boolean(editor.settings.get('plainEditor')); + editor.input = await (isPlainEditor ? initTextAreaInput() : initCodemirrorInput()); + editor.settings.onChange('plainEditor', async (value) => { + const isPlain = Boolean(value); + const newInput = await (isPlain ? initTextAreaInput() : initCodemirrorInput()); + editor.input.teardown(); + editor.input = newInput; + }); // window.devinput = editor.input; listenToCommonEvents(editor); diff --git a/resources/js/markdown/inputs/codemirror.ts b/resources/js/markdown/inputs/codemirror.ts index 029d238fe..3ab219a63 100644 --- a/resources/js/markdown/inputs/codemirror.ts +++ b/resources/js/markdown/inputs/codemirror.ts @@ -10,6 +10,10 @@ export class CodemirrorInput implements MarkdownEditorInput { this.cm = cm; } + teardown(): void { + this.cm.destroy(); + } + focus(): void { if (!this.cm.hasFocus) { this.cm.focus(); diff --git a/resources/js/markdown/inputs/interface.ts b/resources/js/markdown/inputs/interface.ts index c0397ecd0..66a8c07e7 100644 --- a/resources/js/markdown/inputs/interface.ts +++ b/resources/js/markdown/inputs/interface.ts @@ -73,4 +73,9 @@ export interface MarkdownEditorInput { * Search and return a line range which includes the provided text. */ searchForLineContaining(text: string): MarkdownEditorInputSelection|null; + + /** + * Tear down the input. + */ + teardown(): void; } \ No newline at end of file diff --git a/resources/js/markdown/inputs/textarea.ts b/resources/js/markdown/inputs/textarea.ts index d1eabd270..25c8779fc 100644 --- a/resources/js/markdown/inputs/textarea.ts +++ b/resources/js/markdown/inputs/textarea.ts @@ -8,23 +8,43 @@ export class TextareaInput implements MarkdownEditorInput { protected input: HTMLTextAreaElement; protected shortcuts: MarkdownEditorShortcutMap; protected events: MarkdownEditorEventMap; - - constructor(input: HTMLTextAreaElement, shortcuts: MarkdownEditorShortcutMap, events: MarkdownEditorEventMap) { + protected onChange: () => void; + protected eventController = new AbortController(); + + constructor( + input: HTMLTextAreaElement, + shortcuts: MarkdownEditorShortcutMap, + events: MarkdownEditorEventMap, + onChange: () => void + ) { this.input = input; this.shortcuts = shortcuts; this.events = events; + this.onChange = onChange; this.onKeyDown = this.onKeyDown.bind(this); this.configureListeners(); + + this.input.style.removeProperty("display"); + } + + teardown() { + this.eventController.abort('teardown'); } configureListeners(): void { - // TODO - Teardown handling - this.input.addEventListener('keydown', this.onKeyDown); + // Keyboard shortcuts + this.input.addEventListener('keydown', this.onKeyDown, {signal: this.eventController.signal}); + // Shared event listeners for (const [name, listener] of Object.entries(this.events)) { - this.input.addEventListener(name, listener); + this.input.addEventListener(name, listener, {signal: this.eventController.signal}); } + + // Input change handling + this.input.addEventListener('input', () => { + this.onChange(); + }, {signal: this.eventController.signal}); } onKeyDown(e: KeyboardEvent) { diff --git a/resources/js/markdown/shortcuts.ts b/resources/js/markdown/shortcuts.ts index 734160f29..175e8f4f0 100644 --- a/resources/js/markdown/shortcuts.ts +++ b/resources/js/markdown/shortcuts.ts @@ -1,5 +1,4 @@ import {MarkdownEditor} from "./index.mjs"; -import {KeyBinding} from "@codemirror/view"; export type MarkdownEditorShortcutMap = Record void>; @@ -42,22 +41,3 @@ export function provideShortcutMap(editor: MarkdownEditor): MarkdownEditorShortc return shortcuts; } - -/** - * Get the editor shortcuts in CodeMirror keybinding format. - */ -export function provideKeyBindings(editor: MarkdownEditor): KeyBinding[] { - const shortcuts = provideShortcutMap(editor); - const keyBindings = []; - - const wrapAction = (action: ()=>void) => () => { - action(); - return true; - }; - - for (const [shortcut, action] of Object.entries(shortcuts)) { - keyBindings.push({key: shortcut, run: wrapAction(action), preventDefault: true}); - } - - return keyBindings; -}