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