From: Dan Brown Date: Mon, 21 Jul 2025 10:49:58 +0000 (+0100) Subject: MD Editor: Started work on input interface X-Git-Url: http://source.bookstackapp.com/bookstack/commitdiff_plain/ec07793cda66d319bf716ef3dcbbe2fe19f0c355?ds=inline MD Editor: Started work on input interface Created implementation for codemirror, yet to use it. --- diff --git a/resources/js/global.d.ts b/resources/js/global.d.ts index b637c97c1..239f4b924 100644 --- a/resources/js/global.d.ts +++ b/resources/js/global.d.ts @@ -15,4 +15,6 @@ declare global { baseUrl: (path: string) => string; importVersioned: (module: string) => Promise; } -} \ No newline at end of file +} + +export type CodeModule = (typeof import('./code/index.mjs')); \ No newline at end of file diff --git a/resources/js/markdown/codemirror.ts b/resources/js/markdown/codemirror.ts index a3b81418f..1b54c5819 100644 --- a/resources/js/markdown/codemirror.ts +++ b/resources/js/markdown/codemirror.ts @@ -3,13 +3,12 @@ import {debounce} from '../services/util'; import {Clipboard} from '../services/clipboard'; import {EditorView, ViewUpdate} from "@codemirror/view"; 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 { - const Code = await window.importVersioned('code') as (typeof import('../code/index.mjs')); - +export function init(editor: MarkdownEditor, Code: CodeModule): EditorView { function onViewUpdate(v: ViewUpdate) { if (v.docChanged) { editor.actions.updateAndRender(); diff --git a/resources/js/markdown/index.mts b/resources/js/markdown/index.mts index d487b7972..b983285d9 100644 --- a/resources/js/markdown/index.mts +++ b/resources/js/markdown/index.mts @@ -5,6 +5,8 @@ import {Settings} from './settings'; import {listenToCommonEvents} from './common-events'; import {init as initCodemirror} from './codemirror'; import {EditorView} from "@codemirror/view"; +import {importVersioned} from "../services/util"; +import {CodeModule} from "../global"; export interface MarkdownEditorConfig { pageId: string; @@ -29,6 +31,8 @@ export interface MarkdownEditor { * Initiate a new Markdown editor instance. */ export async function init(config: MarkdownEditorConfig): Promise { + const Code = await window.importVersioned('code') as CodeModule; + const editor: MarkdownEditor = { config, markdown: new Markdown(), @@ -37,7 +41,7 @@ export async function init(config: MarkdownEditorConfig): Promise { + 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); + } + +} \ No newline at end of file diff --git a/resources/js/markdown/inputs/interface.ts b/resources/js/markdown/inputs/interface.ts new file mode 100644 index 000000000..aafd86f91 --- /dev/null +++ b/resources/js/markdown/inputs/interface.ts @@ -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; +} \ No newline at end of file