]> BookStack Code Mirror - bookstack/commitdiff
MD Editor: Started work on input interface
authorDan Brown <redacted>
Mon, 21 Jul 2025 10:49:58 +0000 (11:49 +0100)
committerDan Brown <redacted>
Mon, 21 Jul 2025 10:49:58 +0000 (11:49 +0100)
Created implementation for codemirror, yet to use it.

resources/js/global.d.ts
resources/js/markdown/codemirror.ts
resources/js/markdown/index.mts
resources/js/markdown/inputs/codemirror.ts [new file with mode: 0644]
resources/js/markdown/inputs/interface.ts [new file with mode: 0644]

index b637c97c1b92a7cf55dd3556c427d4e0e1992e54..239f4b9249a1a13ad268bf1ea667effc3dcdb062 100644 (file)
@@ -15,4 +15,6 @@ declare global {
         baseUrl: (path: string) => string;
         importVersioned: (module: string) => Promise<object>;
     }
         baseUrl: (path: string) => string;
         importVersioned: (module: string) => Promise<object>;
     }
-}
\ No newline at end of file
+}
+
+export type CodeModule = (typeof import('./code/index.mjs'));
\ No newline at end of file
index a3b81418f9f5128aebf6779fbae5da8291427fce..1b54c58196b4372fa27ded2b70098bb088c4aa0a 100644 (file)
@@ -3,13 +3,12 @@ import {debounce} from '../services/util';
 import {Clipboard} from '../services/clipboard';
 import {EditorView, ViewUpdate} from "@codemirror/view";
 import {MarkdownEditor} from "./index.mjs";
 import {Clipboard} from '../services/clipboard';
 import {EditorView, ViewUpdate} from "@codemirror/view";
 import {MarkdownEditor} from "./index.mjs";
+import {CodeModule} from "../global";
 
 /**
 
 /**
- * Initiate the codemirror instance for the markdown editor.
+ * Initiate the codemirror instance for the MarkDown editor.
  */
  */
-export async function init(editor: MarkdownEditor): Promise<EditorView> {
-    const Code = await window.importVersioned('code') as (typeof import('../code/index.mjs'));
-
+export function init(editor: MarkdownEditor, Code: CodeModule): EditorView {
     function onViewUpdate(v: ViewUpdate) {
         if (v.docChanged) {
             editor.actions.updateAndRender();
     function onViewUpdate(v: ViewUpdate) {
         if (v.docChanged) {
             editor.actions.updateAndRender();
index d487b7972e878b4130a0a3072a8d212264e69438..b983285d90785f17efd4246e12bd1027ec0d579b 100644 (file)
@@ -5,6 +5,8 @@ import {Settings} from './settings';
 import {listenToCommonEvents} from './common-events';
 import {init as initCodemirror} from './codemirror';
 import {EditorView} from "@codemirror/view";
 import {listenToCommonEvents} from './common-events';
 import {init as initCodemirror} from './codemirror';
 import {EditorView} from "@codemirror/view";
+import {importVersioned} from "../services/util";
+import {CodeModule} from "../global";
 
 export interface MarkdownEditorConfig {
     pageId: string;
 
 export interface MarkdownEditorConfig {
     pageId: string;
@@ -29,6 +31,8 @@ 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(),
@@ -37,7 +41,7 @@ export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor
 
     editor.actions = new Actions(editor);
     editor.display = new Display(editor);
 
     editor.actions = new Actions(editor);
     editor.display = new Display(editor);
-    editor.cm = await initCodemirror(editor);
+    editor.cm = initCodemirror(editor, Code);
 
     listenToCommonEvents(editor);
 
 
     listenToCommonEvents(editor);
 
diff --git a/resources/js/markdown/inputs/codemirror.ts b/resources/js/markdown/inputs/codemirror.ts
new file mode 100644 (file)
index 0000000..8fc6b68
--- /dev/null
@@ -0,0 +1,120 @@
+import {MarkdownEditorInput, MarkdownEditorInputSelection} from "./interface";
+import {MarkdownEditor} from "../index.mjs";
+import {EditorView} from "@codemirror/view";
+import {ChangeSpec, SelectionRange, TransactionSpec} from "@codemirror/state";
+
+
+export class CodemirrorInput implements MarkdownEditorInput {
+
+    protected editor: MarkdownEditor;
+    protected cm: EditorView;
+
+    constructor(cm: EditorView) {
+        this.cm = cm;
+    }
+
+    focus(): void {
+        if (!this.editor.cm.hasFocus) {
+            this.editor.cm.focus();
+        }
+    }
+
+    getSelection(): MarkdownEditorInputSelection {
+        return this.editor.cm.state.selection.main;
+    }
+
+    getSelectionText(selection: MarkdownEditorInputSelection|null = null): string {
+        selection = selection || this.getSelection();
+        return this.editor.cm.state.sliceDoc(selection.from, selection.to);
+    }
+
+    setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean = false) {
+        this.editor.cm.dispatch({
+            selection: {anchor: selection.from, head: selection.to},
+            scrollIntoView,
+        });
+    }
+
+    getText(): string {
+        return this.editor.cm.state.doc.toString();
+    }
+
+    getTextAboveView(): string {
+        const blockInfo = this.editor.cm.lineBlockAtHeight(scrollEl.scrollTop);
+        return this.editor.cm.state.sliceDoc(0, blockInfo.from);
+    }
+
+    setText(text: string, selection: MarkdownEditorInputSelection | null = null) {
+        selection = selection || this.getSelection();
+        const newDoc = this.editor.cm.state.toText(text);
+        const newSelectFrom = Math.min(selection.from, newDoc.length);
+        const scrollTop = this.editor.cm.scrollDOM.scrollTop;
+        this.dispatchChange(0, this.editor.cm.state.doc.length, text, newSelectFrom);
+        this.focus();
+        window.requestAnimationFrame(() => {
+            this.editor.cm.scrollDOM.scrollTop = scrollTop;
+        });
+    }
+
+    spliceText(from: number, to: number, newText: string, selection: MarkdownEditorInputSelection | null = null) {
+        const end = (selection?.from === selection?.to) ? null : selection?.to;
+        this.dispatchChange(from, to, newText, selection?.from, end)
+    }
+
+    appendText(text: string) {
+        const end = this.editor.cm.state.doc.length;
+        this.dispatchChange(end, end, `\n${text}`);
+    }
+
+    getLineText(lineIndex: number = -1): string {
+        const index = lineIndex > -1 ? lineIndex : this.getSelection().from;
+        return this.editor.cm.state.doc.lineAt(index).text;
+    }
+
+    wrapLine(start: string, end: string) {
+        const selectionRange = this.getSelection();
+        const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
+        const lineContent = line.text;
+        let newLineContent;
+        let lineOffset = 0;
+
+        if (lineContent.startsWith(start) && lineContent.endsWith(end)) {
+            newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
+            lineOffset = -(start.length);
+        } else {
+            newLineContent = `${start}${lineContent}${end}`;
+            lineOffset = start.length;
+        }
+
+        this.dispatchChange(line.from, line.to, newLineContent, selectionRange.from + lineOffset);
+    }
+
+    coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
+        const cursorPos = this.editor.cm.posAtCoords({x, y}, false);
+        return {from: cursorPos, to: cursorPos};
+    }
+
+    /**
+     * Dispatch changes to the editor.
+     */
+    protected dispatchChange(from: number, to: number|null = null, text: string|null = null, selectFrom: number|null = null, selectTo: number|null = null): void {
+        const change: ChangeSpec = {from};
+        if (to) {
+            change.to = to;
+        }
+        if (text) {
+            change.insert = text;
+        }
+        const tr: TransactionSpec = {changes: change};
+
+        if (selectFrom) {
+            tr.selection = {anchor: selectFrom};
+            if (selectTo) {
+                tr.selection.head = selectTo;
+            }
+        }
+
+        this.cm.dispatch(tr);
+    }
+
+}
\ No newline at end of file
diff --git a/resources/js/markdown/inputs/interface.ts b/resources/js/markdown/inputs/interface.ts
new file mode 100644 (file)
index 0000000..aafd86f
--- /dev/null
@@ -0,0 +1,71 @@
+
+export interface MarkdownEditorInputSelection {
+    from: number;
+    to: number;
+}
+
+export interface MarkdownEditorInput {
+    /**
+     * Focus on the editor.
+     */
+    focus(): void;
+
+    /**
+     * Get the current selection range.
+     */
+    getSelection(): MarkdownEditorInputSelection;
+
+    /**
+     * Get the text of the given (or current) selection range.
+     */
+    getSelectionText(selection: MarkdownEditorInputSelection|null = null): string;
+
+    /**
+     * Set the selection range of the editor.
+     */
+    setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean = false): void;
+
+    /**
+     * Get the full text of the input.
+     */
+    getText(): string;
+
+    /**
+     * Get just the text which is above (out) the current view range.
+     * This is used for position estimation.
+     */
+    getTextAboveView(): string;
+
+    /**
+     * Set the full text of the input.
+     * Optionally can provide a selection to restore after setting text.
+     */
+    setText(text: string, selection: MarkdownEditorInputSelection|null = null): void;
+
+    /**
+     * Splice in/out text within the input.
+     * Optionally can provide a selection to restore after setting text.
+     */
+    spliceText(from: number, to: number, newText: string, selection: MarkdownEditorInputSelection|null = null): void;
+
+    /**
+     * Append text to the end of the editor.
+     */
+    appendText(text: string): void;
+
+    /**
+     * Get the text of the given line number otherwise the text
+     * of the current selected line.
+     */
+    getLineText(lineIndex:number = -1): string;
+
+    /**
+     * Wrap the current line in the given start/end contents.
+     */
+    wrapLine(start: string, end: string): void;
+
+    /**
+     * Convert the given screen coords to a selection position within the input.
+     */
+    coordsToSelection(x: number, y: number): MarkdownEditorInputSelection;
+}
\ No newline at end of file