Skip to content

Commit ec07793

Browse files
committed
MD Editor: Started work on input interface
Created implementation for codemirror, yet to use it.
1 parent 61adc73 commit ec07793

File tree

5 files changed

+202
-6
lines changed

5 files changed

+202
-6
lines changed

resources/js/global.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ declare global {
1515
baseUrl: (path: string) => string;
1616
importVersioned: (module: string) => Promise<object>;
1717
}
18-
}
18+
}
19+
20+
export type CodeModule = (typeof import('./code/index.mjs'));

resources/js/markdown/codemirror.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ import {debounce} from '../services/util';
33
import {Clipboard} from '../services/clipboard';
44
import {EditorView, ViewUpdate} from "@codemirror/view";
55
import {MarkdownEditor} from "./index.mjs";
6+
import {CodeModule} from "../global";
67

78
/**
8-
* Initiate the codemirror instance for the markdown editor.
9+
* Initiate the codemirror instance for the MarkDown editor.
910
*/
10-
export async function init(editor: MarkdownEditor): Promise<EditorView> {
11-
const Code = await window.importVersioned('code') as (typeof import('../code/index.mjs'));
12-
11+
export function init(editor: MarkdownEditor, Code: CodeModule): EditorView {
1312
function onViewUpdate(v: ViewUpdate) {
1413
if (v.docChanged) {
1514
editor.actions.updateAndRender();

resources/js/markdown/index.mts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {Settings} from './settings';
55
import {listenToCommonEvents} from './common-events';
66
import {init as initCodemirror} from './codemirror';
77
import {EditorView} from "@codemirror/view";
8+
import {importVersioned} from "../services/util";
9+
import {CodeModule} from "../global";
810

911
export interface MarkdownEditorConfig {
1012
pageId: string;
@@ -29,6 +31,8 @@ export interface MarkdownEditor {
2931
* Initiate a new Markdown editor instance.
3032
*/
3133
export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor> {
34+
const Code = await window.importVersioned('code') as CodeModule;
35+
3236
const editor: MarkdownEditor = {
3337
config,
3438
markdown: new Markdown(),
@@ -37,7 +41,7 @@ export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor
3741

3842
editor.actions = new Actions(editor);
3943
editor.display = new Display(editor);
40-
editor.cm = await initCodemirror(editor);
44+
editor.cm = initCodemirror(editor, Code);
4145

4246
listenToCommonEvents(editor);
4347

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
2+
export interface MarkdownEditorInputSelection {
3+
from: number;
4+
to: number;
5+
}
6+
7+
export interface MarkdownEditorInput {
8+
/**
9+
* Focus on the editor.
10+
*/
11+
focus(): void;
12+
13+
/**
14+
* Get the current selection range.
15+
*/
16+
getSelection(): MarkdownEditorInputSelection;
17+
18+
/**
19+
* Get the text of the given (or current) selection range.
20+
*/
21+
getSelectionText(selection: MarkdownEditorInputSelection|null = null): string;
22+
23+
/**
24+
* Set the selection range of the editor.
25+
*/
26+
setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean = false): void;
27+
28+
/**
29+
* Get the full text of the input.
30+
*/
31+
getText(): string;
32+
33+
/**
34+
* Get just the text which is above (out) the current view range.
35+
* This is used for position estimation.
36+
*/
37+
getTextAboveView(): string;
38+
39+
/**
40+
* Set the full text of the input.
41+
* Optionally can provide a selection to restore after setting text.
42+
*/
43+
setText(text: string, selection: MarkdownEditorInputSelection|null = null): void;
44+
45+
/**
46+
* Splice in/out text within the input.
47+
* Optionally can provide a selection to restore after setting text.
48+
*/
49+
spliceText(from: number, to: number, newText: string, selection: MarkdownEditorInputSelection|null = null): void;
50+
51+
/**
52+
* Append text to the end of the editor.
53+
*/
54+
appendText(text: string): void;
55+
56+
/**
57+
* Get the text of the given line number otherwise the text
58+
* of the current selected line.
59+
*/
60+
getLineText(lineIndex:number = -1): string;
61+
62+
/**
63+
* Wrap the current line in the given start/end contents.
64+
*/
65+
wrapLine(start: string, end: string): void;
66+
67+
/**
68+
* Convert the given screen coords to a selection position within the input.
69+
*/
70+
coordsToSelection(x: number, y: number): MarkdownEditorInputSelection;
71+
}

0 commit comments

Comments
 (0)