1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-07-28 17:02:04 +03:00

MD Editor: Added plaintext/cm switching

Also aligned the construction of the inputs where possible.
This commit is contained in:
Dan Brown
2025-07-22 10:34:29 +01:00
parent 6b4b500a33
commit d55db06c01
6 changed files with 82 additions and 43 deletions

View File

@ -1,25 +1,48 @@
import {provideKeyBindings} from './shortcuts'; import {EditorView, KeyBinding, ViewUpdate} from "@codemirror/view";
import {EditorView, ViewUpdate} from "@codemirror/view";
import {MarkdownEditor} from "./index.mjs";
import {CodeModule} from "../global"; import {CodeModule} from "../global";
import {MarkdownEditorEventMap} from "./dom-handlers"; import {MarkdownEditorEventMap} from "./dom-handlers";
import {MarkdownEditorShortcutMap} from "./shortcuts";
/**
* Convert editor shortcuts to CodeMirror keybinding format.
*/
export function shortcutsToKeyBindings(shortcuts: MarkdownEditorShortcutMap): KeyBinding[] {
const keyBindings = [];
const wrapAction = (action: () => void) => () => {
action();
return true;
};
for (const [shortcut, action] of Object.entries(shortcuts)) {
keyBindings.push({key: shortcut, run: wrapAction(action), preventDefault: true});
}
return keyBindings;
}
/** /**
* Initiate the codemirror instance for the Markdown editor. * Initiate the codemirror instance for the Markdown editor.
*/ */
export function init(editor: MarkdownEditor, Code: CodeModule, domEventHandlers: MarkdownEditorEventMap): EditorView { export async function init(
input: HTMLTextAreaElement,
shortcuts: MarkdownEditorShortcutMap,
domEventHandlers: MarkdownEditorEventMap,
onChange: () => void
): Promise<EditorView> {
const Code = await window.importVersioned('code') as CodeModule;
function onViewUpdate(v: ViewUpdate) { function onViewUpdate(v: ViewUpdate) {
if (v.docChanged) { if (v.docChanged) {
editor.actions.updateAndRender(); onChange();
} }
} }
const cm = Code.markdownEditor( const cm = Code.markdownEditor(
editor.config.inputEl, input,
onViewUpdate, onViewUpdate,
domEventHandlers, domEventHandlers,
provideKeyBindings(editor), shortcutsToKeyBindings(shortcuts),
); );
// Add editor view to the window for easy access/debugging. // Add editor view to the window for easy access/debugging.

View File

@ -4,7 +4,6 @@ import {Actions} from './actions';
import {Settings} from './settings'; 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 {CodeModule} from "../global";
import {MarkdownEditorInput} from "./inputs/interface"; import {MarkdownEditorInput} from "./inputs/interface";
import {CodemirrorInput} from "./inputs/codemirror"; import {CodemirrorInput} from "./inputs/codemirror";
import {TextareaInput} from "./inputs/textarea"; import {TextareaInput} from "./inputs/textarea";
@ -34,8 +33,6 @@ 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(),
@ -46,15 +43,25 @@ export async function init(config: MarkdownEditorConfig): Promise<MarkdownEditor
editor.display = new Display(editor); editor.display = new Display(editor);
const eventHandlers = getMarkdownDomEventHandlers(editor); const eventHandlers = getMarkdownDomEventHandlers(editor);
// TODO - Switching const shortcuts = provideShortcutMap(editor);
// const codeMirror = initCodemirror(editor, Code); const onInputChange = () => editor.actions.updateAndRender();
// editor.input = new CodemirrorInput(codeMirror);
editor.input = new TextareaInput(
config.inputEl,
provideShortcutMap(editor),
eventHandlers
);
const initCodemirrorInput: () => Promise<MarkdownEditorInput> = async () => {
const codeMirror = await initCodemirror(config.inputEl, shortcuts, eventHandlers, onInputChange);
return new CodemirrorInput(codeMirror);
};
const initTextAreaInput: () => Promise<MarkdownEditorInput> = async () => {
return new TextareaInput(config.inputEl, shortcuts, eventHandlers, onInputChange);
};
const isPlainEditor = Boolean(editor.settings.get('plainEditor'));
editor.input = await (isPlainEditor ? initTextAreaInput() : initCodemirrorInput());
editor.settings.onChange('plainEditor', async (value) => {
const isPlain = Boolean(value);
const newInput = await (isPlain ? initTextAreaInput() : initCodemirrorInput());
editor.input.teardown();
editor.input = newInput;
});
// window.devinput = editor.input; // window.devinput = editor.input;
listenToCommonEvents(editor); listenToCommonEvents(editor);

View File

@ -10,6 +10,10 @@ export class CodemirrorInput implements MarkdownEditorInput {
this.cm = cm; this.cm = cm;
} }
teardown(): void {
this.cm.destroy();
}
focus(): void { focus(): void {
if (!this.cm.hasFocus) { if (!this.cm.hasFocus) {
this.cm.focus(); this.cm.focus();

View File

@ -73,4 +73,9 @@ export interface MarkdownEditorInput {
* Search and return a line range which includes the provided text. * Search and return a line range which includes the provided text.
*/ */
searchForLineContaining(text: string): MarkdownEditorInputSelection|null; searchForLineContaining(text: string): MarkdownEditorInputSelection|null;
/**
* Tear down the input.
*/
teardown(): void;
} }

View File

@ -8,23 +8,43 @@ export class TextareaInput implements MarkdownEditorInput {
protected input: HTMLTextAreaElement; protected input: HTMLTextAreaElement;
protected shortcuts: MarkdownEditorShortcutMap; protected shortcuts: MarkdownEditorShortcutMap;
protected events: MarkdownEditorEventMap; protected events: MarkdownEditorEventMap;
protected onChange: () => void;
protected eventController = new AbortController();
constructor(input: HTMLTextAreaElement, shortcuts: MarkdownEditorShortcutMap, events: MarkdownEditorEventMap) { constructor(
input: HTMLTextAreaElement,
shortcuts: MarkdownEditorShortcutMap,
events: MarkdownEditorEventMap,
onChange: () => void
) {
this.input = input; this.input = input;
this.shortcuts = shortcuts; this.shortcuts = shortcuts;
this.events = events; this.events = events;
this.onChange = onChange;
this.onKeyDown = this.onKeyDown.bind(this); this.onKeyDown = this.onKeyDown.bind(this);
this.configureListeners(); this.configureListeners();
this.input.style.removeProperty("display");
}
teardown() {
this.eventController.abort('teardown');
} }
configureListeners(): void { configureListeners(): void {
// TODO - Teardown handling // Keyboard shortcuts
this.input.addEventListener('keydown', this.onKeyDown); this.input.addEventListener('keydown', this.onKeyDown, {signal: this.eventController.signal});
// Shared event listeners
for (const [name, listener] of Object.entries(this.events)) { for (const [name, listener] of Object.entries(this.events)) {
this.input.addEventListener(name, listener); this.input.addEventListener(name, listener, {signal: this.eventController.signal});
} }
// Input change handling
this.input.addEventListener('input', () => {
this.onChange();
}, {signal: this.eventController.signal});
} }
onKeyDown(e: KeyboardEvent) { onKeyDown(e: KeyboardEvent) {

View File

@ -1,5 +1,4 @@
import {MarkdownEditor} from "./index.mjs"; import {MarkdownEditor} from "./index.mjs";
import {KeyBinding} from "@codemirror/view";
export type MarkdownEditorShortcutMap = Record<string, () => void>; export type MarkdownEditorShortcutMap = Record<string, () => void>;
@ -42,22 +41,3 @@ export function provideShortcutMap(editor: MarkdownEditor): MarkdownEditorShortc
return shortcuts; return shortcuts;
} }
/**
* Get the editor shortcuts in CodeMirror keybinding format.
*/
export function provideKeyBindings(editor: MarkdownEditor): KeyBinding[] {
const shortcuts = provideShortcutMap(editor);
const keyBindings = [];
const wrapAction = (action: ()=>void) => () => {
action();
return true;
};
for (const [shortcut, action] of Object.entries(shortcuts)) {
keyBindings.push({key: shortcut, run: wrapAction(action), preventDefault: true});
}
return keyBindings;
}