]> BookStack Code Mirror - bookstack/commitdiff
MD Editor: Added plaintext/cm switching
authorDan Brown <redacted>
Tue, 22 Jul 2025 09:34:29 +0000 (10:34 +0100)
committerDan Brown <redacted>
Tue, 22 Jul 2025 09:34:29 +0000 (10:34 +0100)
Also aligned the construction of the inputs where possible.

resources/js/markdown/codemirror.ts
resources/js/markdown/index.mts
resources/js/markdown/inputs/codemirror.ts
resources/js/markdown/inputs/interface.ts
resources/js/markdown/inputs/textarea.ts
resources/js/markdown/shortcuts.ts

index 82aeb11418f3c3141e947e6ae25227d0c1c42488..1ae018477047619219f651031cff82f5419f6a8d 100644 (file)
@@ -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 {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.
  */
 
 /**
  * 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<EditorView> {
+    const Code = await window.importVersioned('code') as CodeModule;
+
     function onViewUpdate(v: ViewUpdate) {
         if (v.docChanged) {
     function onViewUpdate(v: ViewUpdate) {
         if (v.docChanged) {
-            editor.actions.updateAndRender();
+            onChange();
         }
     }
 
         }
     }
 
-
     const cm = Code.markdownEditor(
     const cm = Code.markdownEditor(
-        editor.config.inputEl,
+        input,
         onViewUpdate,
         domEventHandlers,
         onViewUpdate,
         domEventHandlers,
-        provideKeyBindings(editor),
+        shortcutsToKeyBindings(shortcuts),
     );
 
     // Add editor view to the window for easy access/debugging.
     );
 
     // Add editor view to the window for easy access/debugging.
index 7edf80d4fb401dfa3caf9816437557fde972053f..4cd89c0777f090aa31f2d3cb34736c8c9952c5d1 100644 (file)
@@ -4,7 +4,6 @@ import {Actions} from './actions';
 import {Settings} from './settings';
 import {listenToCommonEvents} from './common-events';
 import {init as initCodemirror} from './codemirror';
 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";
 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<MarkdownEditor> {
  * Initiate a new Markdown editor instance.
  */
 export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor> {
-    // const Code = await window.importVersioned('code') as CodeModule;
-
     const editor: MarkdownEditor = {
         config,
         markdown: new Markdown(),
     const editor: MarkdownEditor = {
         config,
         markdown: new Markdown(),
@@ -46,15 +43,25 @@ export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor
     editor.display = new Display(editor);
 
     const eventHandlers = getMarkdownDomEventHandlers(editor);
     editor.display = new Display(editor);
 
     const eventHandlers = getMarkdownDomEventHandlers(editor);
-    // TODO - Switching
-    // const codeMirror = initCodemirror(editor, Code);
-    // editor.input = new CodemirrorInput(codeMirror);
-    editor.input = new TextareaInput(
-        config.inputEl,
-        provideShortcutMap(editor),
-        eventHandlers
-    );
+    const shortcuts = provideShortcutMap(editor);
+    const onInputChange = () => editor.actions.updateAndRender();
+
+    const initCodemirrorInput: () => Promise<MarkdownEditorInput> = async () => {
+        const codeMirror = await initCodemirror(config.inputEl, shortcuts, eventHandlers, onInputChange);
+        return new CodemirrorInput(codeMirror);
+    };
+    const initTextAreaInput: () => Promise<MarkdownEditorInput> = 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);
     // window.devinput = editor.input;
 
     listenToCommonEvents(editor);
index 029d238fe85a19d2f558e21d5c9a77da30aefb4c..3ab219a6330e9cd6ee07c32b559b9940d122243a 100644 (file)
@@ -10,6 +10,10 @@ export class CodemirrorInput implements MarkdownEditorInput {
         this.cm = cm;
     }
 
         this.cm = cm;
     }
 
+    teardown(): void {
+        this.cm.destroy();
+    }
+
     focus(): void {
         if (!this.cm.hasFocus) {
             this.cm.focus();
     focus(): void {
         if (!this.cm.hasFocus) {
             this.cm.focus();
index c0397ecd09d8875ffff3c843ba17aa363996848b..66a8c07e7980c844f2f64bab7f595277c466e8cc 100644 (file)
@@ -73,4 +73,9 @@ export interface MarkdownEditorInput {
      * Search and return a line range which includes the provided text.
      */
     searchForLineContaining(text: string): MarkdownEditorInputSelection|null;
      * 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
 }
\ No newline at end of file
index d1eabd27027c54f5f3e5b7b863062aa4addb3f68..25c8779fc7350f063dbdc159e44a64e88822080b 100644 (file)
@@ -8,23 +8,43 @@ export class TextareaInput implements MarkdownEditorInput {
     protected input: HTMLTextAreaElement;
     protected shortcuts: MarkdownEditorShortcutMap;
     protected events: MarkdownEditorEventMap;
     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.input = input;
         this.shortcuts = shortcuts;
         this.events = events;
+        this.onChange = onChange;
 
         this.onKeyDown = this.onKeyDown.bind(this);
         this.configureListeners();
 
         this.onKeyDown = this.onKeyDown.bind(this);
         this.configureListeners();
+
+        this.input.style.removeProperty("display");
+    }
+
+    teardown() {
+        this.eventController.abort('teardown');
     }
 
     configureListeners(): void {
     }
 
     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)) {
         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) {
     }
 
     onKeyDown(e: KeyboardEvent) {
index 734160f29f0353de95140bdc2ada6f6d32ca5b41..175e8f4f04bea7802eb20bc7b9772fdfc0e1e05a 100644 (file)
@@ -1,5 +1,4 @@
 import {MarkdownEditor} from "./index.mjs";
 import {MarkdownEditor} from "./index.mjs";
-import {KeyBinding} from "@codemirror/view";
 
 export type MarkdownEditorShortcutMap = Record<string, () => void>;
 
 
 export type MarkdownEditorShortcutMap = Record<string, () => void>;
 
@@ -42,22 +41,3 @@ export function provideShortcutMap(editor: MarkdownEditor): MarkdownEditorShortc
 
     return shortcuts;
 }
 
     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;
-}