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