]> BookStack Code Mirror - bookstack/blob - resources/js/markdown/inputs/codemirror.ts
MD Editor: Started work on input interface
[bookstack] / resources / js / markdown / inputs / codemirror.ts
1 import {MarkdownEditorInput, MarkdownEditorInputSelection} from "./interface";
2 import {MarkdownEditor} from "../index.mjs";
3 import {EditorView} from "@codemirror/view";
4 import {ChangeSpec, SelectionRange, TransactionSpec} from "@codemirror/state";
5
6
7 export class CodemirrorInput implements MarkdownEditorInput {
8
9     protected editor: MarkdownEditor;
10     protected cm: EditorView;
11
12     constructor(cm: EditorView) {
13         this.cm = cm;
14     }
15
16     focus(): void {
17         if (!this.editor.cm.hasFocus) {
18             this.editor.cm.focus();
19         }
20     }
21
22     getSelection(): MarkdownEditorInputSelection {
23         return this.editor.cm.state.selection.main;
24     }
25
26     getSelectionText(selection: MarkdownEditorInputSelection|null = null): string {
27         selection = selection || this.getSelection();
28         return this.editor.cm.state.sliceDoc(selection.from, selection.to);
29     }
30
31     setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean = false) {
32         this.editor.cm.dispatch({
33             selection: {anchor: selection.from, head: selection.to},
34             scrollIntoView,
35         });
36     }
37
38     getText(): string {
39         return this.editor.cm.state.doc.toString();
40     }
41
42     getTextAboveView(): string {
43         const blockInfo = this.editor.cm.lineBlockAtHeight(scrollEl.scrollTop);
44         return this.editor.cm.state.sliceDoc(0, blockInfo.from);
45     }
46
47     setText(text: string, selection: MarkdownEditorInputSelection | null = null) {
48         selection = selection || this.getSelection();
49         const newDoc = this.editor.cm.state.toText(text);
50         const newSelectFrom = Math.min(selection.from, newDoc.length);
51         const scrollTop = this.editor.cm.scrollDOM.scrollTop;
52         this.dispatchChange(0, this.editor.cm.state.doc.length, text, newSelectFrom);
53         this.focus();
54         window.requestAnimationFrame(() => {
55             this.editor.cm.scrollDOM.scrollTop = scrollTop;
56         });
57     }
58
59     spliceText(from: number, to: number, newText: string, selection: MarkdownEditorInputSelection | null = null) {
60         const end = (selection?.from === selection?.to) ? null : selection?.to;
61         this.dispatchChange(from, to, newText, selection?.from, end)
62     }
63
64     appendText(text: string) {
65         const end = this.editor.cm.state.doc.length;
66         this.dispatchChange(end, end, `\n${text}`);
67     }
68
69     getLineText(lineIndex: number = -1): string {
70         const index = lineIndex > -1 ? lineIndex : this.getSelection().from;
71         return this.editor.cm.state.doc.lineAt(index).text;
72     }
73
74     wrapLine(start: string, end: string) {
75         const selectionRange = this.getSelection();
76         const line = this.editor.cm.state.doc.lineAt(selectionRange.from);
77         const lineContent = line.text;
78         let newLineContent;
79         let lineOffset = 0;
80
81         if (lineContent.startsWith(start) && lineContent.endsWith(end)) {
82             newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
83             lineOffset = -(start.length);
84         } else {
85             newLineContent = `${start}${lineContent}${end}`;
86             lineOffset = start.length;
87         }
88
89         this.dispatchChange(line.from, line.to, newLineContent, selectionRange.from + lineOffset);
90     }
91
92     coordsToSelection(x: number, y: number): MarkdownEditorInputSelection {
93         const cursorPos = this.editor.cm.posAtCoords({x, y}, false);
94         return {from: cursorPos, to: cursorPos};
95     }
96
97     /**
98      * Dispatch changes to the editor.
99      */
100     protected dispatchChange(from: number, to: number|null = null, text: string|null = null, selectFrom: number|null = null, selectTo: number|null = null): void {
101         const change: ChangeSpec = {from};
102         if (to) {
103             change.to = to;
104         }
105         if (text) {
106             change.insert = text;
107         }
108         const tr: TransactionSpec = {changes: change};
109
110         if (selectFrom) {
111             tr.selection = {anchor: selectFrom};
112             if (selectTo) {
113                 tr.selection.head = selectTo;
114             }
115         }
116
117         this.cm.dispatch(tr);
118     }
119
120 }