mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-28 17:02:04 +03:00
MD Editor: Started work on input interface
Created implementation for codemirror, yet to use it.
This commit is contained in:
2
resources/js/global.d.ts
vendored
2
resources/js/global.d.ts
vendored
@ -16,3 +16,5 @@ declare global {
|
|||||||
importVersioned: (module: string) => Promise<object>;
|
importVersioned: (module: string) => Promise<object>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CodeModule = (typeof import('./code/index.mjs'));
|
@ -3,13 +3,12 @@ import {debounce} from '../services/util';
|
|||||||
import {Clipboard} from '../services/clipboard';
|
import {Clipboard} from '../services/clipboard';
|
||||||
import {EditorView, ViewUpdate} from "@codemirror/view";
|
import {EditorView, ViewUpdate} from "@codemirror/view";
|
||||||
import {MarkdownEditor} from "./index.mjs";
|
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> {
|
export function init(editor: MarkdownEditor, Code: CodeModule): EditorView {
|
||||||
const Code = await window.importVersioned('code') as (typeof import('../code/index.mjs'));
|
|
||||||
|
|
||||||
function onViewUpdate(v: ViewUpdate) {
|
function onViewUpdate(v: ViewUpdate) {
|
||||||
if (v.docChanged) {
|
if (v.docChanged) {
|
||||||
editor.actions.updateAndRender();
|
editor.actions.updateAndRender();
|
||||||
|
@ -5,6 +5,8 @@ import {Settings} from './settings';
|
|||||||
import {listenToCommonEvents} from './common-events';
|
import {listenToCommonEvents} from './common-events';
|
||||||
import {init as initCodemirror} from './codemirror';
|
import {init as initCodemirror} from './codemirror';
|
||||||
import {EditorView} from "@codemirror/view";
|
import {EditorView} from "@codemirror/view";
|
||||||
|
import {importVersioned} from "../services/util";
|
||||||
|
import {CodeModule} from "../global";
|
||||||
|
|
||||||
export interface MarkdownEditorConfig {
|
export interface MarkdownEditorConfig {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
@ -29,6 +31,8 @@ export interface MarkdownEditor {
|
|||||||
* Initiate a new Markdown editor instance.
|
* Initiate a new Markdown editor instance.
|
||||||
*/
|
*/
|
||||||
export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor> {
|
export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor> {
|
||||||
|
const Code = await window.importVersioned('code') as CodeModule;
|
||||||
|
|
||||||
const editor: MarkdownEditor = {
|
const editor: MarkdownEditor = {
|
||||||
config,
|
config,
|
||||||
markdown: new Markdown(),
|
markdown: new Markdown(),
|
||||||
@ -37,7 +41,7 @@ export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor
|
|||||||
|
|
||||||
editor.actions = new Actions(editor);
|
editor.actions = new Actions(editor);
|
||||||
editor.display = new Display(editor);
|
editor.display = new Display(editor);
|
||||||
editor.cm = await initCodemirror(editor);
|
editor.cm = initCodemirror(editor, Code);
|
||||||
|
|
||||||
listenToCommonEvents(editor);
|
listenToCommonEvents(editor);
|
||||||
|
|
||||||
|
120
resources/js/markdown/inputs/codemirror.ts
Normal file
120
resources/js/markdown/inputs/codemirror.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
71
resources/js/markdown/inputs/interface.ts
Normal file
71
resources/js/markdown/inputs/interface.ts
Normal file
@ -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;
|
||||||
|
}
|
Reference in New Issue
Block a user